fix: AI-created notes now trigger vault refresh and auto-open in tab

Root causes:
- toolInputMapRef in useAiAgent was overwritten by tool_progress events
  (which arrive with input=undefined AFTER the assistant message set
  the full input), causing detectFileOperation to receive undefined
  and skip file creation detection entirely.
- MCP open_note only broadcast open_tab without vault_changed, so
  the note list didn't refresh when Claude Code called open_note.
- detectFileOperation only handled Write/Edit but not Bash commands
  that create .md files via redirects.

Fixes:
- Preserve accumulated input in toolInputMapRef (input ?? prev?.input)
- MCP open_note now broadcasts vault_changed before open_tab
- detectFileOperation now detects Bash redirect patterns (>, >>, tee)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lucaronin
2026-03-07 03:39:34 +01:00
parent fe9ebc9063
commit 415bbb5413
4 changed files with 35 additions and 4 deletions

View File

@@ -1 +1 @@
66070
88289

View File

@@ -148,6 +148,9 @@ async function handleGetNote(args) {
}
function handleOpenNote(args) {
// Refresh vault first so the new/modified note appears in the note list,
// then signal the UI to open it in a tab.
broadcastUiAction('vault_changed', { path: args.path })
broadcastUiAction('open_tab', { path: args.path })
return { content: [{ type: 'text', text: `Opening ${args.path} in Laputa` }] }
}

View File

@@ -46,8 +46,8 @@ const TOOL_HANDLERS = {
read_note: (args) => getNote(VAULT_PATH, args.path).then(note => ({ content: note.content, frontmatter: note.frontmatter })),
search_notes: (args) => searchNotes(VAULT_PATH, args.query, args.limit),
vault_context: () => vaultContext(VAULT_PATH),
ui_open_note: (args) => { broadcastUiAction('open_note', { path: args.path }); return { ok: true } },
ui_open_tab: (args) => { broadcastUiAction('open_tab', { path: args.path }); return { ok: true } },
ui_open_note: (args) => { broadcastUiAction('vault_changed', { path: args.path }); broadcastUiAction('open_note', { path: args.path }); return { ok: true } },
ui_open_tab: (args) => { broadcastUiAction('vault_changed', { path: args.path }); broadcastUiAction('open_tab', { path: args.path }); return { ok: true } },
ui_highlight: (args) => { broadcastUiAction('highlight', { element: args.element, path: args.path }); return { ok: true } },
ui_set_filter: (args) => { broadcastUiAction('set_filter', { filterType: args.type }); return { ok: true } },
highlight_editor: (args) => { broadcastUiAction('highlight', { element: args.element, path: args.path }); return { ok: true } },

View File

@@ -105,7 +105,10 @@ export function useAiAgent(
if (abortRef.current.aborted) return
markReasoningDone()
setStatus('tool-executing')
toolInputMapRef.current.set(toolId, { tool: toolName, input })
// Preserve accumulated input — tool_progress events arrive with
// input=undefined AFTER the assistant message set the full input.
const prev = toolInputMapRef.current.get(toolId)
toolInputMapRef.current.set(toolId, { tool: toolName, input: input ?? prev?.input })
update(m => {
const existing = m.actions.find(a => a.toolId === toolId)
if (existing) {
@@ -212,6 +215,14 @@ function detectFileOperation(
callbacks: AgentFileCallbacks | undefined,
) {
if (!callbacks) return
// Handle Bash commands that create/write .md files
if (toolName === 'Bash') {
const mdPath = parseBashFileCreation(input, vaultPath)
if (mdPath) callbacks.onFileCreated?.(mdPath)
return
}
if (toolName !== 'Write' && toolName !== 'Edit') return
const filePath = parseFilePath(input)
if (!filePath || !filePath.endsWith('.md')) return
@@ -224,6 +235,23 @@ function detectFileOperation(
}
}
/** Detect .md file creation from a Bash command string. */
function parseBashFileCreation(input: string | undefined, vaultPath: string): string | null {
if (!input) return null
try {
const parsed = JSON.parse(input)
const cmd = parsed.command ?? parsed.cmd
if (typeof cmd !== 'string') return null
// Match redirect patterns: > file.md, >> file.md, tee file.md, cat > file.md
const match = cmd.match(/(?:>|>>|tee\s+(?:-a\s+)?)\s*["']?([^\s"'|;]+\.md)["']?/)
if (!match) return null
const filePath = match[1]
return toVaultRelative(filePath, vaultPath)
} catch {
return null
}
}
/** Generate a human-readable label for a tool call. */
function formatToolLabel(toolName: string, input?: string): string {
// Native Claude Code tools