Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4bbe62a1d | ||
|
|
57e131a16e | ||
|
|
ea6f9e138c | ||
|
|
5177ce2028 | ||
|
|
9f44112479 | ||
|
|
6999f362a3 | ||
|
|
fc546c2430 | ||
|
|
f7e4953038 | ||
|
|
922376fa06 | ||
|
|
3d4ca46c9b | ||
|
|
1d8f203f5b | ||
|
|
41d079a806 | ||
|
|
93c95959d3 | ||
|
|
e7300429f8 | ||
|
|
c7743d082a | ||
|
|
56a4fe905d | ||
|
|
b17775307f |
@@ -5,6 +5,9 @@ const en: Messages = {
|
||||
'common.save': 'Save',
|
||||
'common.cancel': 'Cancel',
|
||||
'common.close': 'Close',
|
||||
'common.reset': 'Reset',
|
||||
'common.zoomIn': 'Zoom in',
|
||||
'common.zoomOut': 'Zoom out',
|
||||
'common.settings': 'Settings',
|
||||
'common.search': 'Search',
|
||||
'common.searchPlaceholder': 'Search...',
|
||||
@@ -1555,7 +1558,7 @@ const en: Messages = {
|
||||
|
||||
// AI Claude Code
|
||||
'ai.claude.title': 'Claude Code',
|
||||
'ai.claude.description': "Anthropic's agentic coding assistant. Uses claude-code-acp for ACP protocol streaming.",
|
||||
'ai.claude.description': "Anthropic's agentic coding assistant. Uses claude-agent-acp for ACP protocol streaming.",
|
||||
'ai.claude.detecting': 'Detecting...',
|
||||
'ai.claude.detected': 'Detected',
|
||||
'ai.claude.notFound': 'Not found',
|
||||
|
||||
@@ -5,6 +5,9 @@ const zhCN: Messages = {
|
||||
'common.save': '保存',
|
||||
'common.cancel': '取消',
|
||||
'common.close': '关闭',
|
||||
'common.reset': '重置',
|
||||
'common.zoomIn': '放大',
|
||||
'common.zoomOut': '缩小',
|
||||
'common.settings': '设置',
|
||||
'common.search': '搜索',
|
||||
'common.connect': '连接',
|
||||
@@ -1570,7 +1573,7 @@ const zhCN: Messages = {
|
||||
|
||||
// AI Claude Code
|
||||
'ai.claude.title': 'Claude Code',
|
||||
'ai.claude.description': 'Anthropic 的智能编程助手。使用 claude-code-acp 进行 ACP 协议流式传输。',
|
||||
'ai.claude.description': 'Anthropic 的智能编程助手。使用 claude-agent-acp 进行 ACP 协议流式传输。',
|
||||
'ai.claude.detecting': '检测中...',
|
||||
'ai.claude.detected': '已检测到',
|
||||
'ai.claude.notFound': '未找到',
|
||||
|
||||
@@ -9,7 +9,7 @@ export type ConversationProps = ComponentProps<typeof StickToBottom>;
|
||||
export const Conversation = ({ className, ...props }: ConversationProps) => (
|
||||
<StickToBottom
|
||||
className={cn('relative flex-1 overflow-y-hidden', className)}
|
||||
initial="smooth"
|
||||
initial="instant"
|
||||
resize="smooth"
|
||||
role="log"
|
||||
{...props}
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
* No avatars. Thinking blocks are collapsible.
|
||||
*/
|
||||
|
||||
import { AlertCircle, FileText } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { AlertCircle, FileText, RotateCcw, X, ZoomIn, ZoomOut } from 'lucide-react';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { useI18n } from '../../application/i18n/I18nProvider';
|
||||
import type { ChatMessage } from '../../infrastructure/ai/types';
|
||||
import { Dialog, DialogContent, DialogTitle } from '../ui/dialog';
|
||||
import {
|
||||
Conversation,
|
||||
ConversationContent,
|
||||
@@ -28,6 +29,70 @@ interface ChatMessageListProps {
|
||||
}
|
||||
|
||||
const ChatMessageList: React.FC<ChatMessageListProps> = ({ messages, isStreaming, onApprove, onReject }) => {
|
||||
const [preview, setPreview] = useState<{ src: string; name: string } | null>(null);
|
||||
const [zoom, setZoom] = useState(100);
|
||||
const [dragged, setDragged] = useState(false);
|
||||
const imgRef = useRef<HTMLImageElement>(null);
|
||||
const dragPos = useRef({ x: 0, y: 0 });
|
||||
const dragStart = useRef<{ startX: number; startY: number; origX: number; origY: number } | null>(null);
|
||||
|
||||
const applyTransform = useCallback((z: number, x: number, y: number, animate: boolean) => {
|
||||
if (!imgRef.current) return;
|
||||
imgRef.current.style.transition = animate ? 'transform 0.25s ease' : 'none';
|
||||
imgRef.current.style.transform = `scale(${z / 100}) translate(${x / (z / 100)}px, ${y / (z / 100)}px)`;
|
||||
}, []);
|
||||
|
||||
const zoomRef = useRef(100);
|
||||
const setZoomAndRef = useCallback((fn: (z: number) => number) => {
|
||||
setZoom(z => { const nz = fn(z); zoomRef.current = nz; return nz; });
|
||||
}, []);
|
||||
const zoomIn = useCallback(() => setZoomAndRef(z => { const nz = Math.min(z + 25, 200); applyTransform(nz, dragPos.current.x, dragPos.current.y, true); return nz; }), [applyTransform, setZoomAndRef]);
|
||||
const zoomOut = useCallback(() => setZoomAndRef(z => { const nz = Math.max(z - 25, 25); applyTransform(nz, dragPos.current.x, dragPos.current.y, true); return nz; }), [applyTransform, setZoomAndRef]);
|
||||
|
||||
const onWheel = useCallback((e: React.WheelEvent) => {
|
||||
e.preventDefault();
|
||||
const delta = e.deltaY > 0 ? -10 : 10;
|
||||
setZoomAndRef(z => {
|
||||
const nz = Math.max(25, Math.min(200, z + delta));
|
||||
applyTransform(nz, dragPos.current.x, dragPos.current.y, false);
|
||||
return nz;
|
||||
});
|
||||
}, [applyTransform, setZoomAndRef]);
|
||||
const openPreview = useCallback((src: string, name: string) => {
|
||||
setZoom(100); zoomRef.current = 100;
|
||||
setDragged(false);
|
||||
dragPos.current = { x: 0, y: 0 };
|
||||
setPreview({ src, name });
|
||||
}, []);
|
||||
|
||||
const resetPreview = useCallback(() => {
|
||||
setZoom(100); zoomRef.current = 100;
|
||||
setDragged(false);
|
||||
dragPos.current = { x: 0, y: 0 };
|
||||
applyTransform(100, 0, 0, true);
|
||||
}, [applyTransform]);
|
||||
|
||||
const onPointerDown = useCallback((e: React.PointerEvent) => {
|
||||
e.preventDefault();
|
||||
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
||||
dragStart.current = { startX: e.clientX, startY: e.clientY, origX: dragPos.current.x, origY: dragPos.current.y };
|
||||
}, []);
|
||||
|
||||
const onPointerMove = useCallback((e: React.PointerEvent) => {
|
||||
if (!dragStart.current) return;
|
||||
if ((e.buttons & 1) === 0) { dragStart.current = null; return; }
|
||||
const x = dragStart.current.origX + (e.clientX - dragStart.current.startX);
|
||||
const y = dragStart.current.origY + (e.clientY - dragStart.current.startY);
|
||||
dragPos.current = { x, y };
|
||||
applyTransform(zoomRef.current, x, y, false);
|
||||
}, [applyTransform]);
|
||||
|
||||
const endDrag = useCallback(() => {
|
||||
if (dragStart.current && (dragPos.current.x !== 0 || dragPos.current.y !== 0)) {
|
||||
setDragged(true);
|
||||
}
|
||||
dragStart.current = null;
|
||||
}, []);
|
||||
const { t } = useI18n();
|
||||
const visibleMessages = messages.filter(m => m.role !== 'system');
|
||||
const resolvedToolCallIds = new Set(
|
||||
@@ -59,6 +124,7 @@ const ChatMessageList: React.FC<ChatMessageListProps> = ({ messages, isStreaming
|
||||
const lastAssistantMessage = visibleMessages.findLast(m => m.role === 'assistant');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Conversation className="flex-1">
|
||||
<ConversationContent className="gap-1.5 px-4 py-2">
|
||||
{visibleMessages.map((message) => {
|
||||
@@ -102,7 +168,8 @@ const ChatMessageList: React.FC<ChatMessageListProps> = ({ messages, isStreaming
|
||||
key={att.filename ? `${att.filename}-${i}` : `att-${message.id}-${i}`}
|
||||
src={`data:${att.mediaType};base64,${att.base64Data}`}
|
||||
alt={att.filename || 'image'}
|
||||
className="max-h-[120px] max-w-[200px] rounded-md object-contain border border-border/20"
|
||||
className="max-h-[120px] max-w-[200px] rounded-md object-contain border border-border/20 cursor-pointer hover:opacity-80 transition-opacity"
|
||||
onClick={() => openPreview(`data:${att.mediaType};base64,${att.base64Data}`, att.filename || 'image')}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
@@ -184,6 +251,83 @@ const ChatMessageList: React.FC<ChatMessageListProps> = ({ messages, isStreaming
|
||||
</ConversationContent>
|
||||
<ConversationScrollButton />
|
||||
</Conversation>
|
||||
|
||||
{/* Image preview lightbox */}
|
||||
<Dialog open={!!preview} onOpenChange={(open) => { if (!open) setPreview(null); }}>
|
||||
<DialogContent
|
||||
hideCloseButton
|
||||
className="max-w-[min(90vw,800px)] max-h-[min(90vh,700px)] min-w-[280px] min-h-[200px] w-fit p-0 gap-0 focus:outline-none shadow-2xl"
|
||||
>
|
||||
{/* Title bar: filename | zoom controls | close — all in one flex row */}
|
||||
<div className="flex items-center h-10 px-3 border-b border-border/40 gap-2 shrink-0">
|
||||
<DialogTitle className="text-sm font-medium truncate flex-1">{preview?.name}</DialogTitle>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<button
|
||||
onClick={resetPreview}
|
||||
disabled={zoom === 100 && !dragged}
|
||||
className="p-1 rounded hover:bg-muted disabled:opacity-30 transition-colors text-muted-foreground"
|
||||
aria-label={t('common.reset')}
|
||||
>
|
||||
<RotateCcw size={14} />
|
||||
</button>
|
||||
<div className="w-px h-3.5 bg-border/40 mx-0.5" />
|
||||
<button
|
||||
onClick={zoomOut}
|
||||
disabled={zoom <= 25}
|
||||
className="p-1 rounded hover:bg-muted disabled:opacity-30 transition-colors text-muted-foreground"
|
||||
aria-label={t('common.zoomOut')}
|
||||
>
|
||||
<ZoomOut size={14} />
|
||||
</button>
|
||||
<span className="text-xs text-muted-foreground tabular-nums w-9 text-center select-none">{zoom}%</span>
|
||||
<button
|
||||
onClick={zoomIn}
|
||||
disabled={zoom >= 200}
|
||||
className="p-1 rounded hover:bg-muted disabled:opacity-30 transition-colors text-muted-foreground"
|
||||
aria-label={t('common.zoomIn')}
|
||||
>
|
||||
<ZoomIn size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setPreview(null)}
|
||||
className="p-1 rounded hover:bg-muted transition-colors text-muted-foreground shrink-0"
|
||||
aria-label={t('common.close')}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
{/* Image area with drag support */}
|
||||
{preview && (
|
||||
<div
|
||||
className="overflow-hidden flex items-center justify-center"
|
||||
style={{
|
||||
height: 'calc(min(90vh, 700px) - 40px)',
|
||||
cursor: 'grab',
|
||||
// Clamp aspect ratio: if image is extremely tall/wide, the container
|
||||
// constrains it; object-contain handles the rest.
|
||||
aspectRatio: 'auto',
|
||||
}}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={endDrag}
|
||||
onPointerCancel={endDrag}
|
||||
onWheel={onWheel}
|
||||
onLostPointerCapture={endDrag}
|
||||
>
|
||||
<img
|
||||
ref={imgRef}
|
||||
src={preview.src}
|
||||
alt={preview.name}
|
||||
draggable={false}
|
||||
className="select-none max-w-full max-h-full object-contain"
|
||||
style={{ transition: 'transform 0.25s ease' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ export const AGENT_DEFAULTS: Record<string, Omit<ExternalAgentConfig, "id" | "co
|
||||
name: "Claude Code",
|
||||
args: ["-p", "--output-format", "text", "{prompt}"],
|
||||
icon: "claude",
|
||||
acpCommand: "claude-code-acp",
|
||||
acpCommand: "claude-agent-acp",
|
||||
acpArgs: [],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -30,13 +30,13 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { hideCloseButton?: boolean }
|
||||
>(({ className, children, hideCloseButton, ...props }, ref) => {
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { hideCloseButton?: boolean; overlayClassName?: string }
|
||||
>(({ className, children, hideCloseButton, overlayClassName, ...props }, ref) => {
|
||||
const { t } = useI18n()
|
||||
|
||||
return (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogOverlay className={overlayClassName} />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
|
||||
@@ -21,6 +21,9 @@ module.exports = {
|
||||
'node_modules/node-pty/**/*',
|
||||
'node_modules/ssh2/**/*',
|
||||
'node_modules/cpu-features/**/*',
|
||||
'node_modules/@zed-industries/claude-agent-acp/**/*',
|
||||
'node_modules/@agentclientprotocol/sdk/**/*',
|
||||
'node_modules/@anthropic-ai/claude-agent-sdk/**/*',
|
||||
'node_modules/@zed-industries/codex-acp/**/*',
|
||||
'node_modules/@zed-industries/codex-acp-*/**/*',
|
||||
'node_modules/@modelcontextprotocol/sdk/**/*',
|
||||
|
||||
@@ -141,6 +141,48 @@ async function getShellEnv() {
|
||||
return _cachedShellEnv;
|
||||
}
|
||||
|
||||
// ── Claude Code ACP binary resolution ──
|
||||
|
||||
/**
|
||||
* Resolve the Claude ACP binary, returning { command, prependArgs }.
|
||||
*
|
||||
* On macOS/Linux a shebang-based .js script can be spawned directly, but on
|
||||
* Windows `child_process.spawn` does not interpret shebangs — so when the
|
||||
* resolved path is a JS file we invoke it via the system Node runtime.
|
||||
*/
|
||||
function resolveClaudeAcpBinaryPath(shellEnv, electronModule) {
|
||||
const binaryName = "claude-agent-acp";
|
||||
|
||||
// Dev mode: prefer system PATH (npm creates platform-appropriate wrappers)
|
||||
const isPackaged = electronModule?.app?.isPackaged;
|
||||
if (!isPackaged && shellEnv) {
|
||||
const systemPath = resolveCliFromPath(binaryName, shellEnv);
|
||||
if (systemPath) return { command: systemPath, prependArgs: [] };
|
||||
}
|
||||
|
||||
// Packaged build (or dev fallback): use npm-bundled binary
|
||||
try {
|
||||
const resolved = require.resolve("@zed-industries/claude-agent-acp/dist/index.js");
|
||||
const scriptPath = toUnpackedAsarPath(resolved);
|
||||
|
||||
// On Windows, .js files cannot be spawned directly (no shebang support) —
|
||||
// invoke via Node. In packaged Electron builds process.execPath is the
|
||||
// app binary (e.g. Netcatty.exe), not a Node runtime, so we must resolve
|
||||
// the real `node` from PATH. If Node is not installed, fall back to the
|
||||
// bare command name and let the system find the npm-generated .cmd wrapper.
|
||||
if (process.platform === "win32") {
|
||||
const nodePath = resolveCliFromPath("node", shellEnv);
|
||||
if (nodePath) {
|
||||
return { command: nodePath, prependArgs: [scriptPath] };
|
||||
}
|
||||
return { command: binaryName, prependArgs: [] };
|
||||
}
|
||||
return { command: scriptPath, prependArgs: [] };
|
||||
} catch {
|
||||
return { command: binaryName, prependArgs: [] };
|
||||
}
|
||||
}
|
||||
|
||||
// ── Stream chunk serialization ──
|
||||
|
||||
function serializeStreamChunk(chunk) {
|
||||
@@ -197,6 +239,7 @@ module.exports = {
|
||||
isLocalhostHostname,
|
||||
extractFirstNonLocalhostUrl,
|
||||
resolveCliFromPath,
|
||||
resolveClaudeAcpBinaryPath,
|
||||
toUnpackedAsarPath,
|
||||
getShellEnv,
|
||||
serializeStreamChunk,
|
||||
|
||||
@@ -18,6 +18,7 @@ const mcpServerBridge = require("./mcpServerBridge.cjs");
|
||||
const {
|
||||
stripAnsi,
|
||||
resolveCliFromPath,
|
||||
resolveClaudeAcpBinaryPath,
|
||||
getShellEnv,
|
||||
serializeStreamChunk,
|
||||
} = require("./ai/shellUtils.cjs");
|
||||
@@ -1176,7 +1177,7 @@ function registerHandlers(ipcMain) {
|
||||
}
|
||||
}
|
||||
|
||||
// Discover external agents from PATH, plus the bundled Codex CLI if present.
|
||||
// Discover external agents from PATH, plus bundled ACP binaries if present.
|
||||
ipcMain.handle("netcatty:ai:agents:discover", async (event) => {
|
||||
if (!validateSenderOrSettings(event)) return { ok: false, error: "Unauthorized IPC sender" };
|
||||
const agents = [];
|
||||
@@ -1186,9 +1187,10 @@ function registerHandlers(ipcMain) {
|
||||
name: "Claude Code",
|
||||
icon: "claude",
|
||||
description: "Anthropic's agentic coding assistant",
|
||||
acpCommand: "claude-code-acp",
|
||||
acpCommand: "claude-agent-acp",
|
||||
acpArgs: [],
|
||||
args: ["-p", "--output-format", "text", "{prompt}"],
|
||||
resolveAcp: resolveClaudeAcpBinaryPath,
|
||||
},
|
||||
{
|
||||
command: "codex",
|
||||
@@ -1198,6 +1200,7 @@ function registerHandlers(ipcMain) {
|
||||
acpCommand: "codex-acp",
|
||||
acpArgs: [],
|
||||
args: ["exec", "--full-auto", "--json", "{prompt}"],
|
||||
resolveAcp: resolveCodexAcpBinaryPath,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1222,20 +1225,54 @@ function registerHandlers(ipcMain) {
|
||||
resolvedPath = null;
|
||||
}
|
||||
|
||||
// If the base command is not on PATH, check whether the bundled ACP
|
||||
// binary is available — the agent can still work via ACP without the
|
||||
// standalone CLI installed.
|
||||
// resolveClaudeAcpBinaryPath returns { command, prependArgs },
|
||||
// resolveCodexAcpBinaryPath returns a plain string.
|
||||
let versionCommand = null;
|
||||
let versionPrependArgs = [];
|
||||
if (!resolvedPath && agent.resolveAcp) {
|
||||
const result = agent.resolveAcp(shellEnv, electronModule);
|
||||
if (typeof result === "string") {
|
||||
if (result && result !== agent.acpCommand && existsSync(result)) {
|
||||
resolvedPath = result;
|
||||
}
|
||||
} else if (result?.command) {
|
||||
// On Windows the command may be `node` with the script in prependArgs.
|
||||
// Use the script path for display/dedup so the UI shows the actual
|
||||
// agent rather than the Node binary.
|
||||
const scriptPath = result.prependArgs?.[0];
|
||||
const displayPath = scriptPath || result.command;
|
||||
if (displayPath !== agent.acpCommand && existsSync(displayPath)) {
|
||||
resolvedPath = displayPath;
|
||||
if (scriptPath) {
|
||||
versionCommand = result.command;
|
||||
versionPrependArgs = result.prependArgs;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolvedPath || seenPaths.has(resolvedPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let version = "";
|
||||
try {
|
||||
const result = await runCommand(resolvedPath, ["--version"], { env: shellEnv });
|
||||
// When the agent is invoked via Node (Windows), probe version with
|
||||
// the full command (e.g. `node /path/to/dist/index.js --version`).
|
||||
const probeCmd = versionCommand || resolvedPath;
|
||||
const probeArgs = [...versionPrependArgs, "--version"];
|
||||
const result = await runCommand(probeCmd, probeArgs, { env: shellEnv });
|
||||
version = (result.stdout || result.stderr || "").trim().split("\n")[0];
|
||||
} catch {
|
||||
version = "";
|
||||
}
|
||||
|
||||
const { resolveAcp: _unused, ...agentInfo } = agent;
|
||||
agents.push({
|
||||
...agent,
|
||||
...agentInfo,
|
||||
path: resolvedPath,
|
||||
version,
|
||||
available: true,
|
||||
@@ -1447,7 +1484,7 @@ function registerHandlers(ipcMain) {
|
||||
|
||||
// Known agent command names (must match knownAgents in discover handler)
|
||||
const ALLOWED_AGENT_COMMANDS = new Set([
|
||||
"claude", "claude-code-acp",
|
||||
"claude", "claude-agent-acp",
|
||||
"codex", "codex-acp",
|
||||
]);
|
||||
|
||||
@@ -1665,6 +1702,7 @@ function registerHandlers(ipcMain) {
|
||||
const shellEnv = await getShellEnv();
|
||||
const sessionCwd = cwd || process.cwd();
|
||||
const isCodexAgent = acpCommand === "codex-acp";
|
||||
const isClaudeAgent = acpCommand === "claude-agent-acp";
|
||||
|
||||
// Resolve API key from providerId (decrypted in main process only)
|
||||
const resolvedProvider = providerId ? resolveProviderApiKey(providerId) : null;
|
||||
@@ -1742,13 +1780,19 @@ function registerHandlers(ipcMain) {
|
||||
agentEnv.CODEX_API_KEY = apiKey;
|
||||
}
|
||||
|
||||
const claudeAcp = isClaudeAgent ? resolveClaudeAcpBinaryPath(shellEnv, electronModule) : null;
|
||||
const resolvedCommand = isCodexAgent
|
||||
? resolveCodexAcpBinaryPath(shellEnv, electronModule)
|
||||
: acpCommand;
|
||||
: claudeAcp
|
||||
? claudeAcp.command
|
||||
: acpCommand;
|
||||
const resolvedArgs = claudeAcp
|
||||
? [...claudeAcp.prependArgs, ...(acpArgs || [])]
|
||||
: acpArgs || [];
|
||||
|
||||
const provider = createACPProvider({
|
||||
command: resolvedCommand,
|
||||
args: acpArgs || [],
|
||||
args: resolvedArgs,
|
||||
env: agentEnv,
|
||||
session: {
|
||||
cwd: sessionCwd,
|
||||
@@ -1785,11 +1829,16 @@ function registerHandlers(ipcMain) {
|
||||
|
||||
cleanupAcpProvider(chatSessionId);
|
||||
|
||||
const fallbackClaudeAcp = isClaudeAgent ? resolveClaudeAcpBinaryPath(shellEnv, electronModule) : null;
|
||||
const fallbackProvider = createACPProvider({
|
||||
command: isCodexAgent
|
||||
? resolveCodexAcpBinaryPath(shellEnv, electronModule)
|
||||
: acpCommand,
|
||||
args: acpArgs || [],
|
||||
: fallbackClaudeAcp
|
||||
? fallbackClaudeAcp.command
|
||||
: acpCommand,
|
||||
args: fallbackClaudeAcp
|
||||
? [...fallbackClaudeAcp.prependArgs, ...(acpArgs || [])]
|
||||
: acpArgs || [],
|
||||
env: apiKey ? { ...shellEnv, CODEX_API_KEY: apiKey } : { ...shellEnv },
|
||||
session: {
|
||||
cwd: sessionCwd,
|
||||
|
||||
@@ -150,7 +150,7 @@ export interface ExternalAgentConfig {
|
||||
env?: Record<string, string>;
|
||||
icon?: string;
|
||||
enabled: boolean;
|
||||
/** ACP command (e.g. 'codex-acp', 'claude-code-acp', 'gemini --experimental-acp') */
|
||||
/** ACP command (e.g. 'codex-acp', 'claude-agent-acp', 'gemini --experimental-acp') */
|
||||
acpCommand?: string;
|
||||
acpArgs?: string[];
|
||||
}
|
||||
|
||||
351
package-lock.json
generated
351
package-lock.json
generated
@@ -37,6 +37,7 @@
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"@xterm/addon-webgl": "^0.18.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"@zed-industries/claude-agent-acp": "0.22.2",
|
||||
"@zed-industries/codex-acp": "0.10.0",
|
||||
"ai": "^6.0.116",
|
||||
"clsx": "2.1.1",
|
||||
@@ -194,6 +195,29 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk": {
|
||||
"version": "0.2.76",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.76.tgz",
|
||||
"integrity": "sha512-HZxvnT8ZWkzCnQygaYCA0dl8RSUzuVbxE1YG4ecy6vh4nQbTT36CxUxBy+QVdR12pPQluncC0mCOLhI2918Eaw==",
|
||||
"license": "SEE LICENSE IN README.md",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "^0.34.2",
|
||||
"@img/sharp-darwin-x64": "^0.34.2",
|
||||
"@img/sharp-linux-arm": "^0.34.2",
|
||||
"@img/sharp-linux-arm64": "^0.34.2",
|
||||
"@img/sharp-linux-x64": "^0.34.2",
|
||||
"@img/sharp-linuxmusl-arm64": "^0.34.2",
|
||||
"@img/sharp-linuxmusl-x64": "^0.34.2",
|
||||
"@img/sharp-win32-arm64": "^0.34.2",
|
||||
"@img/sharp-win32-x64": "^0.34.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-crypto/crc32": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz",
|
||||
@@ -2657,6 +2681,310 @@
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
||||
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
||||
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
@@ -6347,6 +6675,29 @@
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/@zed-industries/claude-agent-acp": {
|
||||
"version": "0.22.2",
|
||||
"resolved": "https://registry.npmjs.org/@zed-industries/claude-agent-acp/-/claude-agent-acp-0.22.2.tgz",
|
||||
"integrity": "sha512-GLiKxy5MBNS9UoiE1XaM9EHVxlEcvk0sXSMCnyDp9JNAQliynt0axZrhptTl5AWe6PXGjVh5hMFdPp+yulw2uQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.16.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.2.76",
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"claude-agent-acp": "dist/index.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@zed-industries/claude-agent-acp/node_modules/@agentclientprotocol/sdk": {
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.16.1.tgz",
|
||||
"integrity": "sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@zed-industries/codex-acp": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@zed-industries/codex-acp/-/codex-acp-0.10.0.tgz",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"@xterm/addon-webgl": "^0.18.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"@zed-industries/claude-agent-acp": "0.22.2",
|
||||
"@zed-industries/codex-acp": "0.10.0",
|
||||
"ai": "^6.0.116",
|
||||
"clsx": "2.1.1",
|
||||
|
||||
Reference in New Issue
Block a user