Compare commits

...

17 Commits

Author SHA1 Message Date
bincxz
f4bbe62a1d fix: eliminate scroll bounce when switching tabs with AI chat open
Some checks failed
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
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>
2026-03-19 19:43:06 +08:00
陈大猫
57e131a16e feat: support mouse wheel zoom in image preview (#409)
Scroll up to zoom in, scroll down to zoom out (10% per tick, range
25%-200%). Uses zoomRef to avoid stale closures so wheel + drag
always read the latest zoom level.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 19:34:40 +08:00
bincxz
ea6f9e138c feat: support mouse wheel zoom in image preview
Scroll up to zoom in, scroll down to zoom out (10% per tick, range
25%-200%). Uses zoomRef to avoid stale closures so wheel + drag
always read the latest zoom level.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 19:33:51 +08:00
陈大猫
5177ce2028 feat: image preview enhancements — zoom, drag, reset (#408)
* fix: remove padding around image in preview modal

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add zoom controls and constrain image preview modal size

- Add zoom in/out buttons with percentage display in the title bar
- Zoom range: 25% - 200%, step 25%, resets to 100% on open
- Constrain modal max size to 800x700px to prevent oversized previews
- Scrollable image area when zoomed beyond container

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: improve image preview with aligned controls, drag-pan, animation

- Put filename, zoom controls, and close button in a single flex row
  so they are properly aligned
- Add smooth animation on zoom (width 0.2s ease, transform 0.15s ease)
- Add drag-to-pan when zoomed beyond 100% (pointer capture based)
- Set min-width/min-height on modal to prevent extreme aspect ratios
  from making the dialog too narrow or too short
- Container uses overflow hidden + fixed height to contain the image

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use transform scale for smooth zoom animation

Replace width-based zoom with transform: scale() which is GPU-
accelerated and produces smooth 0.25s ease transitions when clicking
zoom in/out buttons. Drag translation is adjusted for current scale.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: allow drag at any zoom level and add reset button

- Remove zoom > 100 restriction on drag — image can be panned at any
  zoom level
- Add reset button (rotate-ccw icon) left of zoom controls with a
  separator, resets zoom to 100% and position to center
- Reset button is disabled when already at default state
- Cursor shows grab at all times in the image area

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace backdrop blur with box-shadow for image preview modal

Drop the dark blurred overlay in favor of a shadow-2xl box-shadow
so the window boundary is clear without obscuring the background.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: use refs for drag state to avoid rerendering chat list

Drag position was stored in React state, causing the entire message
list to rerender on every pointermove frame. Move drag tracking to
refs and update the img transform directly via DOM, so only zoom
button clicks trigger React rerenders.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add aria-labels to image preview controls for accessibility

Add localized aria-label to reset, zoom in, zoom out, and close
buttons. Add i18n keys for common.reset, common.zoomIn, common.zoomOut
in en and zh-CN locales.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: reset button restores drag position and stays enabled after drag

Reset was disabled when zoom was 100%, so dragging without zooming
left no way to restore position. Track drag state separately and
keep reset enabled whenever the image has been dragged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: prevent stuck drag state on pointer cancel or lost capture

If pointerup fires outside the window, dragStart was never cleared
and the image kept following the cursor. Now:
- Check e.buttons in pointermove to bail if primary button released
- Handle onPointerCancel and onLostPointerCapture to end drag

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 19:25:49 +08:00
bincxz
9f44112479 fix: prevent stuck drag state on pointer cancel or lost capture
If pointerup fires outside the window, dragStart was never cleared
and the image kept following the cursor. Now:
- Check e.buttons in pointermove to bail if primary button released
- Handle onPointerCancel and onLostPointerCapture to end drag

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 19:02:57 +08:00
bincxz
6999f362a3 fix: reset button restores drag position and stays enabled after drag
Reset was disabled when zoom was 100%, so dragging without zooming
left no way to restore position. Track drag state separately and
keep reset enabled whenever the image has been dragged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 18:55:46 +08:00
bincxz
fc546c2430 fix: add aria-labels to image preview controls for accessibility
Add localized aria-label to reset, zoom in, zoom out, and close
buttons. Add i18n keys for common.reset, common.zoomIn, common.zoomOut
in en and zh-CN locales.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 18:48:19 +08:00
bincxz
f7e4953038 perf: use refs for drag state to avoid rerendering chat list
Drag position was stored in React state, causing the entire message
list to rerender on every pointermove frame. Move drag tracking to
refs and update the img transform directly via DOM, so only zoom
button clicks trigger React rerenders.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 18:04:46 +08:00
bincxz
922376fa06 fix: replace backdrop blur with box-shadow for image preview modal
Drop the dark blurred overlay in favor of a shadow-2xl box-shadow
so the window boundary is clear without obscuring the background.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 17:50:51 +08:00
bincxz
3d4ca46c9b feat: allow drag at any zoom level and add reset button
- Remove zoom > 100 restriction on drag — image can be panned at any
  zoom level
- Add reset button (rotate-ccw icon) left of zoom controls with a
  separator, resets zoom to 100% and position to center
- Reset button is disabled when already at default state
- Cursor shows grab at all times in the image area

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 17:49:55 +08:00
bincxz
1d8f203f5b fix: use transform scale for smooth zoom animation
Replace width-based zoom with transform: scale() which is GPU-
accelerated and produces smooth 0.25s ease transitions when clicking
zoom in/out buttons. Drag translation is adjusted for current scale.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 17:47:10 +08:00
bincxz
41d079a806 feat: improve image preview with aligned controls, drag-pan, animation
- Put filename, zoom controls, and close button in a single flex row
  so they are properly aligned
- Add smooth animation on zoom (width 0.2s ease, transform 0.15s ease)
- Add drag-to-pan when zoomed beyond 100% (pointer capture based)
- Set min-width/min-height on modal to prevent extreme aspect ratios
  from making the dialog too narrow or too short
- Container uses overflow hidden + fixed height to contain the image

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 17:44:58 +08:00
bincxz
93c95959d3 feat: add zoom controls and constrain image preview modal size
- Add zoom in/out buttons with percentage display in the title bar
- Zoom range: 25% - 200%, step 25%, resets to 100% on open
- Constrain modal max size to 800x700px to prevent oversized previews
- Scrollable image area when zoomed beyond container

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 17:41:27 +08:00
bincxz
e7300429f8 fix: remove padding around image in preview modal
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 17:37:54 +08:00
陈大猫
c7743d082a feat: click-to-preview for images in AI chat (#407)
* feat: add click-to-preview for images in AI chat

Uploaded images in AI chat messages can now be clicked to open a
full-size lightbox preview. Clicking the overlay or the image again
dismisses it. Uses the existing Radix Dialog component.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use standard dialog style for image preview with close button

Replace transparent borderless overlay with proper windowed dialog that
has a background, border, and the built-in close button (X) in the
top-right corner. Remove focus ring that caused the blue border.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add title bar with filename and blurred backdrop to image preview

- Show filename in dialog header with border separator
- Add overlayClassName prop to DialogContent for per-instance overlay
  customization (e.g. backdrop blur, custom background)
- Apply semi-transparent black background with backdrop-blur on overlay

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: align title and close button vertically in image preview

Adjust header padding and close button position so the filename and
X button sit on the same visual line.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 17:23:21 +08:00
陈大猫
56a4fe905d fix: handle Windows spawn for Claude ACP bundled JS binary (#405)
* fix: handle Windows spawn for Claude ACP bundled JS binary

On Windows, child_process.spawn does not interpret shebangs, so spawning
a .js file directly (like claude-agent-acp's dist/index.js) fails with
ENOENT. The @mcpc-tech/acp-ai-provider uses raw spawn() internally.

Change resolveClaudeAcpBinaryPath to return { command, prependArgs } so
that on Windows the resolved .js script is invoked via process.execPath
(Node) with the script path prepended to args. On macOS/Linux the
shebang works natively so the script is spawned directly as before.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use system Node instead of process.execPath on Windows

In packaged Electron builds, process.execPath points to the app binary
(e.g. Netcatty.exe), not a Node runtime. Additionally, main.cjs deletes
ELECTRON_RUN_AS_NODE at startup and the agent spawn handler blocks it
in DANGEROUS_ENV_KEYS.

Resolve the real `node` from PATH instead. If Node is not installed,
fall back to the bare `claude-agent-acp` command name so the system
can find the npm-generated .cmd wrapper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use script path for display and probe version correctly on Windows

In discovery, when resolveClaudeAcpBinaryPath returns { command: node,
prependArgs: [scriptPath] }, use the script path for UI display and
dedup, and probe version with the full command (node script --version)
instead of running node --version.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 17:00:23 +08:00
陈大猫
b17775307f fix: bundle claude-code-acp to prevent crash when binary is missing (#404)
* fix: bundle claude-code-acp to prevent crash when binary is missing (#400)

When users select Claude Code in the AI module, the app spawns
`claude-code-acp` via ACP. Previously only the `claude` CLI was checked
during agent discovery, so if `claude-code-acp` was not on PATH the
spawn would fail with ENOENT and crash the Electron main process.

- Add `@zed-industries/claude-code-acp` as a bundled dependency
- Add `resolveClaudeAcpBinaryPath()` that checks PATH first, then
  falls back to the npm-bundled binary (mirrors Codex pattern)
- Use the resolver in both the primary and fallback ACP provider paths
- Update agent discovery to detect agents via bundled ACP binary when
  the standalone CLI is not installed

Closes #400

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add claude-code-acp and its deps to asarUnpack

In packaged Electron builds, files inside app.asar cannot be executed
by child_process.spawn. Add claude-code-acp and its runtime dependencies
to asarUnpack so the binary is accessible in production.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: migrate from deprecated claude-code-acp to claude-agent-acp

The @zed-industries/claude-code-acp package has been renamed to
@zed-industries/claude-agent-acp (bin: claude-agent-acp). Update all
references across the codebase:

- package.json: replace dep with @zed-industries/claude-agent-acp@0.22.2
- electron-builder.config.cjs: update asarUnpack entries, remove stale
  deps (diff, minimatch) no longer needed by the new package
- shellUtils.cjs: update binary name and require.resolve path
- aiBridge.cjs: update acpCommand, ALLOWED_AGENT_COMMANDS, isClaudeAgent
- settings types, i18n locales: update command references

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 16:24:29 +08:00
12 changed files with 617 additions and 20 deletions

View File

@@ -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',

View File

@@ -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': '未找到',

View File

@@ -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}

View File

@@ -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>
</>
);
};

View File

@@ -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: [],
},
};

View File

@@ -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(

View File

@@ -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/**/*',

View File

@@ -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,

View File

@@ -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,

View File

@@ -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
View File

@@ -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",

View File

@@ -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",