feat: enable full shell access for AI agent + simplify MCP tools
Remove --tools "" restriction so the agent has native bash/read/write/edit access. Set vault path as working directory for the subprocess. Simplify MCP to 4 Laputa-specific tools (search_notes, get_vault_context, get_note, open_note) — everything else is handled by native tools. Add file operation detection from Write/Edit tool calls to auto-open created notes and refresh modified notes in the UI. Enhanced tool call labels show bash commands, file paths, and note names. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -66,8 +66,8 @@ describe('AiActionCard', () => {
|
||||
expect(header.getAttribute('tabindex')).toBe('0')
|
||||
})
|
||||
|
||||
it('uses lighter background for ui_ tools', () => {
|
||||
render(<AiActionCard {...defaults} tool="ui_open_tab" label="Opened tab" />)
|
||||
it('uses lighter background for open_note tool', () => {
|
||||
render(<AiActionCard {...defaults} tool="open_note" label="Opening note" />)
|
||||
const card = screen.getByTestId('ai-action-card')
|
||||
expect(card.style.background).toContain('0.06')
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { type KeyboardEvent, type ReactNode, useCallback } from 'react'
|
||||
import {
|
||||
PencilSimple, MagnifyingGlass, Link, Trash, ChartBar, Eye, Sparkle,
|
||||
PencilSimple, MagnifyingGlass, Trash, ChartBar, Eye,
|
||||
CircleNotch, CheckCircle, XCircle, CaretRight, CaretDown,
|
||||
Terminal, File, FolderOpen, NotePencil,
|
||||
} from '@phosphor-icons/react'
|
||||
|
||||
export type AiActionStatus = 'pending' | 'done' | 'error'
|
||||
@@ -23,18 +24,21 @@ const MAX_DETAIL_LENGTH = 800
|
||||
type IconRenderer = (size: number) => ReactNode
|
||||
|
||||
const TOOL_ICON_MAP: Record<string, IconRenderer> = {
|
||||
create_note: (s) => <PencilSimple size={s} />,
|
||||
edit_note_frontmatter: (s) => <PencilSimple size={s} />,
|
||||
append_to_note: (s) => <PencilSimple size={s} />,
|
||||
// Native Claude Code tools
|
||||
Bash: (s) => <Terminal size={s} />,
|
||||
Write: (s) => <PencilSimple size={s} />,
|
||||
Edit: (s) => <NotePencil size={s} />,
|
||||
Read: (s) => <File size={s} />,
|
||||
Glob: (s) => <FolderOpen size={s} />,
|
||||
Grep: (s) => <MagnifyingGlass size={s} />,
|
||||
// Laputa MCP tools
|
||||
search_notes: (s) => <MagnifyingGlass size={s} />,
|
||||
list_notes: (s) => <MagnifyingGlass size={s} />,
|
||||
link_notes: (s) => <Link size={s} />,
|
||||
get_vault_context: (s) => <ChartBar size={s} />,
|
||||
get_note: (s) => <File size={s} />,
|
||||
open_note: (s) => <Eye size={s} />,
|
||||
// Legacy tools (for backward compatibility with existing messages)
|
||||
create_note: (s) => <PencilSimple size={s} />,
|
||||
delete_note: (s) => <Trash size={s} />,
|
||||
vault_context: (s) => <ChartBar size={s} />,
|
||||
ui_open_note: (s) => <Eye size={s} />,
|
||||
ui_open_tab: (s) => <Eye size={s} />,
|
||||
ui_highlight: (s) => <Sparkle size={s} />,
|
||||
ui_set_filter: (s) => <Sparkle size={s} />,
|
||||
}
|
||||
|
||||
const DEFAULT_ICON: IconRenderer = (s) => <PencilSimple size={s} />
|
||||
@@ -96,11 +100,15 @@ function DetailBlock({ label, content, isError }: {
|
||||
)
|
||||
}
|
||||
|
||||
/** Whether this tool is a Laputa UI-only tool (lighter styling). */
|
||||
function isUiOnlyTool(tool: string): boolean {
|
||||
return tool === 'open_note'
|
||||
}
|
||||
|
||||
export function AiActionCard({
|
||||
tool, label, path, status, input, output, expanded, onToggle, onOpenNote,
|
||||
}: AiActionCardProps) {
|
||||
const renderIcon = TOOL_ICON_MAP[tool] ?? DEFAULT_ICON
|
||||
const isUiTool = tool.startsWith('ui_')
|
||||
const hasDetails = !!(input || output)
|
||||
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
@@ -129,7 +137,7 @@ export function AiActionCard({
|
||||
className="rounded"
|
||||
style={{
|
||||
fontSize: 12,
|
||||
background: isUiTool ? 'rgba(74, 158, 255, 0.06)' : 'rgba(74, 158, 255, 0.1)',
|
||||
background: isUiOnlyTool(tool) ? 'rgba(74, 158, 255, 0.06)' : 'rgba(74, 158, 255, 0.1)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, useRef, useEffect, useCallback, useMemo } from 'react'
|
||||
import { Robot, X, PaperPlaneRight, Plus, Link } from '@phosphor-icons/react'
|
||||
import { AiMessage } from './AiMessage'
|
||||
import { WikilinkChatInput } from './WikilinkChatInput'
|
||||
import { useAiAgent, type AiAgentMessage } from '../hooks/useAiAgent'
|
||||
import { useAiAgent, type AiAgentMessage, type AgentFileCallbacks } from '../hooks/useAiAgent'
|
||||
import { collectLinkedEntries, buildContextSnapshot, type NoteReference, type NoteListItem } from '../utils/ai-context'
|
||||
import type { VaultEntry } from '../types'
|
||||
|
||||
@@ -11,6 +11,8 @@ export type { AiAgentMessage } from '../hooks/useAiAgent'
|
||||
interface AiPanelProps {
|
||||
onClose: () => void
|
||||
onOpenNote?: (path: string) => void
|
||||
onFileCreated?: (relativePath: string) => void
|
||||
onFileModified?: (relativePath: string) => void
|
||||
vaultPath: string
|
||||
activeEntry?: VaultEntry | null
|
||||
entries?: VaultEntry[]
|
||||
@@ -107,7 +109,7 @@ function MessageHistory({ messages, isActive, onOpenNote, hasContext }: {
|
||||
)
|
||||
}
|
||||
|
||||
export function AiPanel({ onClose, onOpenNote, vaultPath, activeEntry, entries, allContent, openTabs, noteList, noteListFilter }: AiPanelProps) {
|
||||
export function AiPanel({ onClose, onOpenNote, onFileCreated, onFileModified, vaultPath, activeEntry, entries, allContent, openTabs, noteList, noteListFilter }: AiPanelProps) {
|
||||
const [input, setInput] = useState('')
|
||||
const [pendingRefs, setPendingRefs] = useState<NoteReference[]>([])
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
@@ -131,7 +133,12 @@ export function AiPanel({ onClose, onOpenNote, vaultPath, activeEntry, entries,
|
||||
})
|
||||
}, [activeEntry, allContent, openTabs, noteList, noteListFilter, entries, pendingRefs])
|
||||
|
||||
const agent = useAiAgent(vaultPath, contextPrompt)
|
||||
const fileCallbacks = useMemo<AgentFileCallbacks>(() => ({
|
||||
onFileCreated,
|
||||
onFileModified,
|
||||
}), [onFileCreated, onFileModified])
|
||||
|
||||
const agent = useAiAgent(vaultPath, contextPrompt, fileCallbacks)
|
||||
const hasContext = !!activeEntry
|
||||
const isActive = agent.status === 'thinking' || agent.status === 'tool-executing'
|
||||
|
||||
|
||||
@@ -71,6 +71,8 @@ interface EditorProps {
|
||||
rawToggleRef?: React.MutableRefObject<() => void>
|
||||
/** Mutable ref that Editor registers its diff-mode toggle into, for command palette access. */
|
||||
diffToggleRef?: React.MutableRefObject<() => void>
|
||||
onFileCreated?: (relativePath: string) => void
|
||||
onFileModified?: (relativePath: string) => void
|
||||
}
|
||||
|
||||
function useEditorModeExclusion({
|
||||
@@ -127,6 +129,8 @@ export const Editor = memo(function Editor({
|
||||
isDarkTheme,
|
||||
rawToggleRef,
|
||||
diffToggleRef,
|
||||
onFileCreated,
|
||||
onFileModified,
|
||||
}: EditorProps) {
|
||||
const vaultPathRef = useRef(vaultPath)
|
||||
useEffect(() => { vaultPathRef.current = vaultPath }, [vaultPath])
|
||||
@@ -242,6 +246,8 @@ export const Editor = memo(function Editor({
|
||||
onDeleteProperty={onDeleteProperty}
|
||||
onAddProperty={onAddProperty}
|
||||
onOpenNote={onNavigateWikilink}
|
||||
onFileCreated={onFileCreated}
|
||||
onFileModified={onFileModified}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,8 @@ interface EditorRightPanelProps {
|
||||
onDeleteProperty?: (path: string, key: string) => Promise<void>
|
||||
onAddProperty?: (path: string, key: string, value: FrontmatterValue) => Promise<void>
|
||||
onOpenNote?: (path: string) => void
|
||||
onFileCreated?: (relativePath: string) => void
|
||||
onFileModified?: (relativePath: string) => void
|
||||
}
|
||||
|
||||
export function EditorRightPanel({
|
||||
@@ -32,6 +34,7 @@ export function EditorRightPanel({
|
||||
noteList, noteListFilter,
|
||||
onToggleInspector, onToggleAIChat, onNavigateWikilink, onViewCommitDiff,
|
||||
onUpdateFrontmatter, onDeleteProperty, onAddProperty, onOpenNote,
|
||||
onFileCreated, onFileModified,
|
||||
}: EditorRightPanelProps) {
|
||||
if (showAIChat) {
|
||||
return (
|
||||
@@ -42,6 +45,8 @@ export function EditorRightPanel({
|
||||
<AiPanel
|
||||
onClose={() => onToggleAIChat?.()}
|
||||
onOpenNote={onOpenNote}
|
||||
onFileCreated={onFileCreated}
|
||||
onFileModified={onFileModified}
|
||||
vaultPath={vaultPath}
|
||||
activeEntry={inspectorEntry}
|
||||
entries={entries}
|
||||
|
||||
Reference in New Issue
Block a user