From 1fe260ca3e5ba11ad1b73fcb219a886776e7750b Mon Sep 17 00:00:00 2001 From: "mingholy.lmh" Date: Mon, 8 Jun 2026 21:23:03 +0800 Subject: [PATCH 1/2] Add inline vault directory create action --- web/src/components/Tree.tsx | 28 ++++++++++++++++++++++-- web/src/pages/ContextPage.tsx | 40 +++++++++++++++++++++++++++++------ 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/web/src/components/Tree.tsx b/web/src/components/Tree.tsx index d441fb6d..75118168 100644 --- a/web/src/components/Tree.tsx +++ b/web/src/components/Tree.tsx @@ -64,6 +64,8 @@ export type TreeProps = { nodeClassName?: (node: TreeNodeData, depth: number, isOpen: boolean, isPicked: boolean) => string /** Bump to force re-fetch children for all nodes */ reloadKey?: number + /** Called when the inline add button on a directory row is clicked */ + onAddInDir?: (node: TreeNodeData) => void } export function Tree({ @@ -78,6 +80,7 @@ export function Tree({ renderNode, nodeClassName, reloadKey, + onAddInDir, }: TreeProps) { return ( <> @@ -95,6 +98,7 @@ export function Tree({ renderNode={renderNode} nodeClassName={nodeClassName} reloadKey={reloadKey} + onAddInDir={onAddInDir} /> ))} @@ -113,6 +117,7 @@ function TreeNode({ renderNode, nodeClassName, reloadKey, + onAddInDir, }: { treeId: string entry: TreeNodeData @@ -125,6 +130,7 @@ function TreeNode({ renderNode?: (node: TreeNodeData, depth: number, isOpen: boolean, toggleOpen: () => void) => ReactNode nodeClassName?: (node: TreeNodeData, depth: number, isOpen: boolean, isPicked: boolean) => string reloadKey?: number + onAddInDir?: (node: TreeNodeData) => void }) { const [open, setOpen] = useState(() => getExpanded(treeId).has(entry.path)) const [children, setChildren] = useState(null) @@ -187,6 +193,7 @@ function TreeNode({ renderNode={renderNode} nodeClassName={nodeClassName} reloadKey={reloadKey} + onAddInDir={onAddInDir} /> ))} @@ -206,7 +213,7 @@ function TreeNode({ return ( <> -
+
+ {onAddInDir && ( + + )}
{loading && (
@@ -242,6 +265,7 @@ function TreeNode({ renderNode={renderNode} nodeClassName={nodeClassName} reloadKey={reloadKey} + onAddInDir={onAddInDir} /> ))} {children.length === 0 && ( diff --git a/web/src/pages/ContextPage.tsx b/web/src/pages/ContextPage.tsx index 73581a63..28271e60 100644 --- a/web/src/pages/ContextPage.tsx +++ b/web/src/pages/ContextPage.tsx @@ -266,7 +266,7 @@ function VaultPane({ vault, initialFile, initialEditing }: { vault: VaultId; ini } }, [pickedPath, searchParams, setSearchParams]) const [reloadKey, setReloadKey] = useState(0) - const [showNewFile, setShowNewFile] = useState(false) + const [showNewFile, setShowNewFile] = useState<{ initialPath: string } | null>(null) const [query, setQuery] = useState("") const [sidebarOpen, setSidebarOpen] = useState(false) const [creating, setCreating] = useState<{ type: "file" | "folder"; path: string } | null>(null) @@ -340,7 +340,7 @@ function VaultPane({ vault, initialFile, initialEditing }: { vault: VaultId; ini const onCreate = async (path: string) => { const r = await vaultCreateFile(vault, path) if (r.ok) { - setShowNewFile(false) + setShowNewFile(null) setReloadKey((k) => k + 1) setPickedPath(path) } else { @@ -348,6 +348,10 @@ function VaultPane({ vault, initialFile, initialEditing }: { vault: VaultId; ini } } + const handleAddInDir = useCallback((node: TreeNodeData) => { + setShowNewFile({ initialPath: node.path ? node.path + "/" : "" }) + }, []) + const handleCreate = async () => { if (!creating || !newName.trim()) { setCreating(null); return } const targetPath = creating.path + "/" + newName.trim() @@ -448,7 +452,7 @@ function VaultPane({ vault, initialFile, initialEditing }: { vault: VaultId; ini className="flex-1 min-w-0 bg-transparent outline-none text-[12px] text-gray-700 placeholder:text-gray-400" />
)} - {showNewFile && setShowNewFile(false)} onCreate={onCreate} />} + {showNewFile && ( + setShowNewFile(null)} + onCreate={onCreate} + /> + )} {creating && ( void onCreate: (path: string) => void }) { - const [path, setPath] = useState("") + const [path, setPath] = useState(initialPath) + const inputRef = useRef(null) + + useEffect(() => { + const input = inputRef.current + if (!input) return + input.focus() + const end = input.value.length + input.setSelectionRange(end, end) + }, []) + return (
new file in {vault}/
setPath(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && path.trim()) onCreate(path.trim()) + if (e.key === "Escape") onClose() + }} placeholder="loopat/new-doc.md" className="w-full px-3 py-2 text-sm border border-gray-300 rounded outline-none focus:border-gray-500 font-mono" /> From 038b119a3eb9f6a4c45b96b985097279a3149b6d Mon Sep 17 00:00:00 2001 From: "mingholy.lmh" Date: Tue, 9 Jun 2026 13:56:30 +0800 Subject: [PATCH 2/2] fix(web): guard NewFileDialog Enter against IME composition Add `!e.nativeEvent.isComposing` check to the onKeyDown handler in NewFileDialog, consistent with the existing guard in CreateItemDialog. This prevents CJK IME users from accidentally triggering form submission when confirming a composition sequence with Enter. Co-Authored-By: Claude Sonnet 4.6 --- web/src/pages/ContextPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/pages/ContextPage.tsx b/web/src/pages/ContextPage.tsx index 28271e60..15a633ab 100644 --- a/web/src/pages/ContextPage.tsx +++ b/web/src/pages/ContextPage.tsx @@ -1184,7 +1184,7 @@ function NewFileDialog({ value={path} onChange={(e) => setPath(e.target.value)} onKeyDown={(e) => { - if (e.key === "Enter" && path.trim()) onCreate(path.trim()) + if (e.key === "Enter" && !e.nativeEvent.isComposing && path.trim()) onCreate(path.trim()) if (e.key === "Escape") onClose() }} placeholder="loopat/new-doc.md"