feat: add Claude Agent SDK streaming and Codex OAuth integration

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>
This commit is contained in:
bincxz
2026-03-14 21:27:41 +08:00
parent 7f3214e088
commit 8949394756
23 changed files with 2460 additions and 226 deletions

4
.gitignore vendored
View File

@@ -35,8 +35,8 @@ coverage
*.sln
*.sw?
# Claude Code local settings
/.claude/settings.local.json
# Claude Code
/.claude/
/CLAUDE.md
# AI / Superpowers generated docs (local only)

View File

@@ -57,9 +57,10 @@ export function useAgentDiscovery(
const newArgs = JSON.stringify(match.args);
const acpChanged = ea.acpCommand !== match.acpCommand
|| JSON.stringify(ea.acpArgs || []) !== JSON.stringify(match.acpArgs || []);
if (currentArgs !== newArgs || acpChanged) {
const sdkTypeChanged = ea.sdkType !== match.sdkType;
if (currentArgs !== newArgs || acpChanged || sdkTypeChanged) {
changed = true;
return { ...ea, args: match.args, acpCommand: match.acpCommand, acpArgs: match.acpArgs };
return { ...ea, args: match.args, acpCommand: match.acpCommand, acpArgs: match.acpArgs, sdkType: match.sdkType };
}
return ea;
});
@@ -86,6 +87,7 @@ export function useAgentDiscovery(
enabled: true,
acpCommand: agent.acpCommand,
acpArgs: agent.acpArgs,
sdkType: agent.sdkType,
};
},
[],

View File

