@adhdev/daemon-core 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2662 -0
- package/dist/index.js +11341 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
- package/providers/_builtin/.github/workflows/generate-registry.yml +57 -0
- package/providers/_builtin/COMPATIBILITY.md +217 -0
- package/providers/_builtin/CONTRIBUTING.md +200 -0
- package/providers/_builtin/README.md +119 -0
- package/providers/_builtin/_helpers/index.js +188 -0
- package/providers/_builtin/acp/agentpool/provider.json +54 -0
- package/providers/_builtin/acp/amp/provider.json +52 -0
- package/providers/_builtin/acp/auggie/provider.json +57 -0
- package/providers/_builtin/acp/autodev/provider.json +54 -0
- package/providers/_builtin/acp/autohand/provider.json +52 -0
- package/providers/_builtin/acp/blackbox-ai/provider.json +54 -0
- package/providers/_builtin/acp/claude-agent/provider.json +57 -0
- package/providers/_builtin/acp/cline-acp/provider.json +54 -0
- package/providers/_builtin/acp/codebuddy/provider.json +54 -0
- package/providers/_builtin/acp/codex-cli/provider.json +57 -0
- package/providers/_builtin/acp/corust-agent/provider.json +52 -0
- package/providers/_builtin/acp/crow-cli/provider.json +54 -0
- package/providers/_builtin/acp/cursor-acp/provider.json +54 -0
- package/providers/_builtin/acp/deepagents/provider.json +52 -0
- package/providers/_builtin/acp/dimcode/provider.json +54 -0
- package/providers/_builtin/acp/docker-cagent/provider.json +57 -0
- package/providers/_builtin/acp/factory-droid/provider.json +60 -0
- package/providers/_builtin/acp/fast-agent/provider.json +52 -0
- package/providers/_builtin/acp/gemini-cli/provider.json +114 -0
- package/providers/_builtin/acp/github-copilot/provider.json +54 -0
- package/providers/_builtin/acp/goose/provider.json +57 -0
- package/providers/_builtin/acp/junie/provider.json +52 -0
- package/providers/_builtin/acp/kilo/provider.json +54 -0
- package/providers/_builtin/acp/kimi-cli/provider.json +57 -0
- package/providers/_builtin/acp/minion-code/provider.json +52 -0
- package/providers/_builtin/acp/mistral-vibe/provider.json +57 -0
- package/providers/_builtin/acp/nova/provider.json +54 -0
- package/providers/_builtin/acp/openclaw/provider.json +54 -0
- package/providers/_builtin/acp/opencode/provider.json +52 -0
- package/providers/_builtin/acp/openhands/provider.json +54 -0
- package/providers/_builtin/acp/pi-acp/provider.json +52 -0
- package/providers/_builtin/acp/qoder/provider.json +54 -0
- package/providers/_builtin/acp/qwen-code/provider.json +60 -0
- package/providers/_builtin/acp/stakpak/provider.json +54 -0
- package/providers/_builtin/acp/vtcode/provider.json +54 -0
- package/providers/_builtin/cli/claude-cli/provider.json +100 -0
- package/providers/_builtin/cli/codex-cli/provider.json +89 -0
- package/providers/_builtin/cli/gemini-cli/provider.json +93 -0
- package/providers/_builtin/docs/CDP_SELECTOR_GUIDE.md +370 -0
- package/providers/_builtin/docs/PROVIDER_GUIDE.md +916 -0
- package/providers/_builtin/extension/cline/provider.json +35 -0
- package/providers/_builtin/extension/cline/scripts/focus_editor.js +48 -0
- package/providers/_builtin/extension/cline/scripts/list_chats.js +100 -0
- package/providers/_builtin/extension/cline/scripts/list_models.js +43 -0
- package/providers/_builtin/extension/cline/scripts/list_modes.js +35 -0
- package/providers/_builtin/extension/cline/scripts/new_session.js +85 -0
- package/providers/_builtin/extension/cline/scripts/open_panel.js +25 -0
- package/providers/_builtin/extension/cline/scripts/read_chat.js +257 -0
- package/providers/_builtin/extension/cline/scripts/resolve_action.js +83 -0
- package/providers/_builtin/extension/cline/scripts/send_message.js +95 -0
- package/providers/_builtin/extension/cline/scripts/set_mode.js +36 -0
- package/providers/_builtin/extension/cline/scripts/set_model.js +36 -0
- package/providers/_builtin/extension/cline/scripts/switch_session.js +206 -0
- package/providers/_builtin/extension/cline/scripts.js +73 -0
- package/providers/_builtin/extension/roo-code/provider.json +35 -0
- package/providers/_builtin/extension/roo-code/scripts.js +659 -0
- package/providers/_builtin/ide/antigravity/provider.json +68 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/focus_editor.js +20 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/list_chats.js +137 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/list_models.js +38 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/list_modes.js +48 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/new_session.js +75 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/read_chat.js +262 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/resolve_action.js +68 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/scripts.js +57 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/send_message.js +56 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/set_mode.js +34 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/set_model.js +47 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/switch_session.js +114 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/focus_editor.js +20 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/list_chats.js +137 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/list_models.js +61 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/list_modes.js +72 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/new_session.js +75 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/read_chat.js +262 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/resolve_action.js +68 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/scripts.js +67 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/send_message.js +56 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/set_mode.js +67 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/set_model.js +72 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/switch_session.js +114 -0
- package/providers/_builtin/ide/cursor/provider.json +70 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/dismiss_notification.js +30 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/focus_editor.js +13 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/list_models.js +78 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/list_modes.js +40 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/list_notifications.js +23 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/list_sessions.js +42 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/new_session.js +20 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/open_panel.js +23 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/read_chat.js +75 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/resolve_action.js +19 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/scripts.js +78 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/send_message.js +23 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/set_mode.js +38 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/set_model.js +81 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/switch_session.js +28 -0
- package/providers/_builtin/ide/kiro/provider.json +67 -0
- package/providers/_builtin/ide/kiro/scripts/focus_editor.js +20 -0
- package/providers/_builtin/ide/kiro/scripts/open_panel.js +47 -0
- package/providers/_builtin/ide/kiro/scripts/resolve_action.js +54 -0
- package/providers/_builtin/ide/kiro/scripts/send_message.js +29 -0
- package/providers/_builtin/ide/kiro/scripts/webview_list_models.js +39 -0
- package/providers/_builtin/ide/kiro/scripts/webview_list_modes.js +39 -0
- package/providers/_builtin/ide/kiro/scripts/webview_list_sessions.js +21 -0
- package/providers/_builtin/ide/kiro/scripts/webview_new_session.js +34 -0
- package/providers/_builtin/ide/kiro/scripts/webview_read_chat.js +68 -0
- package/providers/_builtin/ide/kiro/scripts/webview_send_message.js +72 -0
- package/providers/_builtin/ide/kiro/scripts/webview_set_mode.js +15 -0
- package/providers/_builtin/ide/kiro/scripts/webview_set_model.js +15 -0
- package/providers/_builtin/ide/kiro/scripts/webview_switch_session.js +26 -0
- package/providers/_builtin/ide/kiro/scripts.js +62 -0
- package/providers/_builtin/ide/pearai/provider.json +67 -0
- package/providers/_builtin/ide/pearai/scripts/focus_editor.js +20 -0
- package/providers/_builtin/ide/pearai/scripts/list_sessions.js +38 -0
- package/providers/_builtin/ide/pearai/scripts/new_session.js +55 -0
- package/providers/_builtin/ide/pearai/scripts/open_panel.js +46 -0
- package/providers/_builtin/ide/pearai/scripts/resolve_action.js +54 -0
- package/providers/_builtin/ide/pearai/scripts/send_message.js +29 -0
- package/providers/_builtin/ide/pearai/scripts/webview_list_models.js +43 -0
- package/providers/_builtin/ide/pearai/scripts/webview_list_modes.js +35 -0
- package/providers/_builtin/ide/pearai/scripts/webview_list_sessions.js +62 -0
- package/providers/_builtin/ide/pearai/scripts/webview_new_session.js +49 -0
- package/providers/_builtin/ide/pearai/scripts/webview_read_chat.js +92 -0
- package/providers/_builtin/ide/pearai/scripts/webview_resolve_action.js +59 -0
- package/providers/_builtin/ide/pearai/scripts/webview_send_message.js +72 -0
- package/providers/_builtin/ide/pearai/scripts/webview_set_mode.js +36 -0
- package/providers/_builtin/ide/pearai/scripts/webview_set_model.js +36 -0
- package/providers/_builtin/ide/pearai/scripts/webview_switch_session.js +34 -0
- package/providers/_builtin/ide/pearai/scripts.js +74 -0
- package/providers/_builtin/ide/trae/provider.json +66 -0
- package/providers/_builtin/ide/trae/scripts/focus_editor.js +20 -0
- package/providers/_builtin/ide/trae/scripts/list_chats.js +24 -0
- package/providers/_builtin/ide/trae/scripts/list_models.js +39 -0
- package/providers/_builtin/ide/trae/scripts/list_modes.js +39 -0
- package/providers/_builtin/ide/trae/scripts/new_session.js +30 -0
- package/providers/_builtin/ide/trae/scripts/open_panel.js +44 -0
- package/providers/_builtin/ide/trae/scripts/read_chat.js +113 -0
- package/providers/_builtin/ide/trae/scripts/resolve_action.js +54 -0
- package/providers/_builtin/ide/trae/scripts/send_message.js +69 -0
- package/providers/_builtin/ide/trae/scripts/set_mode.js +15 -0
- package/providers/_builtin/ide/trae/scripts/set_model.js +15 -0
- package/providers/_builtin/ide/trae/scripts/switch_session.js +23 -0
- package/providers/_builtin/ide/trae/scripts.js +57 -0
- package/providers/_builtin/ide/vscode/provider.json +64 -0
- package/providers/_builtin/ide/vscode-insiders/provider.json +62 -0
- package/providers/_builtin/ide/vscodium/provider.json +63 -0
- package/providers/_builtin/ide/windsurf/provider.json +53 -0
- package/providers/_builtin/ide/windsurf/scripts/focus_editor.js +30 -0
- package/providers/_builtin/ide/windsurf/scripts/list_chats.js +117 -0
- package/providers/_builtin/ide/windsurf/scripts/list_models.js +39 -0
- package/providers/_builtin/ide/windsurf/scripts/list_modes.js +39 -0
- package/providers/_builtin/ide/windsurf/scripts/new_session.js +69 -0
- package/providers/_builtin/ide/windsurf/scripts/open_panel.js +58 -0
- package/providers/_builtin/ide/windsurf/scripts/read_chat.js +297 -0
- package/providers/_builtin/ide/windsurf/scripts/resolve_action.js +68 -0
- package/providers/_builtin/ide/windsurf/scripts/send_message.js +87 -0
- package/providers/_builtin/ide/windsurf/scripts/set_mode.js +15 -0
- package/providers/_builtin/ide/windsurf/scripts/set_model.js +15 -0
- package/providers/_builtin/ide/windsurf/scripts/switch_session.js +58 -0
- package/providers/_builtin/ide/windsurf/scripts.js +57 -0
- package/providers/_builtin/registry.json +266 -0
- package/providers/_builtin/validate.js +156 -0
- package/src/agent-stream/index.ts +6 -0
- package/src/agent-stream/manager.ts +286 -0
- package/src/agent-stream/poller.ts +154 -0
- package/src/agent-stream/provider-adapter.ts +138 -0
- package/src/agent-stream/types.ts +61 -0
- package/src/boot/daemon-lifecycle.ts +252 -0
- package/src/cdp/devtools.ts +335 -0
- package/src/cdp/initializer.ts +191 -0
- package/src/cdp/manager.ts +897 -0
- package/src/cdp/scanner.ts +185 -0
- package/src/cdp/setup.ts +150 -0
- package/src/cli-adapter-types.ts +25 -0
- package/src/cli-adapters/provider-cli-adapter.ts +448 -0
- package/src/commands/cdp-commands.ts +208 -0
- package/src/commands/chat-commands.ts +675 -0
- package/src/commands/cli-manager.ts +353 -0
- package/src/commands/handler.ts +328 -0
- package/src/commands/router.ts +258 -0
- package/src/commands/stream-commands.ts +325 -0
- package/src/config/chat-history.ts +211 -0
- package/src/config/config.ts +219 -0
- package/src/daemon/dev-server.ts +2378 -0
- package/src/daemon/scaffold-template.ts +394 -0
- package/src/daemon-core.ts +50 -0
- package/src/detection/cli-detector.ts +89 -0
- package/src/detection/ide-detector.ts +157 -0
- package/src/index.ts +103 -0
- package/src/installer.ts +263 -0
- package/src/ipc-protocol.ts +133 -0
- package/src/launch.ts +433 -0
- package/src/logging/command-log.ts +180 -0
- package/src/logging/logger.ts +316 -0
- package/src/providers/acp-provider-instance.ts +1140 -0
- package/src/providers/cli-provider-instance.ts +207 -0
- package/src/providers/contracts.ts +524 -0
- package/src/providers/extension-provider-instance.ts +156 -0
- package/src/providers/ide-provider-instance.ts +377 -0
- package/src/providers/index.ts +18 -0
- package/src/providers/provider-instance-manager.ts +182 -0
- package/src/providers/provider-instance.ts +112 -0
- package/src/providers/provider-loader.ts +1031 -0
- package/src/providers/status-monitor.ts +125 -0
- package/src/providers/version-archive.ts +266 -0
- package/src/status/reporter.ts +294 -0
- package/src/types.ts +206 -0
|
@@ -0,0 +1,1140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AcpProviderInstance — ACP (Agent Client Protocol) Provider runtime instance
|
|
3
|
+
*
|
|
4
|
+
* Spawns ACP agent process and communicates via the official ACP SDK.
|
|
5
|
+
* Uses ClientSideConnection + ndJsonStream for structured protocol communication.
|
|
6
|
+
*
|
|
7
|
+
* ACP spec: https://agentclientprotocol.com
|
|
8
|
+
* ACP SDK: @agentclientprotocol/sdk@0.16.1
|
|
9
|
+
*
|
|
10
|
+
* lifecycle:
|
|
11
|
+
* 1. init() → Spawn agent process + ACP initialize handshake
|
|
12
|
+
* 2. onTick() → no-op (ACP event based)
|
|
13
|
+
* 3. getState() → ProviderState return (dashboard for display)
|
|
14
|
+
* 4. onEvent('send_message') → session/prompt transmit
|
|
15
|
+
* 5. dispose() → kill process
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { Readable, Writable } from 'stream';
|
|
19
|
+
import { spawn, type ChildProcess } from 'child_process';
|
|
20
|
+
import {
|
|
21
|
+
ClientSideConnection,
|
|
22
|
+
ndJsonStream,
|
|
23
|
+
RequestError,
|
|
24
|
+
PROTOCOL_VERSION,
|
|
25
|
+
type Client,
|
|
26
|
+
type Agent,
|
|
27
|
+
type SessionNotification,
|
|
28
|
+
type RequestPermissionRequest,
|
|
29
|
+
type RequestPermissionResponse,
|
|
30
|
+
type WriteTextFileRequest,
|
|
31
|
+
type WriteTextFileResponse,
|
|
32
|
+
type ReadTextFileRequest,
|
|
33
|
+
type ReadTextFileResponse,
|
|
34
|
+
type CreateTerminalRequest,
|
|
35
|
+
type CreateTerminalResponse,
|
|
36
|
+
type TerminalOutputRequest,
|
|
37
|
+
type TerminalOutputResponse,
|
|
38
|
+
type ReleaseTerminalRequest,
|
|
39
|
+
type ReleaseTerminalResponse,
|
|
40
|
+
type WaitForTerminalExitRequest,
|
|
41
|
+
type WaitForTerminalExitResponse,
|
|
42
|
+
type KillTerminalRequest,
|
|
43
|
+
type KillTerminalResponse,
|
|
44
|
+
type SessionUpdate,
|
|
45
|
+
type ToolCallStatus,
|
|
46
|
+
} from '@agentclientprotocol/sdk';
|
|
47
|
+
import type { ProviderModule, ContentBlock, ToolCallInfo, ToolCallContent as TCC, ToolKind, ToolCallStatus as TCS } from './contracts.js';
|
|
48
|
+
import { normalizeContent, flattenContent } from './contracts.js';
|
|
49
|
+
import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext } from './provider-instance.js';
|
|
50
|
+
import { StatusMonitor } from './status-monitor.js';
|
|
51
|
+
import { LOG } from '../logging/logger.js';
|
|
52
|
+
|
|
53
|
+
// ─── Internal Display Types (dashboard용) ────────────────────────────
|
|
54
|
+
|
|
55
|
+
interface AcpMessage {
|
|
56
|
+
role: 'user' | 'assistant' | 'system';
|
|
57
|
+
/** Rich content blocks (ACP standard) or plain text (legacy) */
|
|
58
|
+
content: string | ContentBlock[];
|
|
59
|
+
timestamp?: number;
|
|
60
|
+
/** Tool calls associated with this message */
|
|
61
|
+
toolCalls?: ToolCallInfo[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface AcpToolCall {
|
|
65
|
+
id: string;
|
|
66
|
+
name: string;
|
|
67
|
+
status: 'running' | 'completed' | 'failed';
|
|
68
|
+
input?: string;
|
|
69
|
+
output?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface AcpConfigOption {
|
|
73
|
+
category: 'model' | 'mode' | 'thought_level' | 'other';
|
|
74
|
+
configId: string;
|
|
75
|
+
currentValue?: string;
|
|
76
|
+
options: { value: string; name: string; description?: string; group?: string }[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface AcpMode {
|
|
80
|
+
id: string;
|
|
81
|
+
name: string;
|
|
82
|
+
description?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── AcpProviderInstance ───────────────────────────
|
|
86
|
+
|
|
87
|
+
export class AcpProviderInstance implements ProviderInstance {
|
|
88
|
+
readonly type: string;
|
|
89
|
+
readonly category = 'acp' as const;
|
|
90
|
+
private readonly log = LOG.forComponent('ACP');
|
|
91
|
+
|
|
92
|
+
private provider: ProviderModule;
|
|
93
|
+
private context: InstanceContext | null = null;
|
|
94
|
+
private settings: Record<string, any> = {};
|
|
95
|
+
private events: ProviderEvent[] = [];
|
|
96
|
+
private monitor: StatusMonitor;
|
|
97
|
+
|
|
98
|
+
// Process
|
|
99
|
+
private process: ChildProcess | null = null;
|
|
100
|
+
private connection: ClientSideConnection | null = null;
|
|
101
|
+
|
|
102
|
+
// State
|
|
103
|
+
private sessionId: string | null = null;
|
|
104
|
+
private messages: AcpMessage[] = [];
|
|
105
|
+
private currentStatus: ProviderState['status'] = 'starting';
|
|
106
|
+
private lastStatus: string = 'starting';
|
|
107
|
+
private generatingStartedAt = 0;
|
|
108
|
+
private agentCapabilities: Record<string, any> = {};
|
|
109
|
+
private currentModel: string | undefined;
|
|
110
|
+
private currentMode: string | undefined;
|
|
111
|
+
private activeToolCalls: AcpToolCall[] = [];
|
|
112
|
+
private stopReason: string | null = null;
|
|
113
|
+
private partialContent = '';
|
|
114
|
+
/** Rich content blocks accumulated during streaming */
|
|
115
|
+
private partialBlocks: ContentBlock[] = [];
|
|
116
|
+
/** Tool calls collected during current turn */
|
|
117
|
+
private turnToolCalls: ToolCallInfo[] = [];
|
|
118
|
+
|
|
119
|
+
// Error tracking
|
|
120
|
+
private errorMessage: string | null = null;
|
|
121
|
+
private errorReason: 'not_installed' | 'auth_failed' | 'spawn_error' | 'init_failed' | 'crash' | null = null;
|
|
122
|
+
private stderrBuffer: string[] = [];
|
|
123
|
+
private spawnedAt = 0;
|
|
124
|
+
|
|
125
|
+
// ACP ConfigOptions & Modes (from session/new response or static fallback)
|
|
126
|
+
private configOptions: AcpConfigOption[] = [];
|
|
127
|
+
private availableModes: AcpMode[] = [];
|
|
128
|
+
/** Static config mode — agent doesn't support config/* methods */
|
|
129
|
+
private useStaticConfig = false;
|
|
130
|
+
/** Current config selections (for spawnArgBuilder) */
|
|
131
|
+
private selectedConfig: Record<string, string> = {};
|
|
132
|
+
|
|
133
|
+
// Config
|
|
134
|
+
private workingDir: string;
|
|
135
|
+
private instanceId: string;
|
|
136
|
+
|
|
137
|
+
constructor(
|
|
138
|
+
provider: ProviderModule,
|
|
139
|
+
workingDir: string,
|
|
140
|
+
private cliArgs: string[] = [],
|
|
141
|
+
) {
|
|
142
|
+
this.type = provider.type;
|
|
143
|
+
this.provider = provider;
|
|
144
|
+
this.workingDir = workingDir;
|
|
145
|
+
this.instanceId = crypto.randomUUID();
|
|
146
|
+
|
|
147
|
+
this.monitor = new StatusMonitor();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ─── Lifecycle ─────────────────────────────────
|
|
151
|
+
|
|
152
|
+
async init(context: InstanceContext): Promise<void> {
|
|
153
|
+
this.context = context;
|
|
154
|
+
this.settings = context.settings || {};
|
|
155
|
+
this.monitor.updateConfig({
|
|
156
|
+
approvalAlert: this.settings.approvalAlert !== false,
|
|
157
|
+
longGeneratingAlert: this.settings.longGeneratingAlert !== false,
|
|
158
|
+
longGeneratingThresholdSec: this.settings.longGeneratingThresholdSec || 180,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
await this.spawnAgent();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async onTick(): Promise<void> {
|
|
165
|
+
// ACP event based — tick unnecessary
|
|
166
|
+
// Run process health check only
|
|
167
|
+
if (this.process && this.process.exitCode !== null) {
|
|
168
|
+
this.currentStatus = 'stopped';
|
|
169
|
+
this.detectStatusTransition();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
getState(): ProviderState {
|
|
174
|
+
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
175
|
+
|
|
176
|
+
// Recent 50 messages
|
|
177
|
+
const recentMessages = this.messages.slice(-50).map(m => {
|
|
178
|
+
const content = this.truncateContent(m.content);
|
|
179
|
+
return {
|
|
180
|
+
role: m.role,
|
|
181
|
+
content,
|
|
182
|
+
timestamp: m.timestamp,
|
|
183
|
+
toolCalls: m.toolCalls,
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// generating during partial response add
|
|
188
|
+
if (this.currentStatus === 'generating' && (this.partialContent || this.partialBlocks.length > 0)) {
|
|
189
|
+
const blocks = this.buildPartialBlocks();
|
|
190
|
+
if (blocks.length > 0) {
|
|
191
|
+
recentMessages.push({
|
|
192
|
+
role: 'assistant',
|
|
193
|
+
content: blocks,
|
|
194
|
+
timestamp: Date.now(),
|
|
195
|
+
toolCalls: this.turnToolCalls.length > 0 ? [...this.turnToolCalls] : undefined,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
type: this.type,
|
|
202
|
+
name: this.provider.name,
|
|
203
|
+
category: 'acp',
|
|
204
|
+
status: this.currentStatus,
|
|
205
|
+
mode: 'chat',
|
|
206
|
+
activeChat: {
|
|
207
|
+
id: this.sessionId || `${this.type}_${this.workingDir}`,
|
|
208
|
+
title: `${this.provider.name} · ${dirName}`,
|
|
209
|
+
status: this.currentStatus,
|
|
210
|
+
messages: recentMessages,
|
|
211
|
+
activeModal: this.currentStatus === 'waiting_approval' ? {
|
|
212
|
+
message: this.activeToolCalls.find(t => t.status === 'running')?.name || 'Permission requested',
|
|
213
|
+
buttons: ['Approve', 'Reject'],
|
|
214
|
+
} : null,
|
|
215
|
+
inputContent: '',
|
|
216
|
+
},
|
|
217
|
+
workingDir: this.workingDir,
|
|
218
|
+
currentModel: this.currentModel,
|
|
219
|
+
currentPlan: this.currentMode,
|
|
220
|
+
instanceId: this.instanceId,
|
|
221
|
+
lastUpdated: Date.now(),
|
|
222
|
+
settings: this.settings,
|
|
223
|
+
pendingEvents: this.flushEvents(),
|
|
224
|
+
// ACP-specific: expose available models/modes for dashboard
|
|
225
|
+
acpConfigOptions: this.configOptions,
|
|
226
|
+
acpModes: this.availableModes,
|
|
227
|
+
// Error details for dashboard display
|
|
228
|
+
errorMessage: this.errorMessage,
|
|
229
|
+
errorReason: this.errorReason,
|
|
230
|
+
} as any;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
onEvent(event: string, data?: any): void {
|
|
234
|
+
if (event === 'send_message' && data?.text) {
|
|
235
|
+
this.sendPrompt(data.text).catch(e =>
|
|
236
|
+
this.log.warn(`[${this.type}] sendPrompt error: ${e?.message}`)
|
|
237
|
+
);
|
|
238
|
+
} else if (event === 'resolve_action') {
|
|
239
|
+
const action = data?.action || 'approve';
|
|
240
|
+
this.resolvePermission(action === 'approve' || action === 'accept')
|
|
241
|
+
.catch(e => this.log.warn(`[${this.type}] resolvePermission error: ${e?.message}`));
|
|
242
|
+
} else if (event === 'cancel') {
|
|
243
|
+
this.cancelSession().catch(e =>
|
|
244
|
+
this.log.warn(`[${this.type}] cancel error: ${e?.message}`)
|
|
245
|
+
);
|
|
246
|
+
} else if (event === 'change_model' && data?.model) {
|
|
247
|
+
this.setConfigOption('model', data.model).catch(e =>
|
|
248
|
+
this.log.warn(`[${this.type}] change_model error: ${e?.message}`)
|
|
249
|
+
);
|
|
250
|
+
} else if (event === 'set_mode' && data?.mode) {
|
|
251
|
+
this.setMode(data.mode).catch(e =>
|
|
252
|
+
this.log.warn(`[${this.type}] set_mode error: ${e?.message}`)
|
|
253
|
+
);
|
|
254
|
+
} else if (event === 'set_thought_level' && data?.level) {
|
|
255
|
+
this.setConfigOption('thought_level', data.level).catch(e =>
|
|
256
|
+
this.log.warn(`[${this.type}] set_thought_level error: ${e?.message}`)
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ─── ACP Config Options & Modes ─────────────────────
|
|
262
|
+
|
|
263
|
+
private parseConfigOptions(raw: any): void {
|
|
264
|
+
if (!Array.isArray(raw)) return;
|
|
265
|
+
this.configOptions = [];
|
|
266
|
+
for (const opt of raw) {
|
|
267
|
+
const category = opt.category || 'other';
|
|
268
|
+
const configId = opt.configId || opt.id || '';
|
|
269
|
+
const currentValue = opt.currentValue ?? opt.select?.currentValue;
|
|
270
|
+
|
|
271
|
+
// flatten options (ungrouped + grouped)
|
|
272
|
+
const flatOptions: AcpConfigOption['options'] = [];
|
|
273
|
+
const selectOpts = opt.select?.options || opt.options;
|
|
274
|
+
if (selectOpts) {
|
|
275
|
+
// ungrouped options
|
|
276
|
+
if (Array.isArray(selectOpts.ungrouped)) {
|
|
277
|
+
for (const o of selectOpts.ungrouped) {
|
|
278
|
+
flatOptions.push({ value: o.value, name: o.name || o.value, description: o.description });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// grouped options
|
|
282
|
+
if (Array.isArray(selectOpts.grouped)) {
|
|
283
|
+
for (const g of selectOpts.grouped) {
|
|
284
|
+
const groupName = g.name || g.group || '';
|
|
285
|
+
for (const o of (Array.isArray(g.options?.ungrouped) ? g.options.ungrouped : (g.options || []))) {
|
|
286
|
+
flatOptions.push({ value: o.value, name: o.name || o.value, description: o.description, group: groupName });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// direct array
|
|
291
|
+
if (Array.isArray(selectOpts)) {
|
|
292
|
+
for (const o of selectOpts) {
|
|
293
|
+
if (o.value) flatOptions.push({ value: o.value, name: o.name || o.value, description: o.description });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
this.configOptions.push({ category: category as any, configId, currentValue, options: flatOptions });
|
|
299
|
+
|
|
300
|
+
// Auto-set currentModel/currentMode from config
|
|
301
|
+
if (category === 'model' && currentValue) this.currentModel = currentValue;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private parseModes(raw: any): void {
|
|
306
|
+
if (!raw) return;
|
|
307
|
+
// modes: { currentModeId, availableModes: [{ id, name, description }] }
|
|
308
|
+
if (raw.currentModeId) this.currentMode = raw.currentModeId;
|
|
309
|
+
if (Array.isArray(raw.availableModes)) {
|
|
310
|
+
this.availableModes = raw.availableModes.map((m: any) => ({
|
|
311
|
+
id: m.id, name: m.name || m.id, description: m.description,
|
|
312
|
+
}));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async setConfigOption(category: string, value: string): Promise<void> {
|
|
317
|
+
// Find configId for this category
|
|
318
|
+
const opt = this.configOptions.find(c => c.category === category);
|
|
319
|
+
if (!opt) {
|
|
320
|
+
this.log.warn(`[${this.type}] No config option for category: ${category}`);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Static config mode: update selection and restart process
|
|
325
|
+
if (this.useStaticConfig) {
|
|
326
|
+
opt.currentValue = value;
|
|
327
|
+
this.selectedConfig[opt.configId] = value;
|
|
328
|
+
if (category === 'model') this.currentModel = value;
|
|
329
|
+
if (category === 'mode') this.currentMode = value;
|
|
330
|
+
this.log.info(`[${this.type}] Static config ${category} set to: ${value} — restarting agent`);
|
|
331
|
+
await this.restartWithNewConfig();
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (!this.connection || !this.sessionId) {
|
|
336
|
+
this.log.warn(`[${this.type}] Cannot set config: no active connection/session`);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
this.log.info(`[${this.type}] Sending session/set_config_option: configId=${opt.configId} value=${value} sessionId=${this.sessionId}`);
|
|
342
|
+
const result = await this.connection.setSessionConfigOption({
|
|
343
|
+
sessionId: this.sessionId,
|
|
344
|
+
configId: opt.configId,
|
|
345
|
+
value,
|
|
346
|
+
});
|
|
347
|
+
// Update local state
|
|
348
|
+
opt.currentValue = value;
|
|
349
|
+
if (category === 'model') this.currentModel = value;
|
|
350
|
+
// Response may include updated configOptions
|
|
351
|
+
if (result?.configOptions) this.parseConfigOptions(result.configOptions);
|
|
352
|
+
this.log.info(`[${this.type}] Config ${category} set to: ${value} | response: ${JSON.stringify(result)?.slice(0, 300)}`);
|
|
353
|
+
} catch (e: any) {
|
|
354
|
+
this.log.warn(`[${this.type}] set_config_option failed: ${e?.message}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async setMode(modeId: string): Promise<void> {
|
|
359
|
+
// Static config: mode changes via restart
|
|
360
|
+
if (this.useStaticConfig) {
|
|
361
|
+
const opt = this.configOptions.find(c => c.category === 'mode');
|
|
362
|
+
if (opt) {
|
|
363
|
+
opt.currentValue = modeId;
|
|
364
|
+
this.selectedConfig[opt.configId] = modeId;
|
|
365
|
+
}
|
|
366
|
+
this.currentMode = modeId;
|
|
367
|
+
this.log.info(`[${this.type}] Static mode set to: ${modeId} — restarting agent`);
|
|
368
|
+
await this.restartWithNewConfig();
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (!this.connection || !this.sessionId) {
|
|
373
|
+
this.log.warn(`[${this.type}] Cannot set mode: no active connection/session`);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
await this.connection.setSessionMode({
|
|
379
|
+
sessionId: this.sessionId,
|
|
380
|
+
modeId,
|
|
381
|
+
});
|
|
382
|
+
this.currentMode = modeId;
|
|
383
|
+
this.log.info(`[${this.type}] Mode set to: ${modeId}`);
|
|
384
|
+
} catch (e: any) {
|
|
385
|
+
this.log.warn(`[${this.type}] set_mode failed: ${e?.message}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Static config: kill process and restart with new args */
|
|
390
|
+
private async restartWithNewConfig(): Promise<void> {
|
|
391
|
+
// Build new args from spawnArgBuilder
|
|
392
|
+
if (this.provider.spawnArgBuilder) {
|
|
393
|
+
this.cliArgs = []; // clear previous extra args
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Kill existing process
|
|
397
|
+
if (this.process) {
|
|
398
|
+
try { this.process.kill('SIGTERM'); } catch { }
|
|
399
|
+
this.process = null;
|
|
400
|
+
}
|
|
401
|
+
this.connection = null;
|
|
402
|
+
this.sessionId = null;
|
|
403
|
+
|
|
404
|
+
this.currentStatus = 'starting';
|
|
405
|
+
this.detectStatusTransition();
|
|
406
|
+
|
|
407
|
+
// Re-spawn with updated config
|
|
408
|
+
await this.spawnAgent();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Update settings at runtime (called when user changes settings from dashboard) */
|
|
412
|
+
updateSettings(newSettings: Record<string, any>): void {
|
|
413
|
+
this.settings = { ...this.settings, ...newSettings };
|
|
414
|
+
this.monitor.updateConfig({
|
|
415
|
+
approvalAlert: this.settings.approvalAlert !== false,
|
|
416
|
+
longGeneratingAlert: this.settings.longGeneratingAlert !== false,
|
|
417
|
+
longGeneratingThresholdSec: this.settings.longGeneratingThresholdSec || 180,
|
|
418
|
+
});
|
|
419
|
+
this.log.info(`[${this.type}] Settings updated: ${Object.keys(newSettings).join(', ')}`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
dispose(): void {
|
|
423
|
+
// kill process
|
|
424
|
+
if (this.process) {
|
|
425
|
+
try { this.process.kill('SIGTERM'); } catch { }
|
|
426
|
+
this.process = null;
|
|
427
|
+
}
|
|
428
|
+
this.connection = null;
|
|
429
|
+
this.monitor.reset();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ─── ACP Process Management ──────────────────────
|
|
433
|
+
|
|
434
|
+
private async spawnAgent(): Promise<void> {
|
|
435
|
+
const spawnConfig = this.provider.spawn;
|
|
436
|
+
if (!spawnConfig) {
|
|
437
|
+
throw new Error(`[ACP:${this.type}] No spawn config defined`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const command = spawnConfig.command;
|
|
441
|
+
// Static config: create args via spawnArgBuilder (when provider defines it)
|
|
442
|
+
let baseArgs = spawnConfig.args || [];
|
|
443
|
+
if (this.provider.spawnArgBuilder && Object.keys(this.selectedConfig).length > 0) {
|
|
444
|
+
baseArgs = this.provider.spawnArgBuilder(this.selectedConfig);
|
|
445
|
+
}
|
|
446
|
+
const args = [...baseArgs, ...this.cliArgs];
|
|
447
|
+
|
|
448
|
+
// Auth: each CLI/ACP tool manages its own authentication.
|
|
449
|
+
// ADHDev does NOT inject API keys — tools read their own env vars or config files.
|
|
450
|
+
|
|
451
|
+
const env = { ...process.env, ...(spawnConfig.env || {}) };
|
|
452
|
+
|
|
453
|
+
this.log.info(`[${this.type}] Spawning: ${command} ${args.join(' ')} in ${this.workingDir}`);
|
|
454
|
+
|
|
455
|
+
this.spawnedAt = Date.now();
|
|
456
|
+
this.errorMessage = null;
|
|
457
|
+
this.errorReason = null;
|
|
458
|
+
this.stderrBuffer = [];
|
|
459
|
+
|
|
460
|
+
this.process = spawn(command, args, {
|
|
461
|
+
cwd: this.workingDir,
|
|
462
|
+
env,
|
|
463
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
464
|
+
shell: spawnConfig.shell || false,
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// stderr → log + auth failure detection
|
|
468
|
+
const AUTH_ERROR_PATTERNS = [
|
|
469
|
+
/unauthorized|unauthenticated/i,
|
|
470
|
+
/invalid.*(?:api[_ ]?key|token|credential)/i,
|
|
471
|
+
/auth(?:entication|orization).*(?:fail|error|denied|invalid|expired)/i,
|
|
472
|
+
/(?:api[_ ]?key|token).*(?:missing|required|not set|not found|invalid|expired)/i,
|
|
473
|
+
/ENOENT|command not found|not recognized/i,
|
|
474
|
+
/permission denied/i,
|
|
475
|
+
/rate.?limit|quota.?exceeded/i,
|
|
476
|
+
/login.*required|please.*(?:login|authenticate|sign.?in)/i,
|
|
477
|
+
];
|
|
478
|
+
|
|
479
|
+
this.process.stderr?.on('data', (data) => {
|
|
480
|
+
const text = data.toString().trim();
|
|
481
|
+
if (!text) return;
|
|
482
|
+
this.log.debug(`[${this.type}:stderr] ${text.slice(0, 300)}`);
|
|
483
|
+
|
|
484
|
+
// Maintain stderr buffer (recent 20 lines)
|
|
485
|
+
this.stderrBuffer.push(text);
|
|
486
|
+
if (this.stderrBuffer.length > 20) this.stderrBuffer.shift();
|
|
487
|
+
|
|
488
|
+
// Auth failure detection
|
|
489
|
+
for (const pattern of AUTH_ERROR_PATTERNS) {
|
|
490
|
+
if (pattern.test(text)) {
|
|
491
|
+
if (/ENOENT|command not found|not recognized/i.test(text)) {
|
|
492
|
+
this.errorReason = 'not_installed';
|
|
493
|
+
this.errorMessage = `Command '${command}' not found. Install: ${(this.provider as any).install || 'check documentation'}`;
|
|
494
|
+
} else {
|
|
495
|
+
this.errorReason = 'auth_failed';
|
|
496
|
+
this.errorMessage = text.slice(0, 300);
|
|
497
|
+
}
|
|
498
|
+
this.log.warn(`[${this.type}] Error detected (${this.errorReason}): ${this.errorMessage?.slice(0, 100)}`);
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// kill process detect
|
|
505
|
+
this.process.on('exit', (code, signal) => {
|
|
506
|
+
const elapsed = Date.now() - this.spawnedAt;
|
|
507
|
+
this.log.info(`[${this.type}] Process exited: code=${code} signal=${signal} elapsed=${elapsed}ms`);
|
|
508
|
+
|
|
509
|
+
// Exit code analysis
|
|
510
|
+
if (code !== 0 && code !== null) {
|
|
511
|
+
if (!this.errorReason) {
|
|
512
|
+
if (code === 127) {
|
|
513
|
+
this.errorReason = 'not_installed';
|
|
514
|
+
this.errorMessage = `Command '${command}' not found (exit code 127). Install: ${(this.provider as any).install || 'check documentation'}`;
|
|
515
|
+
} else if (elapsed < 3000) {
|
|
516
|
+
// 3-second crash → likely install/auth issue
|
|
517
|
+
this.errorReason = this.stderrBuffer.length > 0 ? 'crash' : 'spawn_error';
|
|
518
|
+
this.errorMessage = this.stderrBuffer.length > 0
|
|
519
|
+
? `Agent crashed immediately (exit code ${code}): ${this.stderrBuffer.slice(-3).join(' | ').slice(0, 300)}`
|
|
520
|
+
: `Agent exited immediately with code ${code}. The agent may not be installed correctly.`;
|
|
521
|
+
} else {
|
|
522
|
+
this.errorReason = 'crash';
|
|
523
|
+
this.errorMessage = `Agent exited with code ${code}${this.stderrBuffer.length > 0 ? ': ' + this.stderrBuffer.slice(-1)[0]?.slice(0, 200) : ''}`;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
this.currentStatus = this.errorReason ? 'error' : 'stopped';
|
|
529
|
+
this.detectStatusTransition();
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
this.process.on('error', (err) => {
|
|
533
|
+
this.log.error(`[${this.type}] Process spawn error: ${err.message}`);
|
|
534
|
+
if (err.message.includes('ENOENT')) {
|
|
535
|
+
this.errorReason = 'not_installed';
|
|
536
|
+
this.errorMessage = `Command '${command}' not found. Install: ${(this.provider as any).install || 'check documentation'}`;
|
|
537
|
+
} else {
|
|
538
|
+
this.errorReason = 'spawn_error';
|
|
539
|
+
this.errorMessage = err.message;
|
|
540
|
+
}
|
|
541
|
+
this.currentStatus = 'error';
|
|
542
|
+
this.detectStatusTransition();
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// ─── SDK Connection Setup ────────────────────────
|
|
546
|
+
// Convert Node.js streams to Web Streams for ndJsonStream
|
|
547
|
+
const webStdin = Writable.toWeb(this.process.stdin!) as WritableStream<Uint8Array>;
|
|
548
|
+
const webStdout = Readable.toWeb(this.process.stdout!) as ReadableStream<Uint8Array>;
|
|
549
|
+
const stream = ndJsonStream(webStdin, webStdout);
|
|
550
|
+
|
|
551
|
+
// Create ClientSideConnection with our Client implementation
|
|
552
|
+
this.connection = new ClientSideConnection((_agent: Agent) => this.createClient(), stream);
|
|
553
|
+
|
|
554
|
+
// Listen for connection close
|
|
555
|
+
this.connection.signal.addEventListener('abort', () => {
|
|
556
|
+
this.log.info(`[${this.type}] ACP connection closed`);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// ACP initialize handshake
|
|
560
|
+
await this.initialize();
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ─── Client Interface Implementation ────────────────────
|
|
564
|
+
|
|
565
|
+
private createClient(): Client {
|
|
566
|
+
return {
|
|
567
|
+
requestPermission: async (params: RequestPermissionRequest): Promise<RequestPermissionResponse> => {
|
|
568
|
+
// Update active tool calls from the request
|
|
569
|
+
const tc = params.toolCall;
|
|
570
|
+
const existing = this.activeToolCalls.find(t => t.id === tc.toolCallId);
|
|
571
|
+
if (existing) {
|
|
572
|
+
existing.status = 'running';
|
|
573
|
+
if (tc.title) existing.name = tc.title;
|
|
574
|
+
} else {
|
|
575
|
+
this.activeToolCalls.push({
|
|
576
|
+
id: tc.toolCallId,
|
|
577
|
+
name: tc.title || 'unknown',
|
|
578
|
+
status: 'running',
|
|
579
|
+
input: tc.rawInput ? (typeof tc.rawInput === 'string' ? tc.rawInput : JSON.stringify(tc.rawInput)) : undefined,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// ─── Auto-approve: skip user confirmation ───
|
|
584
|
+
if (this.settings.autoApprove) {
|
|
585
|
+
this.log.info(`[${this.type}] Auto-approving: ${tc.title || tc.toolCallId}`);
|
|
586
|
+
const allowOption = params.options.find(o => o.kind === 'allow_once') || params.options.find(o => o.kind === 'allow_always');
|
|
587
|
+
if (allowOption) {
|
|
588
|
+
return { outcome: { outcome: 'selected', optionId: allowOption.optionId } };
|
|
589
|
+
}
|
|
590
|
+
return { outcome: { outcome: 'selected', optionId: params.options[0]?.optionId || '' } };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Approval request → switch to waiting_approval status
|
|
594
|
+
this.currentStatus = 'waiting_approval';
|
|
595
|
+
this.detectStatusTransition();
|
|
596
|
+
|
|
597
|
+
// Wait for user approval
|
|
598
|
+
const approved = await new Promise<boolean>((resolve) => {
|
|
599
|
+
this.permissionResolvers.push(resolve);
|
|
600
|
+
// 5-minute timeout → auto-reject
|
|
601
|
+
setTimeout(() => {
|
|
602
|
+
const idx = this.permissionResolvers.indexOf(resolve);
|
|
603
|
+
if (idx >= 0) {
|
|
604
|
+
this.permissionResolvers.splice(idx, 1);
|
|
605
|
+
resolve(false);
|
|
606
|
+
}
|
|
607
|
+
}, 300_000);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
if (approved) {
|
|
611
|
+
// Find the "allow" option (allow_once or allow_always)
|
|
612
|
+
const allowOption = params.options.find(o => o.kind === 'allow_once') || params.options.find(o => o.kind === 'allow_always');
|
|
613
|
+
if (allowOption) {
|
|
614
|
+
return { outcome: { outcome: 'selected', optionId: allowOption.optionId } };
|
|
615
|
+
}
|
|
616
|
+
// Fallback: use first option
|
|
617
|
+
return { outcome: { outcome: 'selected', optionId: params.options[0]?.optionId || '' } };
|
|
618
|
+
} else {
|
|
619
|
+
// Find the "reject" option
|
|
620
|
+
const rejectOption = params.options.find(o => o.kind === 'reject_once') || params.options.find(o => o.kind === 'reject_always');
|
|
621
|
+
if (rejectOption) {
|
|
622
|
+
return { outcome: { outcome: 'selected', optionId: rejectOption.optionId } };
|
|
623
|
+
}
|
|
624
|
+
return { outcome: { outcome: 'cancelled' } };
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
|
|
628
|
+
sessionUpdate: async (params: SessionNotification): Promise<void> => {
|
|
629
|
+
this.handleSessionUpdate(params);
|
|
630
|
+
},
|
|
631
|
+
|
|
632
|
+
// File system — not supported
|
|
633
|
+
readTextFile: async (_params: ReadTextFileRequest): Promise<ReadTextFileResponse> => {
|
|
634
|
+
throw RequestError.methodNotFound('fs/read_text_file');
|
|
635
|
+
},
|
|
636
|
+
writeTextFile: async (_params: WriteTextFileRequest): Promise<WriteTextFileResponse> => {
|
|
637
|
+
throw RequestError.methodNotFound('fs/write_text_file');
|
|
638
|
+
},
|
|
639
|
+
|
|
640
|
+
// Terminal — not supported
|
|
641
|
+
createTerminal: async (_params: CreateTerminalRequest): Promise<CreateTerminalResponse> => {
|
|
642
|
+
throw RequestError.methodNotFound('terminal/create');
|
|
643
|
+
},
|
|
644
|
+
terminalOutput: async (_params: TerminalOutputRequest): Promise<TerminalOutputResponse> => {
|
|
645
|
+
throw RequestError.methodNotFound('terminal/output');
|
|
646
|
+
},
|
|
647
|
+
releaseTerminal: async (_params: ReleaseTerminalRequest): Promise<ReleaseTerminalResponse> => {
|
|
648
|
+
throw RequestError.methodNotFound('terminal/release');
|
|
649
|
+
},
|
|
650
|
+
waitForTerminalExit: async (_params: WaitForTerminalExitRequest): Promise<WaitForTerminalExitResponse> => {
|
|
651
|
+
throw RequestError.methodNotFound('terminal/wait_for_exit');
|
|
652
|
+
},
|
|
653
|
+
killTerminal: async (_params: KillTerminalRequest): Promise<KillTerminalResponse> => {
|
|
654
|
+
throw RequestError.methodNotFound('terminal/kill');
|
|
655
|
+
},
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// ─── ACP Protocol (via SDK) ────────────────────────────
|
|
660
|
+
|
|
661
|
+
private async initialize(): Promise<void> {
|
|
662
|
+
if (!this.connection) return;
|
|
663
|
+
|
|
664
|
+
try {
|
|
665
|
+
const result = await this.connection.initialize({
|
|
666
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
667
|
+
clientCapabilities: {},
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
this.agentCapabilities = result?.agentCapabilities || {};
|
|
671
|
+
this.log.info(`[${this.type}] Initialized. Agent capabilities: ${JSON.stringify(this.agentCapabilities)}`);
|
|
672
|
+
|
|
673
|
+
// new session create
|
|
674
|
+
await this.createSession();
|
|
675
|
+
} catch (e: any) {
|
|
676
|
+
this.log.error(`[${this.type}] Initialize failed: ${e?.message}`);
|
|
677
|
+
if (!this.errorReason) {
|
|
678
|
+
this.errorReason = 'init_failed';
|
|
679
|
+
this.errorMessage = `ACP handshake failed: ${e?.message}${this.stderrBuffer.length > 0 ? '\n' + this.stderrBuffer.slice(-2).join('\n').slice(0, 200) : ''}`;
|
|
680
|
+
}
|
|
681
|
+
this.currentStatus = 'error';
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
private async createSession(): Promise<void> {
|
|
686
|
+
if (!this.connection) return;
|
|
687
|
+
|
|
688
|
+
try {
|
|
689
|
+
const result = await this.connection.newSession({
|
|
690
|
+
cwd: this.workingDir,
|
|
691
|
+
mcpServers: [],
|
|
692
|
+
});
|
|
693
|
+
this.sessionId = result?.sessionId || null;
|
|
694
|
+
this.currentStatus = 'idle';
|
|
695
|
+
this.messages = [];
|
|
696
|
+
|
|
697
|
+
// DEBUG: session/new response key check
|
|
698
|
+
this.log.info(`[${this.type}] session/new result keys: ${result ? Object.keys(result).join(', ') : 'null'}`);
|
|
699
|
+
if (result?.configOptions) this.log.debug(`[${this.type}] configOptions: ${JSON.stringify(result.configOptions).slice(0, 500)}`);
|
|
700
|
+
if (result?.modes) this.log.debug(`[${this.type}] modes: ${JSON.stringify(result.modes).slice(0, 300)}`);
|
|
701
|
+
|
|
702
|
+
// ACP configOptions parsing (model, thought_level etc)
|
|
703
|
+
this.parseConfigOptions(result?.configOptions);
|
|
704
|
+
|
|
705
|
+
// ACP modes parsing
|
|
706
|
+
this.parseModes(result?.modes);
|
|
707
|
+
|
|
708
|
+
// Legacy: models.currentModelId (some agent compat)
|
|
709
|
+
if (!this.currentModel && result?.models?.currentModelId) {
|
|
710
|
+
this.currentModel = result.models.currentModelId;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// ─── Static config fallback (for agents without config/* support) ───
|
|
714
|
+
if (this.configOptions.length === 0 && this.provider.staticConfigOptions?.length) {
|
|
715
|
+
this.useStaticConfig = true;
|
|
716
|
+
for (const sc of this.provider.staticConfigOptions) {
|
|
717
|
+
const defaultVal = this.selectedConfig[sc.configId] || sc.defaultValue || sc.options[0]?.value;
|
|
718
|
+
this.configOptions.push({
|
|
719
|
+
category: sc.category,
|
|
720
|
+
configId: sc.configId,
|
|
721
|
+
currentValue: defaultVal,
|
|
722
|
+
options: sc.options.map(o => ({ ...o })),
|
|
723
|
+
});
|
|
724
|
+
if (defaultVal) {
|
|
725
|
+
this.selectedConfig[sc.configId] = defaultVal;
|
|
726
|
+
if (sc.category === 'model') this.currentModel = defaultVal;
|
|
727
|
+
if (sc.category === 'mode') this.currentMode = defaultVal;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
this.log.info(`[${this.type}] Using static configOptions (${this.configOptions.length} options)`);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
this.log.info(`[${this.type}] Session created: ${this.sessionId}${this.currentModel ? ` (model: ${this.currentModel})` : ''}${this.currentMode ? ` (mode: ${this.currentMode})` : ''}`);
|
|
734
|
+
if (this.configOptions.length > 0) {
|
|
735
|
+
this.log.info(`[${this.type}] Config options: ${this.configOptions.map(c => `${c.category}(${c.options.length})`).join(', ')}`);
|
|
736
|
+
}
|
|
737
|
+
} catch (e: any) {
|
|
738
|
+
this.log.warn(`[${this.type}] session/new failed: ${e?.message}`);
|
|
739
|
+
this.currentStatus = 'idle';
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async sendPrompt(text: string, contentBlocks?: ContentBlock[]): Promise<void> {
|
|
744
|
+
if (!this.connection || !this.sessionId) {
|
|
745
|
+
this.log.warn(`[${this.type}] Cannot send prompt: no active connection/session`);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Build prompt content
|
|
750
|
+
let promptParts: any[];
|
|
751
|
+
if (contentBlocks && contentBlocks.length > 0) {
|
|
752
|
+
// Rich content — forward ContentBlock[] as ACP prompt parts
|
|
753
|
+
promptParts = contentBlocks.map(b => {
|
|
754
|
+
if (b.type === 'text') return { type: 'text', text: (b as any).text };
|
|
755
|
+
if (b.type === 'image') return { type: 'image', data: (b as any).data, mimeType: (b as any).mimeType };
|
|
756
|
+
if (b.type === 'resource_link') return { type: 'resource_link', uri: (b as any).uri, name: (b as any).name };
|
|
757
|
+
if (b.type === 'resource') return { type: 'resource', resource: (b as any).resource };
|
|
758
|
+
return { type: 'text', text: flattenContent([b]) };
|
|
759
|
+
});
|
|
760
|
+
} else {
|
|
761
|
+
promptParts = [{ type: 'text', text }];
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Add user message locally (store as ContentBlock[])
|
|
765
|
+
this.messages.push({
|
|
766
|
+
role: 'user',
|
|
767
|
+
content: contentBlocks && contentBlocks.length > 0 ? contentBlocks : text,
|
|
768
|
+
timestamp: Date.now(),
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
this.currentStatus = 'generating';
|
|
772
|
+
this.partialContent = '';
|
|
773
|
+
this.partialBlocks = [];
|
|
774
|
+
this.turnToolCalls = [];
|
|
775
|
+
this.detectStatusTransition();
|
|
776
|
+
this.log.info(`[${this.type}] Sending prompt: "${text.slice(0, 100)}" (${promptParts.length} parts)`);
|
|
777
|
+
|
|
778
|
+
try {
|
|
779
|
+
const result = await this.connection.prompt({
|
|
780
|
+
sessionId: this.sessionId,
|
|
781
|
+
prompt: promptParts,
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
// Prompt complete → reflect final message
|
|
785
|
+
if (result?.stopReason) {
|
|
786
|
+
this.stopReason = result.stopReason;
|
|
787
|
+
}
|
|
788
|
+
this.log.info(`[${this.type}] Prompt completed: stopReason=${result?.stopReason} partialContent=${this.partialContent.length} chars partialBlocks=${this.partialBlocks.length}`);
|
|
789
|
+
|
|
790
|
+
// Build final assistant message with rich content
|
|
791
|
+
this.finalizeAssistantMessage();
|
|
792
|
+
|
|
793
|
+
this.currentStatus = 'idle';
|
|
794
|
+
this.detectStatusTransition();
|
|
795
|
+
} catch (e: any) {
|
|
796
|
+
this.log.warn(`[${this.type}] prompt error: ${e?.message}`);
|
|
797
|
+
this.finalizeAssistantMessage();
|
|
798
|
+
this.currentStatus = 'idle';
|
|
799
|
+
this.detectStatusTransition();
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
private async cancelSession(): Promise<void> {
|
|
804
|
+
if (!this.connection || !this.sessionId) return;
|
|
805
|
+
|
|
806
|
+
await this.connection.cancel({
|
|
807
|
+
sessionId: this.sessionId,
|
|
808
|
+
});
|
|
809
|
+
this.currentStatus = 'idle';
|
|
810
|
+
this.detectStatusTransition();
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
private permissionResolvers: ((approved: boolean) => void)[] = [];
|
|
814
|
+
|
|
815
|
+
private async resolvePermission(approved: boolean): Promise<void> {
|
|
816
|
+
const resolver = this.permissionResolvers.shift();
|
|
817
|
+
if (resolver) {
|
|
818
|
+
resolver(approved);
|
|
819
|
+
}
|
|
820
|
+
if (this.currentStatus === 'waiting_approval') {
|
|
821
|
+
this.currentStatus = 'generating';
|
|
822
|
+
this.detectStatusTransition();
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// ─── ACP session/update handle ─────────────────────
|
|
827
|
+
|
|
828
|
+
private handleSessionUpdate(params: SessionNotification): void {
|
|
829
|
+
if (!params) return;
|
|
830
|
+
|
|
831
|
+
const update = params.update as SessionUpdate & Record<string, any>;
|
|
832
|
+
this.log.debug(`[${this.type}] sessionUpdate: ${update.sessionUpdate} | keys=${Object.keys(update).join(',')}`);
|
|
833
|
+
|
|
834
|
+
switch (update.sessionUpdate) {
|
|
835
|
+
case 'agent_message_chunk': {
|
|
836
|
+
const content = update.content;
|
|
837
|
+
if (content?.type === 'text' && (content as any).text) {
|
|
838
|
+
this.partialContent += (content as any).text;
|
|
839
|
+
} else if (content?.type === 'image') {
|
|
840
|
+
// Collect image block
|
|
841
|
+
this.partialBlocks.push({
|
|
842
|
+
type: 'image',
|
|
843
|
+
data: (content as any).data || '',
|
|
844
|
+
mimeType: (content as any).mimeType || 'image/png',
|
|
845
|
+
uri: (content as any).uri,
|
|
846
|
+
});
|
|
847
|
+
} else if (content?.type === 'resource_link') {
|
|
848
|
+
this.partialBlocks.push({
|
|
849
|
+
type: 'resource_link',
|
|
850
|
+
uri: (content as any).uri || '',
|
|
851
|
+
name: (content as any).name || 'resource',
|
|
852
|
+
title: (content as any).title,
|
|
853
|
+
mimeType: (content as any).mimeType,
|
|
854
|
+
size: (content as any).size,
|
|
855
|
+
});
|
|
856
|
+
} else if (content?.type === 'resource') {
|
|
857
|
+
this.partialBlocks.push({
|
|
858
|
+
type: 'resource',
|
|
859
|
+
resource: (content as any).resource,
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
this.currentStatus = 'generating';
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
865
|
+
case 'agent_thought_chunk':
|
|
866
|
+
case 'user_message_chunk': {
|
|
867
|
+
// Track but don't display thought chunks as main content
|
|
868
|
+
break;
|
|
869
|
+
}
|
|
870
|
+
case 'tool_call': {
|
|
871
|
+
// New tool call — collect as ToolCallInfo
|
|
872
|
+
const tcId = (update as any).toolCallId || `tc_${Date.now()}`;
|
|
873
|
+
const tcTitle = (update as any).title || 'unknown';
|
|
874
|
+
const tcKind = (update as any).kind as ToolKind | undefined;
|
|
875
|
+
const tcStatus = this.mapToolCallStatus((update as any).status);
|
|
876
|
+
|
|
877
|
+
this.activeToolCalls.push({
|
|
878
|
+
id: tcId,
|
|
879
|
+
name: tcTitle,
|
|
880
|
+
status: tcStatus,
|
|
881
|
+
input: (update as any).rawInput ? (typeof (update as any).rawInput === 'string' ? (update as any).rawInput : JSON.stringify((update as any).rawInput)) : undefined,
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
// Also collect as ToolCallInfo for rich content
|
|
885
|
+
const acpStatus = (update as any).status as string || 'in_progress';
|
|
886
|
+
this.turnToolCalls.push({
|
|
887
|
+
toolCallId: tcId,
|
|
888
|
+
title: tcTitle,
|
|
889
|
+
kind: tcKind,
|
|
890
|
+
status: acpStatus as TCS,
|
|
891
|
+
rawInput: (update as any).rawInput,
|
|
892
|
+
content: this.convertToolCallContent((update as any).content),
|
|
893
|
+
locations: (update as any).locations,
|
|
894
|
+
});
|
|
895
|
+
break;
|
|
896
|
+
}
|
|
897
|
+
case 'tool_call_update': {
|
|
898
|
+
// Update existing tool call
|
|
899
|
+
const toolCallId = (update as any).toolCallId;
|
|
900
|
+
const existing = this.activeToolCalls.find(t => t.id === toolCallId);
|
|
901
|
+
if (existing) {
|
|
902
|
+
if ((update as any).status) existing.status = this.mapToolCallStatus((update as any).status);
|
|
903
|
+
if ((update as any).rawOutput) existing.output = typeof (update as any).rawOutput === 'string' ? (update as any).rawOutput : JSON.stringify((update as any).rawOutput);
|
|
904
|
+
}
|
|
905
|
+
// Update ToolCallInfo too
|
|
906
|
+
const tcInfo = this.turnToolCalls.find(t => t.toolCallId === toolCallId);
|
|
907
|
+
if (tcInfo) {
|
|
908
|
+
if ((update as any).status) tcInfo.status = (update as any).status as TCS;
|
|
909
|
+
if ((update as any).rawOutput) tcInfo.rawOutput = (update as any).rawOutput;
|
|
910
|
+
if ((update as any).content) tcInfo.content = this.convertToolCallContent((update as any).content);
|
|
911
|
+
if ((update as any).locations) tcInfo.locations = (update as any).locations;
|
|
912
|
+
}
|
|
913
|
+
break;
|
|
914
|
+
}
|
|
915
|
+
case 'current_mode_update': {
|
|
916
|
+
this.currentMode = (update as any).currentModeId;
|
|
917
|
+
break;
|
|
918
|
+
}
|
|
919
|
+
case 'config_option_update': {
|
|
920
|
+
if ((update as any).configOptions) {
|
|
921
|
+
this.parseConfigOptions((update as any).configOptions);
|
|
922
|
+
}
|
|
923
|
+
break;
|
|
924
|
+
}
|
|
925
|
+
case 'plan':
|
|
926
|
+
case 'available_commands_update':
|
|
927
|
+
case 'session_info_update':
|
|
928
|
+
case 'usage_update':
|
|
929
|
+
// Noted but no specific handling needed
|
|
930
|
+
break;
|
|
931
|
+
default:
|
|
932
|
+
// Unknown update type — try legacy parsing for backward compatibility
|
|
933
|
+
this.handleLegacyUpdate(update as any);
|
|
934
|
+
break;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/** Handle legacy session/update formats (pre-standardization compat) */
|
|
939
|
+
private handleLegacyUpdate(params: any): void {
|
|
940
|
+
// Legacy: messageDelta format
|
|
941
|
+
if (params.messageDelta) {
|
|
942
|
+
const delta = params.messageDelta;
|
|
943
|
+
if (delta.content) {
|
|
944
|
+
for (const part of Array.isArray(delta.content) ? delta.content : [delta.content]) {
|
|
945
|
+
if (part.type === 'text' && part.text) {
|
|
946
|
+
this.partialContent += part.text;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
this.currentStatus = 'generating';
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Legacy: message complete
|
|
954
|
+
if (params.message) {
|
|
955
|
+
const m = params.message;
|
|
956
|
+
let content = '';
|
|
957
|
+
if (typeof m.content === 'string') {
|
|
958
|
+
content = m.content;
|
|
959
|
+
} else if (Array.isArray(m.content)) {
|
|
960
|
+
content = m.content
|
|
961
|
+
.filter((p: any) => p.type === 'text')
|
|
962
|
+
.map((p: any) => p.text || '')
|
|
963
|
+
.join('\n');
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (content.trim()) {
|
|
967
|
+
this.messages.push({
|
|
968
|
+
role: m.role || 'assistant',
|
|
969
|
+
content: content.trim(),
|
|
970
|
+
timestamp: Date.now(),
|
|
971
|
+
});
|
|
972
|
+
this.partialContent = '';
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Legacy: toolCallUpdate
|
|
977
|
+
if (params.toolCallUpdate) {
|
|
978
|
+
const tc = params.toolCallUpdate;
|
|
979
|
+
const existing = this.activeToolCalls.find(t => t.id === tc.id);
|
|
980
|
+
if (existing) {
|
|
981
|
+
if (tc.status) existing.status = tc.status;
|
|
982
|
+
if (tc.output) existing.output = tc.output;
|
|
983
|
+
} else {
|
|
984
|
+
this.activeToolCalls.push({
|
|
985
|
+
id: tc.id || `tc_${Date.now()}`,
|
|
986
|
+
name: tc.name || 'unknown',
|
|
987
|
+
status: tc.status || 'running',
|
|
988
|
+
input: typeof tc.input === 'string' ? tc.input : JSON.stringify(tc.input),
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Legacy: stopReason
|
|
994
|
+
if (params.stopReason) {
|
|
995
|
+
this.stopReason = params.stopReason;
|
|
996
|
+
if (params.stopReason !== 'cancelled') {
|
|
997
|
+
this.currentStatus = 'idle';
|
|
998
|
+
}
|
|
999
|
+
this.activeToolCalls = [];
|
|
1000
|
+
this.detectStatusTransition();
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Legacy: model info
|
|
1004
|
+
if (params.model) {
|
|
1005
|
+
this.currentModel = params.model;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/** Map SDK ToolCallStatus to internal status */
|
|
1010
|
+
private mapToolCallStatus(status?: ToolCallStatus | string): 'running' | 'completed' | 'failed' {
|
|
1011
|
+
switch (status) {
|
|
1012
|
+
case 'completed': return 'completed';
|
|
1013
|
+
case 'failed': return 'failed';
|
|
1014
|
+
case 'pending':
|
|
1015
|
+
case 'in_progress':
|
|
1016
|
+
default: return 'running';
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// ─── Rich Content Helpers ────────────────────────────
|
|
1021
|
+
|
|
1022
|
+
/** Truncate content for transport (text: 2000 chars, images preserved) */
|
|
1023
|
+
private truncateContent(content: string | ContentBlock[]): string | ContentBlock[] {
|
|
1024
|
+
if (typeof content === 'string') {
|
|
1025
|
+
return content.length > 2000 ? content.slice(0, 2000) + '\n... (truncated)' : content;
|
|
1026
|
+
}
|
|
1027
|
+
return content.map(b => {
|
|
1028
|
+
if (b.type === 'text' && b.text.length > 2000) {
|
|
1029
|
+
return { ...b, text: b.text.slice(0, 2000) + '\n... (truncated)' };
|
|
1030
|
+
}
|
|
1031
|
+
return b;
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/** Build ContentBlock[] from current partial state */
|
|
1036
|
+
private buildPartialBlocks(): ContentBlock[] {
|
|
1037
|
+
const blocks: ContentBlock[] = [];
|
|
1038
|
+
if (this.partialContent.trim()) {
|
|
1039
|
+
blocks.push({ type: 'text', text: this.partialContent.trim() + '...' });
|
|
1040
|
+
}
|
|
1041
|
+
blocks.push(...this.partialBlocks);
|
|
1042
|
+
return blocks;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/** Finalize streaming content into an assistant message */
|
|
1046
|
+
private finalizeAssistantMessage(): void {
|
|
1047
|
+
const blocks = this.buildPartialBlocks();
|
|
1048
|
+
// Remove trailing '...' from text blocks for final message
|
|
1049
|
+
const finalBlocks = blocks.map(b => {
|
|
1050
|
+
if (b.type === 'text' && b.text.endsWith('...')) {
|
|
1051
|
+
return { ...b, text: b.text.slice(0, -3) };
|
|
1052
|
+
}
|
|
1053
|
+
return b;
|
|
1054
|
+
}).filter(b => b.type !== 'text' || (b as any).text.trim());
|
|
1055
|
+
|
|
1056
|
+
if (finalBlocks.length > 0) {
|
|
1057
|
+
this.messages.push({
|
|
1058
|
+
role: 'assistant',
|
|
1059
|
+
content: finalBlocks.length === 1 && finalBlocks[0].type === 'text'
|
|
1060
|
+
? (finalBlocks[0] as any).text // single text → string (backward compat)
|
|
1061
|
+
: finalBlocks,
|
|
1062
|
+
timestamp: Date.now(),
|
|
1063
|
+
toolCalls: this.turnToolCalls.length > 0 ? [...this.turnToolCalls] : undefined,
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
this.partialContent = '';
|
|
1067
|
+
this.partialBlocks = [];
|
|
1068
|
+
this.turnToolCalls = [];
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/** Convert ACP ToolCallContent[] to our ToolCallContent[] */
|
|
1072
|
+
private convertToolCallContent(acpContent?: any[]): TCC[] | undefined {
|
|
1073
|
+
if (!acpContent || !Array.isArray(acpContent)) return undefined;
|
|
1074
|
+
return acpContent.map((c: any) => {
|
|
1075
|
+
if (c.type === 'diff') {
|
|
1076
|
+
return { type: 'diff' as const, path: c.path || '', oldText: c.oldText, newText: c.newText || '' };
|
|
1077
|
+
}
|
|
1078
|
+
if (c.type === 'terminal') {
|
|
1079
|
+
return { type: 'terminal' as const, terminalId: c.terminalId || '' };
|
|
1080
|
+
}
|
|
1081
|
+
// type: 'content' or unknown
|
|
1082
|
+
return { type: 'content' as const, content: c.content || { type: 'text' as const, text: JSON.stringify(c) } };
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// ─── status transition detect ────────────────────────────
|
|
1087
|
+
|
|
1088
|
+
private detectStatusTransition(): void {
|
|
1089
|
+
const now = Date.now();
|
|
1090
|
+
const newStatus = this.currentStatus;
|
|
1091
|
+
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
1092
|
+
const chatTitle = `${this.provider.name} · ${dirName}`;
|
|
1093
|
+
|
|
1094
|
+
if (newStatus !== this.lastStatus) {
|
|
1095
|
+
if (this.lastStatus === 'idle' && newStatus === 'generating') {
|
|
1096
|
+
this.generatingStartedAt = now;
|
|
1097
|
+
this.pushEvent({ event: 'agent:generating_started', chatTitle, timestamp: now });
|
|
1098
|
+
} else if (newStatus === 'waiting_approval') {
|
|
1099
|
+
if (!this.generatingStartedAt) this.generatingStartedAt = now;
|
|
1100
|
+
this.pushEvent({
|
|
1101
|
+
event: 'agent:waiting_approval', chatTitle, timestamp: now,
|
|
1102
|
+
modalMessage: this.activeToolCalls.find(t => t.status === 'running')?.name,
|
|
1103
|
+
});
|
|
1104
|
+
} else if (newStatus === 'idle' && (this.lastStatus === 'generating' || this.lastStatus === 'waiting_approval')) {
|
|
1105
|
+
const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : 0;
|
|
1106
|
+
this.pushEvent({ event: 'agent:generating_completed', chatTitle, duration, timestamp: now });
|
|
1107
|
+
this.generatingStartedAt = 0;
|
|
1108
|
+
} else if (newStatus === 'stopped') {
|
|
1109
|
+
this.pushEvent({ event: 'agent:stopped', chatTitle, timestamp: now });
|
|
1110
|
+
}
|
|
1111
|
+
this.lastStatus = newStatus;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Monitor check
|
|
1115
|
+
const agentKey = `${this.type}:acp`;
|
|
1116
|
+
const monitorEvents = this.monitor.check(agentKey, newStatus, now);
|
|
1117
|
+
for (const me of monitorEvents) {
|
|
1118
|
+
this.pushEvent({ event: me.type, agentKey: me.agentKey, message: me.message, elapsedSec: me.elapsedSec, timestamp: me.timestamp });
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
private pushEvent(event: ProviderEvent): void {
|
|
1123
|
+
this.events.push(event);
|
|
1124
|
+
if (this.events.length > 50) this.events = this.events.slice(-50);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
private flushEvents(): ProviderEvent[] {
|
|
1128
|
+
const events = [...this.events];
|
|
1129
|
+
this.events = [];
|
|
1130
|
+
return events;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// ─── external access ─────────────────────────────────
|
|
1134
|
+
|
|
1135
|
+
get cliType(): string { return this.type; }
|
|
1136
|
+
get cliName(): string { return this.provider.name; }
|
|
1137
|
+
|
|
1138
|
+
/** ACP Agent capabilities (available after initialize) */
|
|
1139
|
+
getCapabilities(): Record<string, any> { return this.agentCapabilities; }
|
|
1140
|
+
}
|