@adhdev/daemon-core 0.8.22 → 0.8.24
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/agent-stream/types.d.ts +3 -0
- package/dist/cli-adapter-types.d.ts +3 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +71 -11
- package/dist/commands/stream-commands.d.ts +1 -0
- package/dist/config/config.d.ts +6 -0
- package/dist/index.js +1162 -307
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1162 -307
- package/dist/index.mjs.map +1 -1
- package/dist/providers/cli-provider-instance.d.ts +6 -0
- package/dist/providers/contracts.d.ts +59 -1
- package/dist/providers/control-effects.d.ts +4 -0
- package/dist/providers/extension-provider-instance.d.ts +9 -0
- package/dist/providers/ide-provider-instance.d.ts +8 -0
- package/dist/shared-types.d.ts +2 -1
- package/node_modules/@adhdev/session-host-core/package.json +1 -1
- package/package.json +3 -2
- package/src/agent-stream/forward.ts +2 -0
- package/src/agent-stream/provider-adapter.ts +5 -15
- package/src/agent-stream/types.ts +4 -0
- package/src/cli-adapter-types.ts +3 -0
- package/src/cli-adapters/provider-cli-adapter.ts +399 -49
- package/src/commands/handler.ts +1 -0
- package/src/commands/stream-commands.ts +99 -8
- package/src/config/config.d.ts +1 -0
- package/src/config/config.ts +9 -0
- package/src/launch.ts +57 -11
- package/src/providers/cli-provider-instance.ts +148 -2
- package/src/providers/contracts.ts +65 -2
- package/src/providers/control-effects.ts +114 -0
- package/src/providers/extension-provider-instance.ts +163 -3
- package/src/providers/ide-provider-instance.ts +181 -2
- package/src/shared-types.d.ts +1 -0
- package/src/shared-types.ts +2 -1
- package/src/status/snapshot.ts +1 -0
package/src/commands/handler.ts
CHANGED
|
@@ -441,6 +441,7 @@ export class DaemonCommandHandler implements CommandHelpers {
|
|
|
441
441
|
case 'set_ide_extension': return Stream.handleSetIdeExtension(this, args);
|
|
442
442
|
|
|
443
443
|
// ─── Extension Model / Mode Control (stream-commands.ts) ──────────
|
|
444
|
+
case 'invoke_provider_script': return Stream.handleProviderScript(this, args);
|
|
444
445
|
case 'list_extension_models': return Stream.handleExtensionScript(this, args, 'listModels');
|
|
445
446
|
case 'set_extension_model': return Stream.handleExtensionScript(this, args, 'setModel');
|
|
446
447
|
case 'list_extension_modes': return Stream.handleExtensionScript(this, args, 'listModes');
|
|
@@ -96,7 +96,67 @@ export function handleSetProviderSetting(h: CommandHelpers, args: any): CommandR
|
|
|
96
96
|
|
|
97
97
|
// ─── Extension Script Execution (Model/Mode) ─────
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
function normalizeProviderScriptArgs(args: any): Record<string, any> {
|
|
100
|
+
const normalizedArgs = { ...(args || {}) };
|
|
101
|
+
for (const key of ['mode', 'model', 'message', 'action', 'button', 'text', 'sessionId', 'value']) {
|
|
102
|
+
if (key in normalizedArgs && !(key.toUpperCase() in normalizedArgs)) {
|
|
103
|
+
normalizedArgs[key.toUpperCase()] = normalizedArgs[key];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return normalizedArgs;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function parseScriptResult(result: unknown): { success: boolean; payload: any } {
|
|
110
|
+
if (typeof result === 'string') {
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(result);
|
|
113
|
+
if (parsed && typeof parsed === 'object' && parsed.success === false) {
|
|
114
|
+
return { success: false, payload: parsed };
|
|
115
|
+
}
|
|
116
|
+
return { success: true, payload: parsed };
|
|
117
|
+
} catch {
|
|
118
|
+
return { success: true, payload: { result } };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (result && typeof result === 'object' && (result as any).success === false) {
|
|
122
|
+
return { success: false, payload: result };
|
|
123
|
+
}
|
|
124
|
+
return { success: true, payload: result };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getCliScriptCommand(payload: any): { type: string; text?: string } | null {
|
|
128
|
+
if (!payload || typeof payload !== 'object') return null;
|
|
129
|
+
|
|
130
|
+
if (typeof payload.sendMessage === 'string' && payload.sendMessage.trim()) {
|
|
131
|
+
return { type: 'send_message', text: payload.sendMessage.trim() };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const command = payload.command;
|
|
135
|
+
if (!command || typeof command !== 'object') return null;
|
|
136
|
+
if (command.type !== 'send_message') return null;
|
|
137
|
+
|
|
138
|
+
const text = typeof command.text === 'string'
|
|
139
|
+
? command.text.trim()
|
|
140
|
+
: typeof command.message === 'string'
|
|
141
|
+
? command.message.trim()
|
|
142
|
+
: '';
|
|
143
|
+
if (!text) return null;
|
|
144
|
+
return { type: 'send_message', text };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function applyProviderPatch(h: CommandHelpers, args: any, payload: any): void {
|
|
148
|
+
if (!payload || typeof payload !== 'object') return;
|
|
149
|
+
const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
|
|
150
|
+
const targetSession = targetSessionId ? h.ctx.sessionRegistry?.get(targetSessionId) : undefined;
|
|
151
|
+
const instanceKey = targetSession?.instanceKey || targetSessionId;
|
|
152
|
+
if (!instanceKey) return;
|
|
153
|
+
h.ctx.instanceManager?.sendEvent(instanceKey, 'provider_state_patch', {
|
|
154
|
+
...payload,
|
|
155
|
+
extensionType: targetSession?.transport === 'cdp-webview' ? targetSession.providerType : undefined,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function executeProviderScript(h: CommandHelpers, args: any, scriptName: string): Promise<CommandResult> {
|
|
100
160
|
const { agentType, ideType } = args || {};
|
|
101
161
|
if (!agentType) return { success: false, error: 'agentType is required' };
|
|
102
162
|
|
|
@@ -115,15 +175,31 @@ export async function handleExtensionScript(h: CommandHelpers, args: any, script
|
|
|
115
175
|
return { success: false, error: `Script '${actualScriptName}' not available for ${agentType}` };
|
|
116
176
|
}
|
|
117
177
|
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
178
|
+
const normalizedArgs = normalizeProviderScriptArgs(args);
|
|
179
|
+
|
|
180
|
+
if (provider.category === 'cli') {
|
|
181
|
+
const adapter = h.getCliAdapter(args?.targetSessionId || agentType);
|
|
182
|
+
if (!adapter?.invokeScript) {
|
|
183
|
+
return { success: false, error: `CLI adapter does not support script '${actualScriptName}'` };
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const raw = await adapter.invokeScript(actualScriptName, normalizedArgs);
|
|
187
|
+
const parsed = parseScriptResult(raw);
|
|
188
|
+
if (!parsed.success) {
|
|
189
|
+
return { success: false, ...(parsed.payload || {}) };
|
|
190
|
+
}
|
|
191
|
+
const cliCommand = getCliScriptCommand(parsed.payload);
|
|
192
|
+
if (cliCommand?.type === 'send_message' && cliCommand.text) {
|
|
193
|
+
await adapter.sendMessage(cliCommand.text);
|
|
194
|
+
}
|
|
195
|
+
applyProviderPatch(h, args, parsed.payload);
|
|
196
|
+
return { success: true, ...(parsed.payload && typeof parsed.payload === 'object' ? parsed.payload : { result: parsed.payload }) };
|
|
197
|
+
} catch (e: any) {
|
|
198
|
+
return { success: false, error: `Script execution failed: ${e.message}` };
|
|
125
199
|
}
|
|
126
200
|
}
|
|
201
|
+
|
|
202
|
+
const scriptFn = provider.scripts[actualScriptName as keyof typeof provider.scripts] as Function;
|
|
127
203
|
const scriptCode = scriptFn(normalizedArgs);
|
|
128
204
|
if (!scriptCode) return { success: false, error: `Script '${actualScriptName}' returned null` };
|
|
129
205
|
|
|
@@ -187,17 +263,32 @@ export async function handleExtensionScript(h: CommandHelpers, args: any, script
|
|
|
187
263
|
if (typeof result === 'string') {
|
|
188
264
|
try {
|
|
189
265
|
const parsed = JSON.parse(result);
|
|
266
|
+
applyProviderPatch(h, args, parsed);
|
|
267
|
+
if (parsed && typeof parsed === 'object' && parsed.success === false) {
|
|
268
|
+
return { success: false, ...parsed };
|
|
269
|
+
}
|
|
190
270
|
return { success: true, ...parsed };
|
|
191
271
|
} catch {
|
|
192
272
|
return { success: true, result };
|
|
193
273
|
}
|
|
194
274
|
}
|
|
275
|
+
applyProviderPatch(h, args, result);
|
|
195
276
|
return { success: true, result };
|
|
196
277
|
} catch (e: any) {
|
|
197
278
|
return { success: false, error: `Script execution failed: ${e.message}` };
|
|
198
279
|
}
|
|
199
280
|
}
|
|
200
281
|
|
|
282
|
+
export async function handleExtensionScript(h: CommandHelpers, args: any, scriptName: string): Promise<CommandResult> {
|
|
283
|
+
return executeProviderScript(h, args, scriptName);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export async function handleProviderScript(h: CommandHelpers, args: any): Promise<CommandResult> {
|
|
287
|
+
const scriptName = typeof args?.scriptName === 'string' ? args.scriptName.trim() : '';
|
|
288
|
+
if (!scriptName) return { success: false, error: 'scriptName is required' };
|
|
289
|
+
return executeProviderScript(h, args, scriptName);
|
|
290
|
+
}
|
|
291
|
+
|
|
201
292
|
// ─── IDE Extension Settings (per-IDE on/off) ─────
|
|
202
293
|
|
|
203
294
|
export function handleGetIdeExtensions(h: CommandHelpers, args: any): CommandResult {
|
package/src/config/config.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export interface ADHDevConfig {
|
|
|
37
37
|
}>;
|
|
38
38
|
disableUpstream?: boolean;
|
|
39
39
|
providerDir?: string;
|
|
40
|
+
terminalSizingMode?: 'measured' | 'fit';
|
|
40
41
|
}
|
|
41
42
|
export declare function generateMachineId(): string;
|
|
42
43
|
export declare function isStableMachineId(machineId?: string | null): boolean;
|
package/src/config/config.ts
CHANGED
|
@@ -78,6 +78,13 @@ export interface ADHDevConfig {
|
|
|
78
78
|
|
|
79
79
|
// Optional custom provider directory for local development
|
|
80
80
|
providerDir?: string;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Browser terminal sizing behavior for dashboard CLI panes.
|
|
84
|
+
* Default `measured` keeps terminal size daemon-authoritative.
|
|
85
|
+
* `fit` opt-in restores xterm fit-based sizing for advanced users.
|
|
86
|
+
*/
|
|
87
|
+
terminalSizingMode?: 'measured' | 'fit';
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
const DEFAULT_CONFIG: ADHDevConfig = {
|
|
@@ -99,6 +106,7 @@ const DEFAULT_CONFIG: ADHDevConfig = {
|
|
|
99
106
|
providerSettings: {},
|
|
100
107
|
ideSettings: {},
|
|
101
108
|
disableUpstream: false,
|
|
109
|
+
terminalSizingMode: 'measured',
|
|
102
110
|
};
|
|
103
111
|
|
|
104
112
|
const MACHINE_ID_PREFIX = 'mach_';
|
|
@@ -149,6 +157,7 @@ function normalizeConfig(raw: unknown): ADHDevConfig & { activeWorkspaceId?: str
|
|
|
149
157
|
ideSettings: isPlainObject(parsed.ideSettings) ? parsed.ideSettings : {},
|
|
150
158
|
disableUpstream: asBoolean(parsed.disableUpstream, DEFAULT_CONFIG.disableUpstream ?? false),
|
|
151
159
|
providerDir: asOptionalString(parsed.providerDir),
|
|
160
|
+
terminalSizingMode: parsed.terminalSizingMode === 'fit' ? 'fit' : 'measured',
|
|
152
161
|
};
|
|
153
162
|
}
|
|
154
163
|
|
package/src/launch.ts
CHANGED
|
@@ -23,6 +23,7 @@ import * as path from 'path';
|
|
|
23
23
|
import { detectIDEs } from './detection/ide-detector.js';
|
|
24
24
|
import { IDEInfo } from './detection/ide-detector.js';
|
|
25
25
|
import { ProviderLoader } from './providers/provider-loader.js';
|
|
26
|
+
import type { ProviderModule } from './providers/contracts.js';
|
|
26
27
|
|
|
27
28
|
// ─── Provider-based dynamic IDE infrastructure ────────────────
|
|
28
29
|
// Reads cdpPorts, processNames from provider.js — only create provider.js to add new IDE
|
|
@@ -50,6 +51,26 @@ function getWinProcessNames(): Record<string, string[]> {
|
|
|
50
51
|
return getProviderLoader().getWinProcessNames();
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
function getProviderMeta(ideId: string): ProviderModule | undefined {
|
|
55
|
+
return getProviderLoader().getMeta(ideId);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getPreferredLaunchMethod(ideId: string, platform: NodeJS.Platform): 'auto' | 'cli' | 'app' {
|
|
59
|
+
const prefer = getProviderMeta(ideId)?.launch?.prefer;
|
|
60
|
+
const value = prefer?.[platform];
|
|
61
|
+
return value === 'cli' || value === 'app' || value === 'auto' ? value : 'auto';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getCdpStartupTimeoutMs(ideId: string): number {
|
|
65
|
+
const value = getProviderMeta(ideId)?.launch?.cdpStartupTimeoutMs;
|
|
66
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return 15000;
|
|
67
|
+
return Math.max(1000, Math.floor(value));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function escapeForAppleScript(value: string): string {
|
|
71
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
72
|
+
}
|
|
73
|
+
|
|
53
74
|
// ─── Helpers ────────────────────────────────────
|
|
54
75
|
|
|
55
76
|
/** Find available port (primary → secondary → sequential after) */
|
|
@@ -110,11 +131,11 @@ export async function killIdeProcess(ideId: string): Promise<boolean> {
|
|
|
110
131
|
if (plat === 'darwin' && appName) {
|
|
111
132
|
// macOS: graceful quit via osascript
|
|
112
133
|
try {
|
|
113
|
-
execSync(`osascript -e 'tell application "${appName}" to quit' 2>/dev/null`, {
|
|
134
|
+
execSync(`osascript -e 'tell application "${escapeForAppleScript(appName)}" to quit' 2>/dev/null`, {
|
|
114
135
|
timeout: 5000,
|
|
115
136
|
});
|
|
116
137
|
} catch {
|
|
117
|
-
try { execSync(`pkill -
|
|
138
|
+
try { execSync(`pkill -x "${appName}" 2>/dev/null`, { timeout: 5000 }); } catch { }
|
|
118
139
|
}
|
|
119
140
|
} else if (plat === 'win32' && winProcesses) {
|
|
120
141
|
// Windows: taskkill for each process name
|
|
@@ -142,7 +163,7 @@ export async function killIdeProcess(ideId: string): Promise<boolean> {
|
|
|
142
163
|
|
|
143
164
|
// Force terminate retry
|
|
144
165
|
if (plat === 'darwin' && appName) {
|
|
145
|
-
try { execSync(`pkill -9 -
|
|
166
|
+
try { execSync(`pkill -9 -x "${appName}" 2>/dev/null`, { timeout: 5000 }); } catch { }
|
|
146
167
|
} else if (plat === 'win32' && winProcesses) {
|
|
147
168
|
for (const proc of winProcesses) {
|
|
148
169
|
try { execSync(`taskkill /IM "${proc}" /F 2>nul`); } catch { }
|
|
@@ -165,8 +186,23 @@ export function isIdeRunning(ideId: string): boolean {
|
|
|
165
186
|
if (plat === 'darwin') {
|
|
166
187
|
const appName = getMacAppIdentifiers()[ideId];
|
|
167
188
|
if (!appName) return false;
|
|
168
|
-
|
|
169
|
-
|
|
189
|
+
try {
|
|
190
|
+
const result = execSync(`pgrep -x "${appName}" 2>/dev/null`, {
|
|
191
|
+
encoding: 'utf-8',
|
|
192
|
+
timeout: 3000,
|
|
193
|
+
});
|
|
194
|
+
return result.trim().length > 0;
|
|
195
|
+
} catch {
|
|
196
|
+
const result = execSync(
|
|
197
|
+
`osascript -e 'tell application "System Events" to count (every process whose name is "${escapeForAppleScript(appName)}")'`,
|
|
198
|
+
{
|
|
199
|
+
encoding: 'utf-8',
|
|
200
|
+
timeout: 3000,
|
|
201
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
202
|
+
},
|
|
203
|
+
);
|
|
204
|
+
return Number.parseInt(result.trim() || '0', 10) > 0;
|
|
205
|
+
}
|
|
170
206
|
} else if (plat === 'win32') {
|
|
171
207
|
const winProcesses = getWinProcessNames()[ideId];
|
|
172
208
|
if (!winProcesses) return false;
|
|
@@ -350,7 +386,8 @@ export async function launchWithCdp(options: LaunchOptions = {}): Promise<Launch
|
|
|
350
386
|
|
|
351
387
|
// Wait for CDP to enable (max 15 seconds)
|
|
352
388
|
let cdpReady = false;
|
|
353
|
-
|
|
389
|
+
const waitDeadline = Date.now() + getCdpStartupTimeoutMs(targetIde.id);
|
|
390
|
+
while (Date.now() < waitDeadline) {
|
|
354
391
|
await new Promise(r => setTimeout(r, 500));
|
|
355
392
|
if (await isCdpActive(port)) {
|
|
356
393
|
cdpReady = true;
|
|
@@ -378,18 +415,27 @@ export async function launchWithCdp(options: LaunchOptions = {}): Promise<Launch
|
|
|
378
415
|
|
|
379
416
|
async function launchMacOS(ide: IDEInfo, port: number, workspace?: string, newWindow?: boolean): Promise<void> {
|
|
380
417
|
const appName = getMacAppIdentifiers()[ide.id];
|
|
418
|
+
const preferredMethod = getPreferredLaunchMethod(ide.id, 'darwin');
|
|
381
419
|
|
|
382
420
|
const args = ['--remote-debugging-port=' + port];
|
|
383
421
|
if (newWindow) args.push('--new-window');
|
|
384
422
|
if (workspace) args.push(workspace);
|
|
385
423
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
424
|
+
const canUseCli = !!ide.cliCommand;
|
|
425
|
+
const canUseAppLauncher = !!appName;
|
|
426
|
+
const useAppLauncher = preferredMethod === 'app'
|
|
427
|
+
? canUseAppLauncher
|
|
428
|
+
: preferredMethod === 'cli'
|
|
429
|
+
? false
|
|
430
|
+
: !canUseCli && canUseAppLauncher;
|
|
431
|
+
|
|
432
|
+
if (!useAppLauncher && ide.cliCommand) {
|
|
391
433
|
// CLI based execute
|
|
392
434
|
spawn(ide.cliCommand, args, { detached: true, stdio: 'ignore' }).unref();
|
|
435
|
+
} else if (appName) {
|
|
436
|
+
// Fallback to `open -a` when no CLI wrapper is available or the provider prefers it.
|
|
437
|
+
const openArgs = ['-a', appName, '--args', ...args];
|
|
438
|
+
spawn('open', openArgs, { detached: true, stdio: 'ignore' }).unref();
|
|
393
439
|
} else {
|
|
394
440
|
throw new Error(`No app identifier or CLI for ${ide.displayName}`);
|
|
395
441
|
}
|
|
@@ -16,9 +16,10 @@ import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
|
|
|
16
16
|
import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
|
|
17
17
|
import type { PtyTransportFactory } from '../cli-adapters/pty-transport.js';
|
|
18
18
|
import { StatusMonitor } from './status-monitor.js';
|
|
19
|
-
import { ChatHistoryWriter } from '../config/chat-history.js';
|
|
19
|
+
import { ChatHistoryWriter, readChatHistory } from '../config/chat-history.js';
|
|
20
20
|
import { LOG } from '../logging/logger.js';
|
|
21
21
|
import type { ChatMessage } from '../types.js';
|
|
22
|
+
import { extractProviderControlValues, normalizeProviderEffects } from './control-effects.js';
|
|
22
23
|
|
|
23
24
|
let CachedDatabaseSync: (new (path: string, options?: { readOnly?: boolean }) => {
|
|
24
25
|
prepare(sql: string): { get(...params: Array<string | number>): unknown };
|
|
@@ -54,6 +55,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
54
55
|
private generatingDebounceTimer: NodeJS.Timeout | null = null;
|
|
55
56
|
private generatingDebouncePending: { chatTitle: string; timestamp: number } | null = null;
|
|
56
57
|
private lastApprovalEventAt = 0;
|
|
58
|
+
private controlValues: Record<string, string | number | boolean> = {};
|
|
59
|
+
private appliedEffectKeys = new Set<string>();
|
|
57
60
|
private historyWriter: ChatHistoryWriter;
|
|
58
61
|
private runtimeMessages: Array<{ key: string; message: ChatMessage }> = [];
|
|
59
62
|
readonly instanceId: string;
|
|
@@ -106,6 +109,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
106
109
|
async init(context: InstanceContext): Promise<void> {
|
|
107
110
|
this.context = context;
|
|
108
111
|
this.settings = context.settings || {};
|
|
112
|
+
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
109
113
|
this.monitor.updateConfig({
|
|
110
114
|
approvalAlert: this.settings.approvalAlert !== false,
|
|
111
115
|
longGeneratingAlert: this.settings.longGeneratingAlert !== false,
|
|
@@ -129,6 +133,21 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
129
133
|
|
|
130
134
|
// PTY spawn
|
|
131
135
|
await this.adapter.spawn();
|
|
136
|
+
if (this.providerSessionId) {
|
|
137
|
+
const restoredHistory = readChatHistory(this.type, 0, 200, this.providerSessionId);
|
|
138
|
+
if (restoredHistory.messages.length > 0) {
|
|
139
|
+
this.adapter.seedCommittedMessages(
|
|
140
|
+
restoredHistory.messages.map((message) => ({
|
|
141
|
+
role: message.role,
|
|
142
|
+
content: message.content,
|
|
143
|
+
timestamp: message.receivedAt,
|
|
144
|
+
receivedAt: message.receivedAt,
|
|
145
|
+
kind: message.kind,
|
|
146
|
+
senderName: message.senderName,
|
|
147
|
+
})),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
132
151
|
if (this.providerSessionId && this.launchMode === 'resume') {
|
|
133
152
|
const resumedAt = Date.now();
|
|
134
153
|
this.historyWriter.appendSystemMarker(
|
|
@@ -228,6 +247,12 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
228
247
|
}
|
|
229
248
|
const runtime = this.adapter.getRuntimeMetadata();
|
|
230
249
|
const parsedMessages = Array.isArray(parsedStatus?.messages) ? parsedStatus.messages : [];
|
|
250
|
+
const controlValues = extractProviderControlValues(this.provider.controls, parsedStatus);
|
|
251
|
+
if (controlValues) {
|
|
252
|
+
this.controlValues = controlValues;
|
|
253
|
+
} else if (Object.keys(this.controlValues).length > 0) {
|
|
254
|
+
this.controlValues = {};
|
|
255
|
+
}
|
|
231
256
|
const mergedMessages = this.mergeConversationMessages(parsedMessages);
|
|
232
257
|
|
|
233
258
|
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
@@ -251,6 +276,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
251
276
|
}
|
|
252
277
|
}
|
|
253
278
|
|
|
279
|
+
this.applyProviderResponse(parsedStatus, { phase: 'immediate' });
|
|
280
|
+
|
|
254
281
|
return {
|
|
255
282
|
type: this.type,
|
|
256
283
|
name: this.provider.name,
|
|
@@ -280,7 +307,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
280
307
|
attachedClients: runtime.attachedClients || [],
|
|
281
308
|
} : undefined,
|
|
282
309
|
resume: this.provider.resume,
|
|
283
|
-
controlValues:
|
|
310
|
+
controlValues: this.controlValues,
|
|
284
311
|
providerControls: this.provider.controls as any,
|
|
285
312
|
};
|
|
286
313
|
}
|
|
@@ -294,6 +321,16 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
294
321
|
return this.presentationMode;
|
|
295
322
|
}
|
|
296
323
|
|
|
324
|
+
updateSettings(newSettings: Record<string, any>): void {
|
|
325
|
+
this.settings = { ...newSettings };
|
|
326
|
+
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
327
|
+
this.monitor.updateConfig({
|
|
328
|
+
approvalAlert: this.settings.approvalAlert !== false,
|
|
329
|
+
longGeneratingAlert: this.settings.longGeneratingAlert !== false,
|
|
330
|
+
longGeneratingThresholdSec: this.settings.longGeneratingThresholdSec || 180,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
297
334
|
onEvent(event: string, data?: any): void {
|
|
298
335
|
if (event === 'send_message' && data?.text) {
|
|
299
336
|
void this.adapter.sendMessage(data.text).catch((e: any) => {
|
|
@@ -305,12 +342,15 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
305
342
|
void this.adapter.resolveAction(data).catch((e: any) => {
|
|
306
343
|
LOG.warn('CLI', `[${this.type}] resolve_action failed: ${e?.message || e}`);
|
|
307
344
|
});
|
|
345
|
+
} else if (event === 'provider_state_patch' && data && typeof data === 'object') {
|
|
346
|
+
this.applyProviderResponse(data, { phase: 'immediate' });
|
|
308
347
|
}
|
|
309
348
|
}
|
|
310
349
|
|
|
311
350
|
dispose(): void {
|
|
312
351
|
this.adapter.shutdown();
|
|
313
352
|
this.monitor.reset();
|
|
353
|
+
this.appliedEffectKeys.clear();
|
|
314
354
|
}
|
|
315
355
|
|
|
316
356
|
private completedDebounceTimer: NodeJS.Timeout | null = null;
|
|
@@ -319,6 +359,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
319
359
|
private detectStatusTransition(): void {
|
|
320
360
|
const now = Date.now();
|
|
321
361
|
const adapterStatus = this.adapter.getStatus();
|
|
362
|
+
const parsedStatus = this.adapter.getScriptParsedStatus?.() || null;
|
|
322
363
|
const newStatus = adapterStatus.status;
|
|
323
364
|
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
324
365
|
const chatTitle = `${this.provider.name} · ${dirName}`;
|
|
@@ -327,6 +368,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
327
368
|
? `${partial || ''}::${adapterStatus.messages.at(-1)?.content || ''}`.slice(-2000)
|
|
328
369
|
: undefined;
|
|
329
370
|
|
|
371
|
+
const previousStatus = this.lastStatus;
|
|
330
372
|
if (newStatus !== this.lastStatus) {
|
|
331
373
|
LOG.info('CLI', `[${this.type}] status: ${this.lastStatus} → ${newStatus}`);
|
|
332
374
|
if (this.lastStatus === 'idle' && newStatus === 'generating') {
|
|
@@ -411,6 +453,12 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
411
453
|
this.lastStatus = newStatus;
|
|
412
454
|
}
|
|
413
455
|
|
|
456
|
+
this.applyProviderResponse(parsedStatus, {
|
|
457
|
+
phase: (newStatus === 'idle' && (previousStatus === 'generating' || previousStatus === 'waiting_approval'))
|
|
458
|
+
? 'turn_completed'
|
|
459
|
+
: 'immediate',
|
|
460
|
+
});
|
|
461
|
+
|
|
414
462
|
// Monitor check (cooldown based notification, IDE/CLI common)
|
|
415
463
|
const agentKey = `${this.type}:cli`;
|
|
416
464
|
const monitorEvents = this.monitor.check(agentKey, newStatus, now, progressFingerprint);
|
|
@@ -431,6 +479,104 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
431
479
|
return events;
|
|
432
480
|
}
|
|
433
481
|
|
|
482
|
+
private applyProviderResponse(data: any, options: { phase: 'immediate' | 'turn_completed' }): void {
|
|
483
|
+
if (!data || typeof data !== 'object') return;
|
|
484
|
+
|
|
485
|
+
const controlValues = extractProviderControlValues(this.provider.controls, data);
|
|
486
|
+
if (controlValues) {
|
|
487
|
+
this.controlValues = { ...this.controlValues, ...controlValues };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const effects = normalizeProviderEffects(data);
|
|
491
|
+
for (const effect of effects) {
|
|
492
|
+
const effectWhen = effect.when || 'immediate';
|
|
493
|
+
if (effectWhen === 'turn_completed' && options.phase !== 'turn_completed') continue;
|
|
494
|
+
if (effectWhen === 'immediate' && options.phase === 'turn_completed') continue;
|
|
495
|
+
|
|
496
|
+
const effectKey = this.getEffectDedupKey(effect);
|
|
497
|
+
if (this.appliedEffectKeys.has(effectKey)) continue;
|
|
498
|
+
this.appliedEffectKeys.add(effectKey);
|
|
499
|
+
|
|
500
|
+
if (effect.persist !== false) {
|
|
501
|
+
const persisted = this.getPersistedEffectContent(effect);
|
|
502
|
+
if (persisted) this.appendRuntimeSystemMessage(persisted, effectKey);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (effect.type === 'message' && effect.message) {
|
|
506
|
+
const content = typeof effect.message.content === 'string'
|
|
507
|
+
? effect.message.content
|
|
508
|
+
: JSON.stringify(effect.message.content);
|
|
509
|
+
this.pushEvent({
|
|
510
|
+
event: 'provider:message',
|
|
511
|
+
timestamp: Date.now(),
|
|
512
|
+
content,
|
|
513
|
+
role: effect.message.role || 'system',
|
|
514
|
+
kind: effect.message.kind,
|
|
515
|
+
senderName: effect.message.senderName,
|
|
516
|
+
});
|
|
517
|
+
} else if (effect.type === 'toast' && effect.toast) {
|
|
518
|
+
this.pushEvent({
|
|
519
|
+
event: 'provider:toast',
|
|
520
|
+
effectId: effect.id || effectKey,
|
|
521
|
+
timestamp: Date.now(),
|
|
522
|
+
message: effect.toast.message,
|
|
523
|
+
level: effect.toast.level || 'info',
|
|
524
|
+
});
|
|
525
|
+
} else if (effect.type === 'notification' && effect.notification) {
|
|
526
|
+
this.pushEvent({
|
|
527
|
+
event: 'provider:notification',
|
|
528
|
+
effectId: effect.id || effectKey,
|
|
529
|
+
timestamp: Date.now(),
|
|
530
|
+
title: effect.notification.title,
|
|
531
|
+
message: effect.notification.body,
|
|
532
|
+
content: typeof effect.notification.bubbleContent === 'string'
|
|
533
|
+
? effect.notification.bubbleContent
|
|
534
|
+
: effect.notification.body,
|
|
535
|
+
level: effect.notification.level || 'info',
|
|
536
|
+
channels: effect.notification.channels || ['toast'],
|
|
537
|
+
preferenceKey: effect.notification.preferenceKey,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (this.appliedEffectKeys.size > 200) {
|
|
543
|
+
this.appliedEffectKeys = new Set(Array.from(this.appliedEffectKeys).slice(-100));
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
private getEffectDedupKey(effect: { id?: string; type: string; message?: { content?: unknown }; toast?: { message?: string }; notification?: { title?: string; body?: string } }): string {
|
|
548
|
+
if (effect.id) return `provider_effect:${effect.id}`;
|
|
549
|
+
if (effect.type === 'message') {
|
|
550
|
+
const content = typeof effect.message?.content === 'string'
|
|
551
|
+
? effect.message.content
|
|
552
|
+
: JSON.stringify(effect.message?.content || '');
|
|
553
|
+
return `provider_effect:message:${content}`;
|
|
554
|
+
}
|
|
555
|
+
if (effect.type === 'notification') {
|
|
556
|
+
return `provider_effect:notification:${effect.notification?.title || ''}:${effect.notification?.body || ''}`;
|
|
557
|
+
}
|
|
558
|
+
return `provider_effect:toast:${effect.toast?.message || ''}`;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private getPersistedEffectContent(effect: { type: string; message?: { content?: unknown }; toast?: { message?: string }; notification?: { title?: string; body?: string; bubbleContent?: unknown } }): string | null {
|
|
562
|
+
if (effect.type === 'message') {
|
|
563
|
+
return typeof effect.message?.content === 'string'
|
|
564
|
+
? effect.message.content
|
|
565
|
+
: JSON.stringify(effect.message?.content || '');
|
|
566
|
+
}
|
|
567
|
+
if (effect.type === 'toast') {
|
|
568
|
+
return effect.toast?.message || null;
|
|
569
|
+
}
|
|
570
|
+
if (effect.type === 'notification') {
|
|
571
|
+
if (typeof effect.notification?.bubbleContent === 'string') return effect.notification.bubbleContent;
|
|
572
|
+
if (typeof effect.notification?.title === 'string' && effect.notification.title.trim()) {
|
|
573
|
+
return `${effect.notification.title}\n${effect.notification.body || ''}`.trim();
|
|
574
|
+
}
|
|
575
|
+
return effect.notification?.body || null;
|
|
576
|
+
}
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
|
|
434
580
|
// ─── Adapter access (backward compat) ──────────────────
|
|
435
581
|
|
|
436
582
|
getAdapter(): ProviderCliAdapter {
|
|
@@ -26,6 +26,10 @@ export interface ReadChatResult {
|
|
|
26
26
|
inputContent?: string;
|
|
27
27
|
model?: string;
|
|
28
28
|
autoApprove?: string;
|
|
29
|
+
/** Explicit dynamic control values returned by the provider */
|
|
30
|
+
controlValues?: Record<string, string | number | boolean>;
|
|
31
|
+
/** Provider-driven UI effects derived from chat state */
|
|
32
|
+
effects?: ProviderEffect[];
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
import type { ChatMessage } from '../types.js';
|
|
@@ -46,6 +50,43 @@ export interface ModalInfo {
|
|
|
46
50
|
height?: number;
|
|
47
51
|
}
|
|
48
52
|
|
|
53
|
+
export interface ProviderEffectMessage {
|
|
54
|
+
role?: 'system' | 'assistant' | 'user';
|
|
55
|
+
content: string | ContentBlock[];
|
|
56
|
+
kind?: string;
|
|
57
|
+
senderName?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ProviderEffectToast {
|
|
61
|
+
level?: 'info' | 'success' | 'warning';
|
|
62
|
+
message: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type ProviderNotificationPreferenceKey = 'disconnect' | 'completion' | 'approval' | 'browser';
|
|
66
|
+
export type ProviderNotificationChannel = 'bubble' | 'toast' | 'browser';
|
|
67
|
+
|
|
68
|
+
export interface ProviderEffectNotification {
|
|
69
|
+
title?: string;
|
|
70
|
+
body: string;
|
|
71
|
+
level?: 'info' | 'success' | 'warning';
|
|
72
|
+
channels?: ProviderNotificationChannel[];
|
|
73
|
+
preferenceKey?: ProviderNotificationPreferenceKey;
|
|
74
|
+
bubbleContent?: string | ContentBlock[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ProviderEffect {
|
|
78
|
+
type: 'message' | 'toast' | 'notification';
|
|
79
|
+
/** Stable dedup key; falls back to a content hash when omitted */
|
|
80
|
+
id?: string;
|
|
81
|
+
/** Default immediate. turn_completed fires only on generating/waiting -> idle transitions. */
|
|
82
|
+
when?: 'immediate' | 'turn_completed';
|
|
83
|
+
/** Default true. False keeps the effect UI-only. */
|
|
84
|
+
persist?: boolean;
|
|
85
|
+
message?: ProviderEffectMessage;
|
|
86
|
+
toast?: ProviderEffectToast;
|
|
87
|
+
notification?: ProviderEffectNotification;
|
|
88
|
+
}
|
|
89
|
+
|
|
49
90
|
// ─── Rich Content Types (ACP Standard) ─────────────────
|
|
50
91
|
// Based on ACP SDK v0.16.1 schema types.
|
|
51
92
|
// All provider categories (ACP, IDE, Extension, CLI) use these as output standard.
|
|
@@ -290,13 +331,35 @@ export interface ProviderModule {
|
|
|
290
331
|
versionCommand?: string;
|
|
291
332
|
/** Versions tested by provider maintainer (informational) */
|
|
292
333
|
testedVersions?: string[];
|
|
293
|
-
|
|
334
|
+
/** Per-OS process names — used by launch.ts to detect/kill IDE processes */
|
|
294
335
|
processNames?: {
|
|
295
336
|
darwin?: string;
|
|
296
337
|
win32?: string[];
|
|
297
338
|
linux?: string[];
|
|
298
339
|
[key: string]: string | string[] | undefined;
|
|
299
340
|
};
|
|
341
|
+
/**
|
|
342
|
+
* IDE launch preferences.
|
|
343
|
+
* Lets each provider choose how its GUI app should be started per platform.
|
|
344
|
+
*/
|
|
345
|
+
launch?: {
|
|
346
|
+
/**
|
|
347
|
+
* Preferred launch method by platform.
|
|
348
|
+
* - 'cli': use the IDE CLI wrapper/binary
|
|
349
|
+
* - 'app': use platform app launcher (e.g. `open -a` on macOS)
|
|
350
|
+
* - 'auto': let core choose a sensible default
|
|
351
|
+
*/
|
|
352
|
+
prefer?: {
|
|
353
|
+
darwin?: 'auto' | 'cli' | 'app';
|
|
354
|
+
win32?: 'auto' | 'cli' | 'app';
|
|
355
|
+
linux?: 'auto' | 'cli' | 'app';
|
|
356
|
+
[key: string]: 'auto' | 'cli' | 'app' | undefined;
|
|
357
|
+
};
|
|
358
|
+
/**
|
|
359
|
+
* Override how long core waits for CDP to come up after launch.
|
|
360
|
+
*/
|
|
361
|
+
cdpStartupTimeoutMs?: number;
|
|
362
|
+
};
|
|
300
363
|
/** Per-OS install paths — used by detector.ts to detect IDE installation */
|
|
301
364
|
paths?: {
|
|
302
365
|
darwin?: string[];
|
|
@@ -580,7 +643,7 @@ export interface ProviderSettingSchema extends ProviderSettingDef {
|
|
|
580
643
|
* - 'slider' — numeric range (temperature: 0–2)
|
|
581
644
|
* - 'action' — one-shot button (show usage, restart, clear context)
|
|
582
645
|
*/
|
|
583
|
-
export type ProviderControlType = 'select' | 'toggle' | 'cycle' | 'slider' | 'action';
|
|
646
|
+
export type ProviderControlType = 'select' | 'toggle' | 'cycle' | 'slider' | 'action' | 'display';
|
|
584
647
|
|
|
585
648
|
/**
|
|
586
649
|
* Where the control appears in the chat UI:
|