feat: better composer etc

This commit is contained in:
Brooklyn Nicholson
2026-05-04 22:19:16 -05:00
parent 42db075e10
commit fcce49db3f
50 changed files with 1299 additions and 668 deletions
+6 -1
View File
@@ -24,7 +24,12 @@ interface FileBrowserPaneProps {
export function FileBrowserPane({ onActivateFile, onChangeCwd }: FileBrowserPaneProps) {
const currentCwd = useStore($currentCwd).trim()
const hasCwd = currentCwd.length > 0
const cwdName = hasCwd ? (currentCwd.split(/[\\/]+/).filter(Boolean).pop() ?? currentCwd) : 'No folder selected'
const cwdName = hasCwd
? (currentCwd
.split(/[\\/]+/)
.filter(Boolean)
.pop() ?? currentCwd)
: 'No folder selected'
const { data, loadChildren, openState, refreshRoot, rootError, rootLoading, setNodeOpen } = useProjectTree(currentCwd)
const chooseFolder = async () => {
+29 -11
View File
@@ -17,7 +17,9 @@ function decodeDataUrl(dataUrl: string) {
const data = match?.[1] || ''
const isBase64 = dataUrl.slice(0, dataUrl.indexOf(',')).includes(';base64')
if (!isBase64) {return decodeURIComponent(data)}
if (!isBase64) {
return decodeURIComponent(data)
}
const bytes = Uint8Array.from(atob(data), ch => ch.charCodeAt(0))
@@ -33,7 +35,9 @@ function relativeTo(root: string, child: string) {
const r = clean(root)
const c = clean(child)
if (c === r) {return ''}
if (c === r) {
return ''
}
return c.startsWith(`${r}/`) ? c.slice(r.length + 1) : null
}
@@ -43,7 +47,9 @@ function ancestorDirs(root: string, dir: string) {
const r = clean(root)
const rel = relativeTo(r, dir)
if (rel === null || rel === '') {return [r]}
if (rel === null || rel === '') {
return [r]
}
const dirs = [r]
let current = r
@@ -57,7 +63,9 @@ function ancestorDirs(root: string, dir: string) {
}
async function gitRootFor(start: string) {
if (!window.hermesDesktop?.gitRoot) {return null}
if (!window.hermesDesktop?.gitRoot) {
return null
}
const key = clean(start)
let cached = gitRootCache.get(key)
@@ -72,12 +80,16 @@ async function gitRootFor(start: string) {
/** Read .gitignore at `dir` if it actually exists — never probe missing files. */
async function readGitignore(dir: string): Promise<GitignoreRule | null> {
if (!window.hermesDesktop?.readDir || !window.hermesDesktop.readFileDataUrl) {return null}
if (!window.hermesDesktop?.readDir || !window.hermesDesktop.readFileDataUrl) {
return null
}
try {
const listing = await window.hermesDesktop.readDir(dir)
if (!listing.entries.some(e => e.name === '.gitignore' && !e.isDirectory)) {return null}
if (!listing.entries.some(e => e.name === '.gitignore' && !e.isDirectory)) {
return null
}
const text = decodeDataUrl(await window.hermesDesktop.readFileDataUrl(`${dir}/.gitignore`))
@@ -103,7 +115,9 @@ function ignoredBy(rules: GitignoreRule[], entry: HermesReadDirEntry) {
return rules.some(rule => {
const rel = relativeTo(rule.base, entry.path)
if (rel === null || rel === '') {return false}
if (rel === null || rel === '') {
return false
}
return rule.ig.ignores(entry.isDirectory ? `${rel}/` : rel)
})
@@ -112,17 +126,21 @@ function ignoredBy(rules: GitignoreRule[], entry: HermesReadDirEntry) {
async function filterIgnored(entries: HermesReadDirEntry[], rootPath: string, dirPath: string) {
const root = await gitRootFor(rootPath)
if (!root) {return entries}
if (!root) {
return entries
}
const rules = (await Promise.all(ancestorDirs(root, dirPath).map(gitignoreFor))).filter(
(r): r is GitignoreRule => Boolean(r)
const rules = (await Promise.all(ancestorDirs(root, dirPath).map(gitignoreFor))).filter((r): r is GitignoreRule =>
Boolean(r)
)
return rules.length > 0 ? entries.filter(entry => !ignoredBy(rules, entry)) : entries
}
export async function readProjectDir(dirPath: string, rootPath = dirPath): Promise<HermesReadDirResult> {
if (!window.hermesDesktop) {return { entries: [], error: 'no-bridge' }}
if (!window.hermesDesktop) {
return { entries: [], error: 'no-bridge' }
}
const result = await window.hermesDesktop.readDir(dirPath)
+9 -3
View File
@@ -33,7 +33,9 @@ export function ProjectTree({
useEffect(() => {
const el = containerRef.current
if (!el || typeof ResizeObserver === 'undefined') {return}
if (!el || typeof ResizeObserver === 'undefined') {
return
}
const observer = new ResizeObserver(([entry]) => {
const { height, width } = entry.contentRect
@@ -49,7 +51,9 @@ export function ProjectTree({
(id: string) => {
const node = treeRef.current?.get(id)
if (!node) {return}
if (!node) {
return
}
onNodeOpenChange(id, node.isOpen)
@@ -121,7 +125,9 @@ function ProjectTreeRow({
onClick={event => {
event.stopPropagation()
if (isPlaceholder) {return}
if (isPlaceholder) {
return
}
if (isFolder) {
node.toggle()
@@ -25,10 +25,14 @@ function makeNode(path: string, name: string, isDirectory: boolean): TreeNode {
}
function patchNode(nodes: TreeNode[] | undefined | null, id: string, patch: (n: TreeNode) => TreeNode): TreeNode[] {
if (!nodes) {return []}
if (!nodes) {
return []
}
return nodes.map(n => {
if (n.id === id) {return patch(n)}
if (n.id === id) {
return patch(n)
}
if (n.children && n.children.length > 0) {
return { ...n, children: patchNode(n.children, id, patch) }
@@ -170,37 +174,46 @@ export function useProjectTree(cwd: string): UseProjectTreeResult {
[cwd]
)
const loadChildren = useCallback(async (id: string) => {
if (!cwd || inflight.has(id)) {return}
inflight.add(id)
setProjectTree(current => {
if (current.cwd !== cwd) {return current}
return {
...current,
data: patchNode(current.data, id, n => ({ ...n, loading: true, children: [placeholderChild(n.id)] }))
const loadChildren = useCallback(
async (id: string) => {
if (!cwd || inflight.has(id)) {
return
}
})
inflight.add(id)
const { entries, error } = await readProjectDir(id, cwd)
setProjectTree(current => {
if (current.cwd !== cwd) {
return current
}
inflight.delete(id)
return {
...current,
data: patchNode(current.data, id, n => ({ ...n, loading: true, children: [placeholderChild(n.id)] }))
}
})
setProjectTree(current => {
if (current.cwd !== cwd) {return current}
const { entries, error } = await readProjectDir(id, cwd)
return {
...current,
data: patchNode(current.data, id, n => ({
...n,
loading: false,
error: error || undefined,
children: error ? [] : entries.map(e => makeNode(e.path, e.name, e.isDirectory))
}))
}
})
}, [cwd])
inflight.delete(id)
setProjectTree(current => {
if (current.cwd !== cwd) {
return current
}
return {
...current,
data: patchNode(current.data, id, n => ({
...n,
loading: false,
error: error || undefined,
children: error ? [] : entries.map(e => makeNode(e.path, e.name, e.isDirectory))
}))
}
})
},
[cwd]
)
useEffect(() => {
void loadRoot(cwd)
@@ -216,6 +229,16 @@ export function useProjectTree(cwd: string): UseProjectTreeResult {
rootLoading: state.cwd === cwd ? state.rootLoading : false,
setNodeOpen
}),
[cwd, loadChildren, refreshRoot, setNodeOpen, state.cwd, state.data, state.openState, state.rootError, state.rootLoading]
[
cwd,
loadChildren,
refreshRoot,
setNodeOpen,
state.cwd,
state.data,
state.openState,
state.rootError,
state.rootLoading
]
)
}