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:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
[],
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
91
global.d.ts
vendored
@@ -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;
|
||||
|
||||
68
index.css
68
index.css
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
151
infrastructure/ai/claudeAgentAdapter.ts
Normal file
151
infrastructure/ai/claudeAgentAdapter.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
334
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user