@@ -30,6 +30,7 @@ import { createCattyTools } from '../infrastructure/ai/sdk/tools';
import { exportAsMarkdown, exportAsJSON, exportAsPlainText, getExportFilename } from '../infrastructure/ai/conversationExport';
import { runExternalAgentTurn } from '../infrastructure/ai/externalAgentAdapter';
import { runAcpAgentTurn } from '../infrastructure/ai/acpAgentAdapter';
import { runClaudeAgentTurn } from '../infrastructure/ai/claudeAgentAdapter';
import { useAgentDiscovery } from '../application/state/useAgentDiscovery';
import { Button } from './ui/button';
import { ScrollArea } from './ui/scroll-area';
@@ -204,12 +205,17 @@ const AIChatSidePanelInner: React.FC<AIChatSidePanelProps> = ({
const handleSend = useCallback(async () => {
const trimmed = inputValue.trim();
console.log('[AIChatPanel] handleSend called, trimmed:', JSON.stringify(trimmed?.slice(0, 50)), 'isStreaming:', isStreaming, 'currentAgentId:', currentAgentId);
if (!trimmed || isStreaming) return;
const isExternalAgent = currentAgentId !== 'catty';
console.log('[AIChatPanel] isExternalAgent:', isExternalAgent, 'activeProvider:', activeProvider?.id);
// For built-in agent, we need a provider configured
if (!isExternalAgent && !activeProvider) return;
if (!isExternalAgent && !activeProvider) {
console.warn('[AIChatPanel] No active provider configured for built-in agent, aborting');
return;
}
// Create session if needed
let sessionId = activeSessionId;
@@ -255,6 +261,8 @@ const AIChatSidePanelInner: React.FC<AIChatSidePanelProps> = ({
// Get current session for context
const currentSession = sessions.find((s) => s.id === sessionId);
console.log('[AIChatPanel] agentConfig:', agentConfig ? { id: agentConfig.id, name: agentConfig.name, sdkType: agentConfig.sdkType, acpCommand: agentConfig.acpCommand } : 'catty');
if (isExternalAgent) {
if (!agentConfig) {
updateLastMessage(sessionId, msg => ({
@@ -269,9 +277,86 @@ const AIChatSidePanelInner: React.FC<AIChatSidePanelProps> = ({
const bridge = (window as unknown as { netcatty?: Record<string, unknown> }).netcatty as
Record<string, (...args: unknown[]) => unknown> | undefined;
// Use ACP protocol if the agent supports it
if (agentConfig.acpCommand && bridge) {
console.log('[AIChatPanel] bridge available:', !!bridge, 'sdkType:', agentConfig.sdkType, 'acpCommand:', agentConfig.acpCommand);
// Use Claude Agent SDK if the agent specifies it
if (agentConfig.sdkType === 'claude-agent-sdk' && bridge) {
const requestId = `claude_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
console.log('[AIChatPanel] → Claude Agent SDK path, requestId:', requestId);
try {
await runClaudeAgentTurn(
bridge,
requestId,
sessionId!,
agentConfig,
trimmed,
{
onTextDelta: (text: string) => {
updateLastMessage(sessionId!, msg => ({
...msg,
content: msg.content + text,
thinkingDurationMs: msg.thinking && !msg.thinkingDurationMs
? Date.now() - msg.timestamp
: msg.thinkingDurationMs,
}));
},
onThinkingDelta: (text: string) => {
updateLastMessage(sessionId!, msg => ({
...msg,
thinking: (msg.thinking || '') + text,
}));
},
onThinkingDone: () => {
updateLastMessage(sessionId!, msg => ({
...msg,
thinkingDurationMs: msg.thinkingDurationMs || (Date.now() - msg.timestamp),
}));
},
onToolCall: (toolName: string, args: Record<string, unknown>) => {
updateLastMessage(sessionId!, msg => ({
...msg,
toolCalls: [...(msg.toolCalls || []), {
id: `tc_${Date.now()}`,
name: toolName,
arguments: args,
}],
executionStatus: 'running',
}));
},
onToolResult: (toolCallId: string, result: string) => {
addMessageToSession(sessionId!, {
id: generateId(),
role: 'tool',
content: '',
toolResults: [{ toolCallId, content: result, isError: false }],
timestamp: Date.now(),
executionStatus: 'completed',
});
},
onError: (error: string) => {
updateLastMessage(sessionId!, msg => ({
...msg,
content: msg.content + '\n\n**Error:** ' + error,
executionStatus: 'failed',
}));
},
onDone: () => {},
},
abortController.signal,
);
} catch (err) {
if (!abortController.signal.aborted) {
updateLastMessage(sessionId!, msg => ({
...msg,
content: msg.content + '\n\n**Error:** ' + (err instanceof Error ? err.message : String(err)),
}));
}
}
} else if (agentConfig.acpCommand && bridge) {
// Use ACP protocol if the agent supports it
const requestId = `acp_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
console.log('[AIChatPanel] → ACP path, requestId:', requestId, 'acpCommand:', agentConfig.acpCommand);
// Try to find an API key from configured providers for this agent
const openaiProvider = providers.find(p => p.providerId === 'openai' && p.enabled && p.apiKey);
@@ -518,6 +603,7 @@ const AIChatSidePanelInner: React.FC<AIChatSidePanelProps> = ({
activeModelId,
globalPermissionMode,
commandBlocklist,
providers,
sessions,
externalAgents,
terminalSessions,
@@ -544,6 +630,8 @@ const AIChatSidePanelInner: React.FC<AIChatSidePanelProps> = ({
const handleDeleteSession = useCallback(
(e: React.MouseEvent, sessionId: string) => {
e.stopPropagation();
const bridge = (window as unknown as { netcatty?: { aiAcpCleanup?: (chatSessionId: string) => Promise<{ ok: boolean }> } }).netcatty;
void bridge?.aiAcpCleanup?.(sessionId).catch(() => {});
deleteSession(sessionId);
if (activeSessionId === sessionId) {
setActiveSessionId(null);

View File

@@ -26,7 +26,7 @@ export const MessageContent = ({ children, className, ...props }: MessageContent
<div
className={cn(
'flex w-fit min-w-0 max-w-full flex-col gap-1.5 overflow-hidden text-[13px] leading-relaxed',
'group-[.is-user]:ml-auto group-[.is-user]:rounded-2xl group-[.is-user]:border group-[.is-user]:border-border/50 group-[.is-user]:px-4 group-[.is-user]:py-2.5',
'group-[.is-user]:ml-auto group-[.is-user]:rounded-lg group-[.is-user]:border group-[.is-user]:border-border/50 group-[.is-user]:bg-muted/50 group-[.is-user]:px-4 group-[.is-user]:py-2.5',
'group-[.is-assistant]:w-full group-[.is-assistant]:text-foreground/90',
className,
)}
@@ -60,9 +60,9 @@ export const MessageResponse = memo(
className={cn(
'size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0',
// Style the rendered markdown
'[&_pre]:rounded-md [&_pre]:bg-muted/40 [&_pre]:border [&_pre]:border-border/30 [&_pre]:p-3 [&_pre]:text-[12px] [&_pre]:overflow-x-auto',
// Code: base styles (code-block overrides are in index.css)
'[&_code]:text-[12px] [&_code]:font-mono',
'[&_p_code]:px-1 [&_p_code]:py-0.5 [&_p_code]:rounded [&_p_code]:bg-muted/40',
'[&_p_code]:px-[0.4em] [&_p_code]:py-[0.15em] [&_p_code]:rounded [&_p_code]:bg-foreground/[0.06] [&_p_code]:text-[85%]',
'[&_p]:my-1.5',
'[&_ul]:my-1.5 [&_ul]:pl-4 [&_ul]:list-disc',
'[&_ol]:my-1.5 [&_ol]:pl-4 [&_ol]:list-decimal',

View File

@@ -33,62 +33,62 @@ const AGENT_ICON_VISUALS: Record<AgentIconKey, AgentIconVisual> = {
catty: {
src: '/ai/agents/sparkles.svg',
badgeClassName: 'border-white/8 bg-white/[0.04]',
imageClassName: 'object-contain brightness-0 invert opacity-90',
imageClassName: 'object-contain dark:brightness-0 dark:invert opacity-90',
},
openai: {
src: '/ai/providers/openai.svg',
badgeClassName: 'border-emerald-500/22 bg-emerald-500/12',
imageClassName: 'object-contain brightness-0 invert',
imageClassName: 'object-contain dark:brightness-0 dark:invert',
},
claude: {
src: '/ai/agents/claude.svg',
badgeClassName: 'border-orange-500/22 bg-orange-500/12',
imageClassName: 'object-contain brightness-0 invert',
imageClassName: 'object-contain dark:brightness-0 dark:invert',
},
anthropic: {
src: '/ai/providers/anthropic.svg',
badgeClassName: 'border-orange-500/22 bg-orange-500/12',
imageClassName: 'object-contain brightness-0 invert',
imageClassName: 'object-contain dark:brightness-0 dark:invert',
},
gemini: {
src: '/ai/agents/gemini.svg',
badgeClassName: 'border-sky-500/22 bg-sky-500/12',
imageClassName: 'object-contain brightness-0 invert',
imageClassName: 'object-contain dark:brightness-0 dark:invert',
},
google: {
src: '/ai/providers/google.svg',
badgeClassName: 'border-sky-500/22 bg-sky-500/12',
imageClassName: 'object-contain brightness-0 invert',
imageClassName: 'object-contain dark:brightness-0 dark:invert',
},
ollama: {
src: '/ai/providers/ollama.svg',
badgeClassName: 'border-violet-500/22 bg-violet-500/12',
imageClassName: 'object-contain brightness-0 invert',
imageClassName: 'object-contain dark:brightness-0 dark:invert',
},
openrouter: {
src: '/ai/providers/openrouter.svg',
badgeClassName: 'border-fuchsia-500/22 bg-fuchsia-500/12',
imageClassName: 'object-contain brightness-0 invert',
imageClassName: 'object-contain dark:brightness-0 dark:invert',
},
zed: {
src: '/ai/agents/zed.svg',
badgeClassName: 'border-cyan-500/22 bg-cyan-500/12',
imageClassName: 'object-contain brightness-0 invert',
imageClassName: 'object-contain dark:brightness-0 dark:invert',
},
atom: {
src: '/ai/agents/atom.svg',
badgeClassName: 'border-amber-500/18 bg-amber-500/10',
imageClassName: 'object-contain brightness-0 invert opacity-90',
imageClassName: 'object-contain dark:brightness-0 dark:invert opacity-90',
},
terminal: {
src: '/ai/agents/terminal.svg',
badgeClassName: 'border-white/8 bg-white/[0.04]',
imageClassName: 'object-contain brightness-0 invert opacity-90',
imageClassName: 'object-contain dark:brightness-0 dark:invert opacity-90',
},
plus: {
src: '/ai/agents/plus.svg',
badgeClassName: 'border-white/8 bg-white/[0.04]',
imageClassName: 'object-contain brightness-0 invert opacity-85',
imageClassName: 'object-contain dark:brightness-0 dark:invert opacity-85',
},
};

View File

@@ -56,8 +56,8 @@ const AgentMenuRow: React.FC<{
<button
onClick={onClick}
className={cn(
'flex h-10 w-full items-center gap-3 rounded-md px-4 text-left text-[13px] text-foreground/86 transition-colors cursor-pointer hover:bg-white/[0.04] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/30',
isActive && 'bg-white/[0.06]',
'flex h-10 w-full items-center gap-3 px-4 text-left text-[13px] text-foreground/86 transition-colors cursor-pointer hover:bg-muted focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/30',
isActive && 'bg-muted',
)}
>
<AgentIconBadge agent={agent} size="xs" variant="plain" className="opacity-78" />
@@ -182,7 +182,7 @@ const AgentSelector: React.FC<AgentSelectorProps> = ({
<DropdownTrigger asChild>
<button
type="button"
className="group flex h-8 min-w-0 max-w-[170px] items-center gap-2 rounded-md px-2 text-left transition-colors hover:bg-white/[0.04] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/28"
className="group flex h-8 min-w-0 max-w-[170px] items-center gap-2 rounded-md px-2 text-left transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/28"
>
<AgentIconBadge
agent={currentAgent}
@@ -206,7 +206,7 @@ const AgentSelector: React.FC<AgentSelectorProps> = ({
<DropdownContent
align="start"
sideOffset={6}
className="w-[288px] rounded-2xl border border-border/50 bg-[#171717]/98 p-0 text-foreground shadow-[0_22px_56px_rgba(0,0,0,0.54)] supports-[backdrop-filter]:bg-[#171717]/94 supports-[backdrop-filter]:backdrop-blur-xl"
className="w-[288px] rounded-2xl border border-border/50 bg-popover p-0 text-foreground shadow-lg supports-[backdrop-filter]:backdrop-blur-xl"
>
{BUILTIN_AGENTS.map((agent) => (
<AgentMenuRow
@@ -265,7 +265,7 @@ const AgentSelector: React.FC<AgentSelectorProps> = ({
<div className="mx-0 my-1 border-t border-border/50" />
<button
onClick={handleManageAgents}
className="flex h-10 w-full items-center gap-3 rounded-md px-4 text-left text-[13px] text-foreground/82 transition-colors cursor-pointer hover:bg-white/[0.04] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/30"
className="flex h-10 w-full items-center gap-3 px-4 text-left text-[13px] text-foreground/82 transition-colors cursor-pointer hover:bg-muted focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/30"
>
<AgentIconBadge agent="add-more" size="xs" variant="plain" className="opacity-72" />
<span className="min-w-0 flex-1 truncate">Add More Agents</span>

View File

@@ -6,7 +6,7 @@
* and a bottom toolbar with muted controls + subtle send button.
*/
import { Cpu, Expand, Plus, Shield, Sparkles } from 'lucide-react';
import { Cpu, Expand, Plus, Shield } from 'lucide-react';
import React, { useCallback, useState } from 'react';
import type { FormEvent } from 'react';
import {
@@ -102,10 +102,6 @@ const ChatInput: React.FC<ChatInputProps> = ({
<PromptInputButton tooltip="Attach context" className={iconButtonClassName}>
<Plus size={13} />
</PromptInputButton>
<div className={chipClassName}>
<Sparkles size={11} className="text-primary" />
<span className="truncate max-w-[92px]">{agentName || 'Catty Agent'}</span>
</div>
<div className={chipClassName}>
<Cpu size={11} className="text-muted-foreground/64" />
<span className="truncate max-w-[82px]">{modelLabel}</span>

View File

@@ -39,7 +39,7 @@ const ChatMessageList: React.FC<ChatMessageListProps> = ({ messages, isStreaming
return (
<Conversation className="flex-1">
<ConversationContent className="gap-2 px-4 py-3">
<ConversationContent className="gap-1.5 px-4 py-2">
{visibleMessages.map((message) => {
if (message.role === 'tool') {
return message.toolResults?.map((tr) => (

View File

@@ -73,7 +73,7 @@ const ThinkingBlock: React.FC<ThinkingBlockProps> = ({
const preview = content.length > 60 ? content.slice(0, 60) + '…' : content;
return (
<div className="my-1">
<div className="mb-0.5">
{/* Header */}
<button
onClick={toggle}
@@ -84,7 +84,7 @@ const ThinkingBlock: React.FC<ThinkingBlockProps> = ({
className={cn(
'shrink-0 text-muted-foreground/50 transition-transform duration-200',
isExpanded && 'rotate-90',
!isExpanded && 'opacity-0 group-hover:opacity-100',
!isExpanded && 'opacity-50',
)}
/>
<span className="text-[12px] font-medium text-muted-foreground/70 whitespace-nowrap shrink-0">
@@ -108,7 +108,7 @@ const ThinkingBlock: React.FC<ThinkingBlockProps> = ({
{/* Content */}
{isExpanded && content && (
<div className="relative mt-0.5">
<div className="relative">
{/* Top gradient fade */}
{isStreaming && (
<div className="absolute inset-x-0 top-0 h-4 bg-gradient-to-b from-background to-transparent z-10 pointer-events-none" />

View File

@@ -7,9 +7,13 @@ import {
ChevronDown,
Eye,
EyeOff,
ExternalLink,
Globe,
LogIn,
LogOut,
Pencil,
Plus,
RefreshCw,
ScanSearch,
Shield,
Trash2,
@@ -19,10 +23,12 @@ import React, { useCallback, useEffect, useState } from "react";
import type {
AIPermissionMode,
AIProviderId,
DiscoveredAgent,
ExternalAgentConfig,
ProviderConfig,
} from "../../../infrastructure/ai/types";
import { PROVIDER_PRESETS } from "../../../infrastructure/ai/types";
import { useAgentDiscovery } from "../../../application/state/useAgentDiscovery";
import { encryptField, decryptField } from "../../../infrastructure/persistence/secureFieldAdapter";
import { TabsContent } from "../../ui/tabs";
import { Button } from "../../ui/button";
@@ -54,6 +60,51 @@ interface SettingsAITabProps {
setMaxIterations: (value: number) => void;
}
type CodexIntegrationState =
| "connected_chatgpt"
| "connected_api_key"
| "not_logged_in"
| "unknown";
interface CodexIntegrationStatus {
state: CodexIntegrationState;
isConnected: boolean;
rawOutput: string;
exitCode: number | null;
}
type CodexLoginState = "running" | "success" | "error" | "cancelled";
interface CodexLoginSession {
sessionId: string;
state: CodexLoginState;
url: string | null;
output: string;
error: string | null;
exitCode: number | null;
}
interface NetcattyAiBridge {
aiCodexGetIntegration?: () => Promise<CodexIntegrationStatus>;
aiCodexStartLogin?: () => Promise<{ ok: boolean; session?: CodexLoginSession; error?: string }>;
aiCodexGetLoginSession?: (sessionId: string) => Promise<{ ok: boolean; session?: CodexLoginSession; error?: string }>;
aiCodexCancelLogin?: (sessionId: string) => Promise<{ ok: boolean; found?: boolean; session?: CodexLoginSession; error?: string }>;
aiCodexLogout?: () => Promise<{ ok: boolean; state?: CodexIntegrationState; isConnected?: boolean; rawOutput?: string; logoutOutput?: string; error?: string }>;
openExternal?: (url: string) => Promise<void>;
}
function getBridge(): NetcattyAiBridge | undefined {
return (window as unknown as { netcatty?: NetcattyAiBridge }).netcatty;
}
function normalizeCodexBridgeError(error: unknown): string {
const message = error instanceof Error ? error.message : String(error);
if (message.includes("No handler registered for 'netcatty:ai:codex:")) {
return "Codex main-process handlers are not loaded yet. Fully restart Netcatty, or restart the Electron dev process, then try again.";
}
return message;
}
// ---------------------------------------------------------------------------
// Provider icon helper
// ---------------------------------------------------------------------------
@@ -409,6 +460,143 @@ const ExternalAgentCard: React.FC<{
</div>
);
const DetectedAgentCard: React.FC<{
agent: DiscoveredAgent;
onAdd: () => void;
}> = ({ agent, onAdd }) => (
<div className="flex items-center gap-3 py-2.5 px-3 rounded-lg border border-border/60 bg-muted/20">
<div className="w-7 h-7 rounded-md bg-muted flex items-center justify-center shrink-0">
<Bot size={14} className="text-muted-foreground" />
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium truncate">{agent.name}</div>
<div className="text-xs text-muted-foreground font-mono truncate mt-0.5">
{agent.version || agent.path}
</div>
</div>
<Button variant="outline" size="sm" onClick={onAdd} className="gap-1.5">
<Plus size={14} />
Add
</Button>
</div>
);
const CodexConnectionCard: React.FC<{
integration: CodexIntegrationStatus | null;
loginSession: CodexLoginSession | null;
isLoading: boolean;
hasOpenAiProviderKey: boolean;
error: string | null;
onRefresh: () => void;
onConnect: () => void;
onCancel: () => void;
onOpenUrl: () => void;
onLogout: () => void;
}> = ({
integration,
loginSession,
isLoading,
hasOpenAiProviderKey,
error,
onRefresh,
onConnect,
onCancel,
onOpenUrl,
onLogout,
}) => {
const status = loginSession?.state === "running"
? "Awaiting login"
: integration?.state === "connected_chatgpt"
? "Connected via ChatGPT"
: integration?.state === "connected_api_key"
? "Connected via API key"
: integration?.state === "not_logged_in"
? "Not connected"
: "Status unknown";
const statusClassName = loginSession?.state === "running"
? "text-amber-500"
: integration?.isConnected
? "text-emerald-500"
: "text-muted-foreground";
const outputText = loginSession?.error
? loginSession.error
: loginSession?.output?.trim()
? loginSession.output.trim()
: integration?.rawOutput?.trim()
? integration.rawOutput.trim()
: "";
return (
<div className="rounded-lg border border-border/60 bg-muted/20 p-4 space-y-3">
<div className="flex items-start justify-between gap-4">
<div className="min-w-0">
<div className="flex items-center gap-2">
<ProviderIconBadge providerId="openai" size="sm" />
<span className="text-sm font-medium">Codex CLI</span>
</div>
<p className="text-xs text-muted-foreground mt-2 leading-5">
Bundled <span className="font-mono">codex</span> + <span className="font-mono">codex-acp</span> for ACP protocol streaming.
Login with ChatGPT subscription here, or configure an OpenAI provider API key (passed as <span className="font-mono">CODEX_API_KEY</span>).
</p>
</div>
<div className={cn("text-xs font-medium shrink-0", statusClassName)}>
{status}
</div>
</div>
<div className="flex items-center gap-2 flex-wrap">
{loginSession?.state === "running" ? (
<>
<Button variant="default" size="sm" onClick={onOpenUrl} disabled={!loginSession.url}>
<ExternalLink size={14} className="mr-1.5" />
Open Login
</Button>
<Button variant="outline" size="sm" onClick={onCancel}>
<X size={14} className="mr-1.5" />
Cancel
</Button>
</>
) : integration?.isConnected ? (
<Button variant="outline" size="sm" onClick={onLogout}>
<LogOut size={14} className="mr-1.5" />
Logout
</Button>
) : (
<Button variant="default" size="sm" onClick={onConnect}>
<LogIn size={14} className="mr-1.5" />
Connect ChatGPT
</Button>
)}
<Button variant="outline" size="sm" onClick={onRefresh} disabled={isLoading}>
<RefreshCw size={14} className={cn("mr-1.5", isLoading && "animate-spin")} />
Refresh Status
</Button>
</div>
{hasOpenAiProviderKey && (
<p className="text-xs text-emerald-500">
Enabled OpenAI provider API key detected. Codex ACP can also authenticate without ChatGPT login.
</p>
)}
{error && (
<p className="text-xs text-destructive">
{error}
</p>
)}
{outputText && (
<pre className="rounded-md border border-border/60 bg-background px-3 py-2 text-[11px] leading-5 text-muted-foreground whitespace-pre-wrap max-h-40 overflow-auto">
{outputText}
</pre>
)}
</div>
);
};
// ---------------------------------------------------------------------------
// Add Agent Form
// ---------------------------------------------------------------------------
@@ -503,7 +691,17 @@ const SettingsAITab: React.FC<SettingsAITabProps> = ({
}) => {
const [editingProviderId, setEditingProviderId] = useState<string | null>(null);
const [showAddAgent, setShowAddAgent] = useState(false);
const [isScanning, setIsScanning] = useState(false);
const [codexIntegration, setCodexIntegration] = useState<CodexIntegrationStatus | null>(null);
const [codexLoginSession, setCodexLoginSession] = useState<CodexLoginSession | null>(null);
const [isCodexLoading, setIsCodexLoading] = useState(false);
const [codexError, setCodexError] = useState<string | null>(null);
const {
unconfiguredAgents,
isDiscovering,
rediscover,
enableAgent,
} = useAgentDiscovery(externalAgents, setExternalAgents);
// Add a new provider from preset
const handleAddProvider = useCallback(
@@ -553,16 +751,64 @@ const SettingsAITab: React.FC<SettingsAITabProps> = ({
.map((a) => ({ value: a.id, label: a.name })),
];
// Scan for external agents on PATH
const handleScanAgents = useCallback(async () => {
setIsScanning(true);
// Simulated scan - in production this would use the bridge to scan PATH
// For now just set a timeout to show the scanning state
setTimeout(() => {
setIsScanning(false);
}, 1500);
const hasOpenAiProviderKey = providers.some(
(provider) => provider.providerId === "openai" && provider.enabled && !!provider.apiKey,
);
const refreshCodexIntegration = useCallback(async () => {
const bridge = getBridge();
if (!bridge?.aiCodexGetIntegration) return;
setIsCodexLoading(true);
setCodexError(null);
try {
const integration = await bridge.aiCodexGetIntegration();
setCodexIntegration(integration);
} catch (err) {
setCodexError(normalizeCodexBridgeError(err));
} finally {
setIsCodexLoading(false);
}
}, []);
useEffect(() => {
void refreshCodexIntegration();
}, [refreshCodexIntegration]);
useEffect(() => {
if (!codexLoginSession || codexLoginSession.state !== "running") {
return;
}
const bridge = getBridge();
if (!bridge?.aiCodexGetLoginSession) {
return;
}
let cancelled = false;
const intervalId = window.setInterval(() => {
void bridge.aiCodexGetLoginSession?.(codexLoginSession.sessionId).then((result) => {
if (cancelled || !result?.ok || !result.session) return;
setCodexLoginSession(result.session);
if (result.session.state !== "running") {
if (result.session.state === "success") {
void refreshCodexIntegration();
}
}
}).catch((err) => {
if (!cancelled) {
setCodexError(normalizeCodexBridgeError(err));
}
});
}, 1000);
return () => {
cancelled = true;
window.clearInterval(intervalId);
};
}, [codexLoginSession, refreshCodexIntegration]);
// Add external agent
const handleAddAgent = useCallback(
(agent: ExternalAgentConfig) => {
@@ -572,6 +818,11 @@ const SettingsAITab: React.FC<SettingsAITabProps> = ({
[setExternalAgents],
);
const handleAddDiscoveredAgent = useCallback((agent: DiscoveredAgent) => {
const config = enableAgent(agent);
setExternalAgents((prev) => [...prev, config]);
}, [enableAgent, setExternalAgents]);
// Remove external agent
const handleRemoveAgent = useCallback(
(id: string) => {
@@ -593,6 +844,67 @@ const SettingsAITab: React.FC<SettingsAITabProps> = ({
[setExternalAgents],
);
const handleStartCodexLogin = useCallback(async () => {
const bridge = getBridge();
if (!bridge?.aiCodexStartLogin) return;
setCodexError(null);
setIsCodexLoading(true);
try {
const result = await bridge.aiCodexStartLogin();
if (!result.ok || !result.session) {
throw new Error(result.error || "Failed to start Codex login");
}
setCodexLoginSession(result.session);
} catch (err) {
setCodexError(normalizeCodexBridgeError(err));
} finally {
setIsCodexLoading(false);
}
}, []);
const handleCancelCodexLogin = useCallback(async () => {
const bridge = getBridge();
if (!bridge?.aiCodexCancelLogin || !codexLoginSession) return;
setCodexError(null);
try {
const result = await bridge.aiCodexCancelLogin(codexLoginSession.sessionId);
if (result.session) {
setCodexLoginSession(result.session);
}
} catch (err) {
setCodexError(normalizeCodexBridgeError(err));
}
}, [codexLoginSession]);
const handleOpenCodexLoginUrl = useCallback(() => {
const bridge = getBridge();
const url = codexLoginSession?.url;
if (!bridge?.openExternal || !url) return;
void bridge.openExternal(url);
}, [codexLoginSession]);
const handleCodexLogout = useCallback(async () => {
const bridge = getBridge();
if (!bridge?.aiCodexLogout) return;
setCodexError(null);
setIsCodexLoading(true);
try {
const result = await bridge.aiCodexLogout();
if (!result.ok) {
throw new Error(result.error || "Failed to log out from Codex");
}
setCodexLoginSession(null);
await refreshCodexIntegration();
} catch (err) {
setCodexError(normalizeCodexBridgeError(err));
} finally {
setIsCodexLoading(false);
}
}, [refreshCodexIntegration]);
return (
<TabsContent
value="ai"
@@ -686,6 +998,27 @@ const SettingsAITab: React.FC<SettingsAITabProps> = ({
</div>
</div>
{/* ── Codex Section ── */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<ProviderIconBadge providerId="openai" size="sm" />
<h3 className="text-base font-medium">Codex</h3>
</div>
<CodexConnectionCard
integration={codexIntegration}
loginSession={codexLoginSession}
isLoading={isCodexLoading}
hasOpenAiProviderKey={hasOpenAiProviderKey}
error={codexError}
onRefresh={() => void refreshCodexIntegration()}
onConnect={() => void handleStartCodexLogin()}
onCancel={() => void handleCancelCodexLogin()}
onOpenUrl={handleOpenCodexLoginUrl}
onLogout={() => void handleCodexLogout()}
/>
</div>
{/* ── External Agents Section ── */}
<div className="space-y-4">
<div className="flex items-center justify-between">
@@ -697,12 +1030,12 @@ const SettingsAITab: React.FC<SettingsAITabProps> = ({
<Button
variant="outline"
size="sm"
onClick={() => void handleScanAgents()}
disabled={isScanning}
onClick={() => void rediscover()}
disabled={isDiscovering}
className="gap-1.5"
>
<ScanSearch size={14} className={isScanning ? "animate-spin" : ""} />
{isScanning ? "Scanning..." : "Scan for Agents"}
<ScanSearch size={14} className={isDiscovering ? "animate-spin" : ""} />
{isDiscovering ? "Scanning..." : "Scan for Agents"}
</Button>
<Button
variant="outline"
@@ -723,7 +1056,22 @@ const SettingsAITab: React.FC<SettingsAITabProps> = ({
/>
)}
{externalAgents.length === 0 && !showAddAgent ? (
{unconfiguredAgents.length > 0 && (
<div className="space-y-2">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
Detected on this machine
</div>
{unconfiguredAgents.map((agent) => (
<DetectedAgentCard
key={`${agent.command}:${agent.path}`}
agent={agent}
onAdd={() => handleAddDiscoveredAgent(agent)}
/>
))}
</div>
)}
{externalAgents.length === 0 && unconfiguredAgents.length === 0 && !showAddAgent ? (
<div className="rounded-lg border border-dashed border-border/60 p-6 text-center">
<ScanSearch size={24} className="mx-auto text-muted-foreground mb-2" />
<p className="text-sm text-muted-foreground">

View File

@@ -20,7 +20,10 @@ module.exports = {
asarUnpack: [
'node_modules/node-pty/**/*',
'node_modules/ssh2/**/*',
'node_modules/cpu-features/**/*'
'node_modules/cpu-features/**/*',
'node_modules/@zed-industries/codex-acp/**/*',
'node_modules/@zed-industries/codex-acp-*/**/*',
'node_modules/@anthropic-ai/claude-agent-sdk/**/*'
],
mac: {
target: [

File diff suppressed because it is too large Load Diff

View File

@@ -859,11 +859,14 @@ async function createWindow(electronModule, options) {
// Register window control handlers
registerWindowHandlers(electronModule.ipcMain, nativeTheme);
// Register IPC handlers BEFORE loading any URL so the renderer never
// calls a handler that hasn't been registered yet.
onRegisterBridge?.(win);
if (isDev) {
try {
await win.loadURL(getDevRendererBaseUrl(devServerUrl));
win.webContents.openDevTools({ mode: "detach" });
onRegisterBridge?.(win);
return win;
} catch (e) {
console.warn("Dev server not reachable, falling back to bundled dist.", e);
@@ -872,8 +875,6 @@ async function createWindow(electronModule, options) {
// Production mode - load via custom protocol.
await win.loadURL("app://netcatty/index.html");
onRegisterBridge?.(win);
return win;
}

View File

@@ -6,3 +6,12 @@ delete env.ELECTRON_RUN_AS_NODE;
const child = spawn(electronPath, ["."], { stdio: "inherit", env });
child.on("exit", (code) => process.exit(code ?? 0));
// Forward SIGINT/SIGTERM to the Electron child process so Ctrl+C works
for (const sig of ["SIGINT", "SIGTERM"]) {
process.on(sig, () => {
if (!child.killed) {
child.kill(sig);
}
});
}

View File

@@ -986,6 +986,21 @@ const api = {
aiDiscoverAgents: async () => {
return ipcRenderer.invoke("netcatty:ai:agents:discover");
},
aiCodexGetIntegration: async () => {
return ipcRenderer.invoke("netcatty:ai:codex:get-integration");
},
aiCodexStartLogin: async () => {
return ipcRenderer.invoke("netcatty:ai:codex:start-login");
},
aiCodexGetLoginSession: async (sessionId) => {
return ipcRenderer.invoke("netcatty:ai:codex:get-login-session", { sessionId });
},
aiCodexCancelLogin: async (sessionId) => {
return ipcRenderer.invoke("netcatty:ai:codex:cancel-login", { sessionId });
},
aiCodexLogout: async () => {
return ipcRenderer.invoke("netcatty:ai:codex:logout");
},
aiSpawnAgent: async (agentId, command, args, env, options) => {
return ipcRenderer.invoke("netcatty:ai:agent:spawn", { agentId, command, args, env, closeStdin: options?.closeStdin });
},
@@ -998,6 +1013,34 @@ const api = {
aiKillAgent: async (agentId) => {
return ipcRenderer.invoke("netcatty:ai:agent:kill", { agentId });
},
// Claude Agent SDK streaming
aiClaudeStream: async (requestId, chatSessionId, prompt, model) => {
return ipcRenderer.invoke("netcatty:ai:claude:stream", { requestId, chatSessionId, prompt, model });
},
aiClaudeCancel: async (requestId) => {
return ipcRenderer.invoke("netcatty:ai:claude:cancel", { requestId });
},
onAiClaudeEvent: (requestId, cb) => {
const handler = (_event, payload) => {
if (payload.requestId === requestId) cb(payload.event);
};
ipcRenderer.on("netcatty:ai:claude:event", handler);
return () => ipcRenderer.removeListener("netcatty:ai:claude:event", handler);
},
onAiClaudeDone: (requestId, cb) => {
const handler = (_event, payload) => {
if (payload.requestId === requestId) cb();
};
ipcRenderer.on("netcatty:ai:claude:done", handler);
return () => ipcRenderer.removeListener("netcatty:ai:claude:done", handler);
},
onAiClaudeError: (requestId, cb) => {
const handler = (_event, payload) => {
if (payload.requestId === requestId) cb(payload.error);
};
ipcRenderer.on("netcatty:ai:claude:error", handler);
return () => ipcRenderer.removeListener("netcatty:ai:claude:error", handler);
},
// ACP streaming
aiAcpStream: async (requestId, chatSessionId, acpCommand, acpArgs, prompt, cwd, apiKey) => {
return ipcRenderer.invoke("netcatty:ai:acp:stream", { requestId, chatSessionId, acpCommand, acpArgs, prompt, cwd, apiKey });

91
global.d.ts vendored
View File

@@ -612,6 +612,97 @@ declare global {
credentialsEncrypt?(plaintext: string): Promise<string>;
credentialsDecrypt?(value: string): Promise<string>;
// AI / external agents
aiChatStream?(requestId: string, url: string, headers?: Record<string, string>, body?: string): Promise<{ ok: boolean; error?: string }>;
aiChatCancel?(requestId: string): Promise<boolean>;
aiFetch?(url: string, method?: string, headers?: Record<string, string>, body?: string): Promise<{ ok: boolean; status: number; data: string; error?: string }>;
aiExec?(sessionId: string, command: string): Promise<{ ok: boolean; stdout?: string; stderr?: string; exitCode?: number | null; error?: string }>;
aiTerminalWrite?(sessionId: string, data: string): Promise<{ ok: boolean; error?: string }>;
aiDiscoverAgents?(): Promise<Array<{
command: string;
name: string;
icon: string;
description: string;
args: string[];
path: string;
version: string;
available: boolean;
acpCommand?: string;
acpArgs?: string[];
sdkType?: 'acp' | 'claude-agent-sdk';
}>>;
aiCodexGetIntegration?(): Promise<{
state: 'connected_chatgpt' | 'connected_api_key' | 'not_logged_in' | 'unknown';
isConnected: boolean;
rawOutput: string;
exitCode: number | null;
}>;
aiCodexStartLogin?(): Promise<{
ok: boolean;
session?: {
sessionId: string;
state: 'running' | 'success' | 'error' | 'cancelled';
url: string | null;
output: string;
error: string | null;
exitCode: number | null;
};
error?: string;
}>;
aiCodexGetLoginSession?(sessionId: string): Promise<{
ok: boolean;
session?: {
sessionId: string;
state: 'running' | 'success' | 'error' | 'cancelled';
url: string | null;
output: string;
error: string | null;
exitCode: number | null;
};
error?: string;
}>;
aiCodexCancelLogin?(sessionId: string): Promise<{
ok: boolean;
found?: boolean;
session?: {
sessionId: string;
state: 'running' | 'success' | 'error' | 'cancelled';
url: string | null;
output: string;
error: string | null;
exitCode: number | null;
};
error?: string;
}>;
aiCodexLogout?(): Promise<{
ok: boolean;
state?: 'connected_chatgpt' | 'connected_api_key' | 'not_logged_in' | 'unknown';
isConnected?: boolean;
rawOutput?: string;
logoutOutput?: string;
error?: string;
}>;
aiSpawnAgent?(agentId: string, command: string, args?: string[], env?: Record<string, string>, options?: { closeStdin?: boolean }): Promise<{ ok: boolean; pid?: number; error?: string }>;
aiWriteToAgent?(agentId: string, data: string): Promise<{ ok: boolean; error?: string }>;
aiCloseAgentStdin?(agentId: string): Promise<{ ok: boolean; error?: string }>;
aiKillAgent?(agentId: string): Promise<{ ok: boolean; error?: string }>;
aiClaudeStream?(requestId: string, chatSessionId: string, prompt: string, model?: string): Promise<{ ok: boolean; error?: string }>;
aiClaudeCancel?(requestId: string): Promise<{ ok: boolean; error?: string }>;
onAiClaudeEvent?(requestId: string, cb: (event: Record<string, unknown>) => void): () => void;
onAiClaudeDone?(requestId: string, cb: () => void): () => void;
onAiClaudeError?(requestId: string, cb: (error: string) => void): () => void;
aiAcpStream?(requestId: string, chatSessionId: string, acpCommand: string, acpArgs: string[], prompt: string, cwd?: string, apiKey?: string): Promise<{ ok: boolean; error?: string }>;
aiAcpCancel?(requestId: string): Promise<{ ok: boolean; error?: string }>;
aiAcpCleanup?(chatSessionId: string): Promise<{ ok: boolean }>;
onAiAcpEvent?(requestId: string, cb: (event: Record<string, unknown>) => void): () => void;
onAiAcpDone?(requestId: string, cb: () => void): () => void;
onAiAcpError?(requestId: string, cb: (error: string) => void): () => void;
onAiStreamData?(requestId: string, cb: (data: string) => void): () => void;
onAiStreamEnd?(requestId: string, cb: () => void): () => void;
onAiAgentStdout?(agentId: string, cb: (data: string) => void): () => void;
onAiAgentStderr?(agentId: string, cb: (data: string) => void): () => void;
onAiAgentExit?(agentId: string, cb: (code: number | null) => void): () => void;
// Auto-update
checkForUpdate?(): Promise<{
available: boolean;

View File

@@ -307,3 +307,71 @@ body {
.workspace-pane:focus-within::after {
opacity: 1;
}
/* ── Streamdown code block overrides ── */
[data-streamdown="code-block"] {
position: relative !important;
border-radius: 10px !important;
background: hsl(var(--muted) / 0.5) !important;
overflow: hidden !important;
margin: 6px 0 !important;
padding: 0 !important;
border: none !important;
gap: 0 !important;
}
[data-streamdown="code-block-header"] {
height: auto !important;
padding: 4px 12px 0 !important;
font-size: 11px !important;
}
[data-streamdown="code-block-header"] span {
margin-left: 0 !important;
}
[data-streamdown="code-block-actions"] {
position: absolute !important;
top: 4px !important;
right: 4px !important;
border: none !important;
background: none !important;
backdrop-filter: none !important;
padding: 0 !important;
gap: 2px !important;
opacity: 0;
transition: opacity 0.15s;
}
[data-streamdown="code-block"]:hover [data-streamdown="code-block-actions"] {
opacity: 1;
}
[data-streamdown="code-block-actions"] button {
padding: 4px !important;
border-radius: 4px;
}
[data-streamdown="code-block-body"] {
border: none !important;
border-radius: 0 !important;
background: transparent !important;
padding: 0 !important;
margin: 0 !important;
overflow-x: auto !important;
font-size: 0 !important; /* collapse whitespace text nodes */
}
[data-streamdown="code-block-body"] pre {
font-size: 12px !important; /* restore in pre */
}
[data-streamdown="code-block"] pre {
margin: 0 !important;
background: transparent !important;
border: none !important;
border-radius: 0 !important;
padding: 0 12px 10px !important;
font-size: 12px !important;
line-height: 1.5 !important;
}

View File

@@ -127,7 +127,7 @@ function cleanup(fns: (() => void)[]) {
function handleStreamEvent(event: StreamEvent, callbacks: AcpAgentCallbacks) {
switch (event.type) {
case 'text-delta': {
const text = (event.delta as string) || '';
const text = (event.textDelta as string) || (event.delta as string) || '';
if (text) callbacks.onTextDelta(text);
break;
}

View File

@@ -92,7 +92,6 @@ function parseItemEvent(
const command = item.command as string || '';
const output = item.aggregated_output as string || '';
const exitCode = item.exit_code as number | null;
const status = item.status as string;
if (eventType === 'item.started' && command) {
segments.push({ type: 'command', content: command });

View File

@@ -0,0 +1,151 @@
/**
* Claude Agent SDK Adapter
*
* Bridges Claude Code via the @anthropic-ai/claude-agent-sdk through IPC.
* The main process runs `query()` and forwards SDK events to the renderer.
*/
import type { ExternalAgentConfig } from './types';
export interface ClaudeAgentCallbacks {
onTextDelta: (text: string) => void;
onThinkingDelta: (text: string) => void;
onThinkingDone: () => void;
onToolCall: (toolName: string, args: Record<string, unknown>) => void;
onToolResult: (toolCallId: string, result: string) => void;
onError: (error: string) => void;
onDone: () => void;
}
interface ClaudeBridge {
aiClaudeStream(
requestId: string,
chatSessionId: string,
prompt: string,
model?: string,
): Promise<{ ok: boolean; error?: string }>;
aiClaudeCancel(requestId: string): Promise<{ ok: boolean }>;
onAiClaudeEvent(requestId: string, cb: (event: ClaudeSDKEvent) => void): () => void;
onAiClaudeDone(requestId: string, cb: () => void): () => void;
onAiClaudeError(requestId: string, cb: (error: string) => void): () => void;
}
interface ClaudeSDKEvent {
type: string;
[key: string]: unknown;
}
/**
* Run a Claude Agent SDK turn.
* Sends the prompt to the main process which runs query() with the Claude Agent SDK.
* SDK events are forwarded back via IPC.
*/
export async function runClaudeAgentTurn(
bridge: Record<string, (...args: unknown[]) => unknown>,
requestId: string,
chatSessionId: string,
_config: ExternalAgentConfig,
prompt: string,
callbacks: ClaudeAgentCallbacks,
signal?: AbortSignal,
model?: string,
): Promise<void> {
const claudeBridge = bridge as unknown as ClaudeBridge;
const cleanupFns: (() => void)[] = [];
// Set up event listeners before starting stream
const unsubEvent = claudeBridge.onAiClaudeEvent(requestId, (event: ClaudeSDKEvent) => {
handleClaudeEvent(event, callbacks);
});
cleanupFns.push(unsubEvent);
const donePromise = new Promise<void>((resolve) => {
const unsubDone = claudeBridge.onAiClaudeDone(requestId, () => {
callbacks.onDone();
resolve();
});
cleanupFns.push(unsubDone);
const unsubError = claudeBridge.onAiClaudeError(requestId, (error: string) => {
callbacks.onError(error);
resolve();
});
cleanupFns.push(unsubError);
});
// Handle abort
if (signal) {
if (signal.aborted) {
cleanup(cleanupFns);
return;
}
const onAbort = () => {
claudeBridge.aiClaudeCancel(requestId).catch(() => {});
};
signal.addEventListener('abort', onAbort, { once: true });
cleanupFns.push(() => signal.removeEventListener('abort', onAbort));
}
// Start the Claude stream in the main process
claudeBridge.aiClaudeStream(
requestId,
chatSessionId,
prompt,
model,
).catch((err: Error) => {
callbacks.onError(err.message);
});
// Wait for done or error
await donePromise;
cleanup(cleanupFns);
}
function cleanup(fns: (() => void)[]) {
for (const fn of fns) {
try { fn(); } catch { /* */ }
}
}
/**
* Handle a single event from the Claude Agent SDK.
*/
function handleClaudeEvent(event: ClaudeSDKEvent, callbacks: ClaudeAgentCallbacks) {
switch (event.type) {
case 'text-delta': {
const text = (event.delta as string) || '';
if (text) callbacks.onTextDelta(text);
break;
}
case 'thinking-delta': {
const text = (event.delta as string) || '';
if (text) callbacks.onThinkingDelta(text);
break;
}
case 'thinking-done': {
callbacks.onThinkingDone();
break;
}
case 'tool-call': {
const toolName = (event.toolName as string) || 'unknown';
const input = (event.input as Record<string, unknown>) || {};
callbacks.onToolCall(toolName, input);
break;
}
case 'tool-result': {
const toolCallId = (event.toolCallId as string) || '';
const output = event.output ?? event.result;
const result = typeof output === 'string'
? output
: JSON.stringify(output);
callbacks.onToolResult(toolCallId, result);
break;
}
case 'error': {
callbacks.onError(String(event.error || 'Unknown error'));
break;
}
// stream_event, result, tool_progress, tool_use_summary — ignore silently
}
}

View File

@@ -124,6 +124,8 @@ export interface ExternalAgentConfig {
/** ACP command (e.g. 'codex-acp', 'claude-code-acp', 'gemini --experimental-acp') */
acpCommand?: string;
acpArgs?: string[];
/** Which SDK to use for streaming ('acp' or 'claude-agent-sdk') */
sdkType?: 'acp' | 'claude-agent-sdk';
}
// Discovered agent from system PATH
@@ -139,6 +141,8 @@ export interface DiscoveredAgent {
/** ACP command if agent supports ACP protocol */
acpCommand?: string;
acpArgs?: string[];
/** Which SDK to use for streaming ('acp' or 'claude-agent-sdk') */
sdkType?: 'acp' | 'claude-agent-sdk';
}
// AI Settings (stored in localStorage)

334
package-lock.json generated
View File

@@ -13,11 +13,12 @@
"@ai-sdk/anthropic": "^3.0.58",
"@ai-sdk/google": "^3.0.43",
"@ai-sdk/openai": "^3.0.41",
"@anthropic-ai/claude-agent-sdk": "0.2.76",
"@aws-sdk/client-s3": "^3.956.0",
"@fontsource/jetbrains-mono": "^5.2.8",
"@fontsource/space-grotesk": "^5.2.10",
"@google/genai": "1.33.0",
"@mcpc-tech/acp-ai-provider": "^0.2.8",
"@mcpc-tech/acp-ai-provider": "0.2.8",
"@monaco-editor/react": "^4.7.0",
"@radix-ui/react-collapsible": "1.1.12",
"@radix-ui/react-context-menu": "2.2.16",
@@ -37,8 +38,8 @@
"@xterm/addon-web-links": "^0.11.0",
"@xterm/addon-webgl": "^0.18.0",
"@xterm/xterm": "^5.5.0",
"@zed-industries/codex-acp": "^0.10.0",
"@zed-industries/codex-acp-darwin-arm64": "^0.10.0",
"@zed-industries/codex-acp": "0.10.0",
"@zed-industries/codex-acp-darwin-arm64": "0.10.0",
"ai": "^6.0.116",
"clsx": "2.1.1",
"electron-updater": "^6.8.3",
@@ -196,6 +197,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",
@@ -2659,6 +2683,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",

View File

@@ -35,7 +35,8 @@
"@fontsource/jetbrains-mono": "^5.2.8",
"@fontsource/space-grotesk": "^5.2.10",
"@google/genai": "1.33.0",
"@mcpc-tech/acp-ai-provider": "^0.2.8",
"@anthropic-ai/claude-agent-sdk": "0.2.76",
"@mcpc-tech/acp-ai-provider": "0.2.8",
"@monaco-editor/react": "^4.7.0",
"@radix-ui/react-collapsible": "1.1.12",
"@radix-ui/react-context-menu": "2.2.16",
@@ -55,8 +56,8 @@
"@xterm/addon-web-links": "^0.11.0",
"@xterm/addon-webgl": "^0.18.0",
"@xterm/xterm": "^5.5.0",
"@zed-industries/codex-acp": "^0.10.0",
"@zed-industries/codex-acp-darwin-arm64": "^0.10.0",
"@zed-industries/codex-acp": "0.10.0",
"@zed-industries/codex-acp-darwin-arm64": "0.10.0",
"ai": "^6.0.116",
"clsx": "2.1.1",
"electron-updater": "^6.8.3",