* chore(ai): upgrade ACP packages and unwrap Skill+CLI command in tool-call panel
Package bumps:
- @zed-industries/claude-agent-acp 0.22.2 → @agentclientprotocol/claude-agent-acp 0.37.0
(old npm package is deprecated; scope rename)
- @zed-industries/codex-acp 0.10.0 → 0.15.0
- @mcpc-tech/acp-ai-provider 0.2.8 → 0.3.3
- electron-builder asarUnpack glob + bridge require.resolve switched to the new scope
After the upgrade Codex tool-call cards started showing the local
worktree path for every step — "Run /Users/.../netcatty-tool-cli session
--session …" — instead of the remote command. Three things lined up:
1. The new acp-ai-provider maps ACP's `title` to `toolName`, and Codex's
title is the full shell invocation it's about to run.
2. Codex local_shell ships args as ["/bin/zsh","-lc","<full>"], so the
old `typeof args.command === 'string'` branch in ToolCall never fired
and we fell through to printing `name` (i.e. the title).
3. The bridge serializes tool args under `args`, but the ACP adapter
only read `event.input`, so even when args were available the
renderer received {}.
Fixes:
- acpAgentAdapter: read tool input from both `event.input` and
`event.args` so bridge-serialized chunks and direct AI SDK chunks
both work.
- ai-elements/tool-call: new extractDisplayCommand() unwraps the shell
array, then the netcatty-tool-cli wrapper (exec/job-start … -- <cmd>),
and renders the real remote command. session/env/job-poll/etc. fall
back to short labels ("netcatty: inspect session", …) instead of
exposing the binary path.
- shellUtils.cjs: defensive JSON-parse the ACP wrapper input in case
the AI SDK ever stops auto-parsing it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(build): exclude bundled Claude CLI binaries from the installer
@anthropic-ai/claude-agent-sdk@0.3.x bundles the native Claude Code CLI
(~211MB per arch) as optional sibling packages. Including them would
silently regress Netcatty's "bring your own Claude" design — the project
has always required users to install Claude Code locally, and the entire
path-discovery flow exists precisely to honor that contract:
- useAgentDiscovery.ts scans the user's PATH for `claude` and writes
the absolute path into the agent config's CLAUDE_CODE_EXECUTABLE env.
- aiBridge.cjs runs normalizeClaudeCodeExecutableEnvForAcp on every ACP
spawn, forwarding the env var to the child process.
- The @agentclientprotocol/claude-agent-acp wrapper's claudeCliPath()
(acp-agent.js) prefers process.env.CLAUDE_CODE_EXECUTABLE over the
bundled binary and only falls back to sibling-package resolution when
the env var is empty.
So the right place to enforce the design is electron-builder: exclude
node_modules/@anthropic-ai/claude-agent-sdk-* from `files`. Dev mode is
unaffected (optional deps still install for `npm run dev`); only the
packaged installer drops the binaries, saving ~150MB. Users without
Claude Code installed get the same SDK error they got pre-upgrade.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix#954: unify Tooltip styling + replace native selects
Replace native HTML title= tooltips and native <select> dropdowns
with the existing Radix-based Tooltip / Select components so they
share the app's rounded styling, theme tokens and i18n pipeline.
Adds a global TooltipProvider in AppWithProviders so every
descendant Tooltip works without a per-file Provider wrapper.
Scope (driven by the issue #954 examples and "全部都处理" follow-up):
- TerminalLayer toolbar: Add Terminal / Split View / SFTP / Scripts
/ Theme / AI Chat / Move panel / Close panel.
- TopTabs middle bar: quick switcher, more tabs, AI assistant, theme
toggle, settings; window-control buttons (min/max/close), tray
close and hotkey reset/disable have their native title dropped per
the user's explicit opt-out ("可以不用Tooltip,直接全局禁用
原生title 属性").
- AI panels: AIChatSidePanel session history / new chat / delete,
ConversationExport, AgentSelector, ChatInput attach / expand /
permission, ModelSelector, ProviderCard, ai-elements/tool-call.
- SFTP: SftpSidePanel header, SftpBreadcrumb, SftpFileRow,
SftpPaneToolbar, SftpTabBar, SftpTransferQueue.
- Settings: SettingsPage close, SettingsAppearanceTab theme/accent
swatches, SettingsFileAssociationsTab edit/remove, SettingsSystemTab
crash-log paths and global hotkey reset.
- Host vault: HostDetailsPanel (clear / suggestions / show-password /
key path / browse key), GroupDetailsPanel, KnownHostsManager,
ConnectionLogsManager, KeychainManager, SyncStatusButton,
CloudSyncSettings, LogView, QuickSwitcher, ScriptsSidePanel,
Terminal status bar copy-host + broadcast/focus, ZmodemProgressIndicator.
- Terminal subcomponents: HostKeywordHighlightPopover, TerminalComposeBar,
TerminalConnectionDialog, TerminalSearchBar.
- Editor: TextEditorPane (subtitle, search, wrap, promote-to-tab).
- TrayPanel session rows and port-forwarding rows.
Native <select> migrated to custom Select component:
- SerialConnectModal (data bits, stop bits, parity, flow control)
- SerialHostDetailsPanel (same four fields)
- HostDetailsPanel backspace behavior
- GroupDetailsPanel backspace behavior
- SettingsTerminalTab local shell picker
- terminal/ThemeSidePanel font weight
Hardcoded English strings extracted to i18n. New keys for both
en and zh-CN: terminal.layer.*, topTabs.*, ai.chat.* (sessionHistory,
attach, collapse, expand, enableAgent), zmodem.*, settings.shortcuts.
resetToDefault. Inline help text on SnippetsManager package-name input
removed because the same hint is already shown in a visible <p> below
the input.
Existing per-file <TooltipProvider> wrappers (SnippetsManager,
ScriptsSidePanel, SelectHostPanel, RuleCard, HostDetailsPanel proxy
section) are left in place — they nest harmlessly under the global
provider and stay self-sufficient for component tests.
Tests:
- tsc clean for changed files (pre-existing repo-wide errors
unrelated to this PR).
- All 802 tests pass (3 skipped pre-existing).
- HostDetailsPanel.proxyProfile.test and TextEditorPane.test
updated to wrap with TooltipProvider, matching the runtime
context now needed by the migrated components.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix#954: wrap Settings + Tray windows with TooltipProvider
Settings and the tray panel mount as separate Electron windows with
their own React root in index.tsx, so they do not inherit the global
TooltipProvider added under AppWithProviders. After the unified
Tooltip migration, any settings tab that used a Tooltip (Appearance,
Application, FileAssociations, System, Shortcuts, Terminal, AI
ProviderCard, AI ModelSelector) — and TrayPanel — threw
"Tooltip must be used within TooltipProvider" and rendered nothing.
Wrap both branches with TooltipProvider at the same level as
ToastProvider in index.tsx.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#838.
Adds stable `data-role="user|assistant|system|tool"` attributes plus
`ai-chat-message` / `ai-chat-message-content` classnames on the chat
message rows in Catty Agent's chat panel. Users can now distinguish
their own messages from agent replies via Settings → Appearance →
Custom CSS, e.g.
.ai-chat-message[data-role="user"] .ai-chat-message-content {
background: rgba(91, 124, 250, 0.12);
}
The default theme is intentionally minimal (bordered user bubble,
plain assistant text). Rather than change the default — different
users want different distinctions — this exposes a hook so anyone
can colour the rows however they prefer without forking.
The attribute names are part of the UI's stable contract; a comment
on the Message component flags this for future renames.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: preserve AI chat history across reconnects
* fix: retarget restored AI sessions on reconnect
* feat: format tool call results with proper line breaks
Extract stdout/stderr from structured results and unescape \n/\t
so command output displays with real line breaks like terminal output.
Supports both JSON object {stdout,stderr} and executor text formats.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: restrict unescape to stdout/stderr fields only
Plain strings may contain legitimate backslash sequences (file paths,
regex patterns) that should not be converted. Only apply unescape to
stdout/stderr fields extracted from command execution results.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address review findings for AI chat reconnect
1. Add explicit activeTerminalTargetIds guard in shouldRetargetActiveSession
to prevent retargeting sessions owned by other terminals, making the
invariant locally verifiable.
2. Only preserve orphaned terminal sessions with hostIds — workspace,
local, and serial sessions generate fresh IDs and would be permanently
unreachable, wasting MAX_STORED_SESSIONS quota.
3. Clear stale streaming state when restoring a session whose ACP handle
was already cleaned up (e.g., reconnect during mid-response), so the
user can send new messages.
4. Restore overflow-hidden on user message bubbles to prevent content
bleeding past rounded border corners.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address round 2 review findings
1. Fix streaming state clear: only clear for sessions whose targetId
doesn't match current scope (restored from different terminal),
not for built-in Catty chats that never set externalSessionId.
2. Exclude local/serial sessions from preservation: their synthetic
hostIds (local-*/serial-*) change on every open and can never be
matched back.
3. Preserve non-zero exitCode in formatted tool results so failed
commands show a visible failure signal.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: only clear streaming state during retarget, not for all restored sessions
The previous condition (targetId !== scopeTargetId) also fired for
built-in Catty sessions during normal operation, killing active streams.
Now streaming is only cleared when shouldRetargetActiveSession is true,
meaning the session came from a disconnected terminal where any
in-flight response is guaranteed to be dead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address round 3 review findings
1. Clear externalSessionId during retarget to prevent stale ACP handle
from surviving if retarget runs before orphan cleanup.
2. Only retarget in visible AI panels — hidden/background panels should
not race to claim orphaned sessions.
3. Remove unescapeTerminalOutput — data flow trace confirms real newline
characters arrive at the component. The unescape was corrupting
legitimate backslash sequences in paths and patterns.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: only ACP-cleanup deleted sessions, not preserved ones
Preserved sessions may be reused on reconnect. Running aiAcpCleanup
on them asynchronously could race with a newly started ACP conversation
on the same session ID, tearing down the fresh provider.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: abort in-flight streams during retarget and restore ACP cleanup
1. Abort the active request's AbortController when retargeting a session
with stale streaming state. Prevents late chunks from the old run
appending into the restored chat.
2. Restore ACP cleanup for all orphaned sessions (not just deleted ones).
Preserved sessions get a new externalSessionId on next use, so
cleaning the old one prevents subprocess leaks without affecting
future conversations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: guard hidden panels from session ownership and skip null map entries
1. Only assign restored sessions in visible panels — hidden panels
should not race to own sessions via setActiveSessionId, preventing
MCP/tool calls from being bound to the wrong terminal.
2. Skip null entries in activeSessionIdMap when building
activeTerminalTargetIds — deleted chats should not block same-host
history matching on other terminals.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: guard MCP sync behind visibility and cancel exec/approvals on retarget
1. Only sync MCP session metadata from visible panels to prevent
hidden panels from overwriting the scope mapping.
2. Cancel pending approvals and in-flight exec (Catty + ACP) during
retarget, matching handleStop behavior. Prevents stale tool results
and approval prompts from reappearing after session retarget.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: restore MCP sync for hidden panels
MCP scope is keyed by chatSessionId so hidden panels don't overwrite
visible panels' mappings. The isVisible guard was breaking background
chats that need updated terminal session metadata after reconnects
or workspace changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: remove unused deletedIds variable
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: inline approval gate for tool execution
Replace SDK-level needsApproval with Promise-based approval gate inside
tool execute functions. The SDK stream stays alive while the UI shows
inline approve/reject buttons on ToolCall blocks.
Changes:
- Add approvalGate.ts: Promise-based approval system with event listeners
- tools.ts: requestApproval() inside execute for confirm mode
- tool-call.tsx: inline approval buttons and keyboard shortcuts
- ChatMessageList.tsx: subscribe to approval events, render approval UI
- useAIChatStreaming.ts: remove old useToolApproval hook integration
- AIChatSidePanel.tsx: remove old approval hook, clean up unused destructuring
- systemPrompt.ts: update confirm mode to not ask for text confirmation
- preload.cjs: filter pager env var prefixes from terminal display
- mcpServerBridge.cjs: add approval gate for ACP/MCP write operations
- aiBridge.cjs: wire IPC for MCP approval response and main window getter
- preload.cjs: add onMcpApprovalRequest/respondMcpApproval APIs
* fix: scope approval gate by chatSessionId and replay for late subscribers
Address Codex PR review comments:
- Add chatSessionId to ApprovalRequest for session isolation
- Scope clearAllPendingApprovals(chatSessionId?) to only clear
approvals belonging to the target session
- Add replayPendingApprovals() so late-mounting ChatMessageList
picks up approvals that fired while unmounted
- Scope MCP clearPendingApprovals in aiBridge cancel handler to
effectiveChatSessionId instead of clearing all
- Pass chatSessionId through MCP approval IPC flow
* chore: remove old approval flow code
- Delete useToolApproval.ts (unused hook)
- Delete InlineApprovalCard.tsx (replaced by ToolCall inline buttons)
- Remove stale comments referencing old hook in AIChatSidePanel
- Remove unused ai.chat.toolApprovalTitle i18n key from en/zh-CN
* fix: session-scoped approval gate and MCP replay survival
- handleStop passes activeSessionId to clearAllPendingApprovals
- setupMcpApprovalBridge stores MCP approvals in pendingApprovals map
so they survive ChatMessageList unmount/remount cycles
- ChatMessageList accepts activeSessionId prop and filters standalone
MCP approval blocks to the current session only
- AIChatSidePanel passes activeSessionId to ChatMessageList
* fix: filter PTY exec marker echoes and exit code lines from terminal
Extend filterMcpMarkers in preload.cjs to strip all shell-visible
artifacts from AI command execution:
- Echoed printf start marker: printf '%s\n' '__NCMCP_..._S'
- Echoed exit code restoration: (exit $__nc)
- PowerShell: Write-Output, $global:LASTEXITCODE, $__nc assignment
- Fish: set __nc $status
- Cmd: echo __NCMCP_...
- Widen guard to also trigger on __nc and PAGER=cat strings
* fix: scope SDK approvals, deny MCP on no renderer, fix memo comparator
- createCattyTools accepts chatSessionId and passes it to
requestApproval so SDK approvals can be matched by
clearAllPendingApprovals(activeSessionId) on stop
- useAIChatStreaming passes sessionId to createCattyTools
- mcpServerBridge: deny (resolve false) when no renderer window is
available instead of auto-approving, preserving confirm mode safety
- ChatMessageList: add activeSessionId to React.memo comparator so
switching sessions triggers re-render for correct MCP approval filter
* fix: MCP listener lifecycle, approval timeout, and UI sync on stop
- Move setupMcpApprovalBridge from ChatMessageList to AIChatSidePanel
so the IPC listener survives tab/panel switches
- Add 5-minute auto-deny timeout to requestApproval to prevent
indefinite isStreaming hangs when user walks away
- Add onApprovalCleared listener system: clearAllPendingApprovals now
notifies UI subscribers so ChatMessageList removes stale cards
- ChatMessageList subscribes to onApprovalCleared to sync local state
* fix: main-process approval timeout and full tool args in payload
- Add 5-minute auto-deny timeout to requestApprovalFromRenderer
matching the renderer-side requestApproval behavior
- Forward all tool params (excluding chatSessionId) to approval UI
instead of cherry-picking command/input/path, so sftpRename
oldPath/newPath and other tool-specific args are visible
* fix: move MCP bridge to TerminalLayer, narrow terminal filter guard
- Move setupMcpApprovalBridge from AIChatSidePanel to TerminalLayer
so the IPC listener stays alive regardless of side panel tab.
AIChatSidePanel only mounts when activeSidePanelTab==='ai'.
- Narrow preload.cjs filter guard back to __NCMCP_ only, preventing
false-positive stripping of user scripts containing __nc or PAGER=cat
* fix: eliminate PTY wrapper echo leakage and duplicate prompts
- Posix wrapper now emits 2 lines instead of 4: start marker + command
on line 1 (joined with ;), end marker + exit on line 2. This
eliminates the duplicate prompt echo from the separate start marker.
- Rename __nc to __NCMCP_rc in all shell variants (posix/fish/powershell)
so every wrapper variable contains the __NCMCP_ prefix. The preload
guard `data.includes("__NCMCP_")` now reliably catches ALL wrapper
artifacts regardless of chunk boundaries.
- Update all filterMcpMarkers regex patterns to match the restructured
wrapper format and renamed variable.
* fix: sync main-process approval timeout with renderer UI cleanup
- When requestApprovalFromRenderer times out, send IPC event
netcatty:ai:mcp:approval-cleared to renderer so stale approval
cards are removed
- Add onMcpApprovalCleared preload bridge for the new IPC channel
- setupMcpApprovalBridge now subscribes to cleared events, removes
timed-out entries from pendingApprovals and notifies clearedListeners
so ChatMessageList drops the stale card
* fix: surface denied inline approvals as errors in UI
- Detect error or denial payloads ("error" string or "ok: false")
returned by tools when the user denies an execution
- Set isError: true on the tool-result message so the ToolCall UI
renders it as a failure (red/rejected) instead of a success (green)
StickToBottom was configured with initial="smooth", causing a visible
elastic scroll animation every time the chat panel remounted on tab
switch. Change to initial="instant" so the scroll position snaps
immediately without animation. Streaming and resize still use smooth.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: support multimodal attachments (images, PDFs, files) in AI chat
Previously uploaded images were displayed in the UI but never sent to
the AI model, and non-image files (PDF, text) were silently rejected.
- Rename useImageUpload → useFileUpload; accept image/*, PDF, and text/*
- Rename ChatMessageImage → ChatMessageAttachment with filePath support
- Build multimodal SDK messages (ImagePart/FilePart) for Catty Agent
- Fix ACP agent path: images inline, non-image files via local path hint
so ACP agents (Claude Code, etc.) read them with native file access
- Use Electron webUtils.getPathForFile() for reliable file path capture
- Compact user message bubble padding
Closes#294 (AI file upload issues)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: show real tool names in AI chat instead of ACP wrapper names
- Unwrap ACP dynamic tool calls in serializeStreamChunk to extract
real tool name, args, and toolCallId from chunk.input
- Simplify MCP tool name prefixes (mcp__server__tool → tool)
- Pass toolCallId from ACP tool-call events to match tool results
- Prevent onToolResult from overwriting correct names with wrapper name
- Build toolCallNames map in ChatMessageList for tool result display
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: backward-compatible fallback for legacy `images` field in chat messages
Persisted sessions may still have `images` instead of `attachments`.
Add `?? m.images` fallback in SDK message builder and renderer so
historical image attachments are not silently dropped after upgrade.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: broaden file type support and handle pasted files without path
- Accept all file types except video/audio (instead of allowlist)
so .json, .yaml, .sh, etc. are not silently rejected
- For ACP agents, save pasted/virtual files (no filePath) to temp
directory so the agent can still read them
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use managed temp dir for pasted ACP attachments
Use tempDirBridge.getTempFilePath() instead of manual os.tmpdir() path
so pasted file attachments are tracked by the app's cleanup system.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Safety enforcement:
- Command Timeout: use user setting in MCP Server (execViaPty/execViaChannel)
and Catty Agent path (aiBridge execViaPtyForCatty/execViaChannel fallback)
- Max Iterations: read user setting for Catty (stepCountIs) and ACP (via IPC)
- Permission Mode: observer mode hard-blocks write ops in MCP Server dispatch;
Catty SDK tools wired to checkToolPermission(); all synced via IPC on change
Command Blocklist UI:
- Add editable regex pattern list in Settings AI Safety section
- Reset to defaults button, add/remove patterns
- IPC sync to MCP Server on change and on mount
Provider UX:
- ModelSelector rewritten as combobox: type-to-filter with suggestions dropdown
- All providers use unified ModelSelector (modelsEndpoint optional)
- API key passed as auth header for model fetching (Bearer / x-api-key)
- Skip model fetch when no API key (except Ollama)
- Provider toggle is now mutually exclusive (activating one disables others)
- New providers default to disabled (switch off)
- Custom provider supports editable display name
- No-provider error: friendly message shown in chat
i18n:
- Full i18n coverage for Settings AI tab (~55 keys)
- Full i18n for AI chat panel (placeholder, empty state, time, sessions)
- en.ts and zh-CN.ts locale files updated
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Integrate Claude Agent SDK for direct streaming chat, add Codex login/logout
flow with OAuth support in settings, improve AI chat panel UI and agent
discovery, and update build config for new dependencies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add auto-discovery of CLI agents (Claude Code, Codex, Gemini) from system PATH
- Integrate ACP (Agent Client Protocol) for real-time streaming with codex-acp
- Bundle @zed-industries/codex-acp binary for reliable agent spawning
- Add ThinkingBlock component with shimmer animation and auto-collapse
- Refactor chat UI: no avatars, bordered user bubbles, plain assistant text
- Support {prompt} placeholder in agent args for flexible invocation
- Add persistent ACP sessions with proper cleanup on app exit
- Detect auth errors and show actionable messages to users
- Fallback to raw process spawn for agents without ACP support
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>