@adhdev/daemon-core 0.8.21 → 0.8.23
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 +1163 -314
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1163 -314
- 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/chat-commands.ts +33 -12
- 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
|
@@ -90,6 +90,31 @@ function isRecentDuplicateSend(key: string): boolean {
|
|
|
90
90
|
return false;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
function parseMaybeJson(value: any): any {
|
|
94
|
+
if (typeof value !== 'string') return value;
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(value);
|
|
97
|
+
} catch {
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function didProviderConfirmSend(result: any): boolean {
|
|
103
|
+
const parsed = parseMaybeJson(result);
|
|
104
|
+
if (parsed === true) return true;
|
|
105
|
+
if (typeof parsed === 'string') {
|
|
106
|
+
const normalized = parsed.trim().toLowerCase();
|
|
107
|
+
return normalized === 'ok' || normalized === 'sent' || normalized === 'success' || normalized === 'true';
|
|
108
|
+
}
|
|
109
|
+
if (!parsed || typeof parsed !== 'object') return false;
|
|
110
|
+
|
|
111
|
+
return parsed.sent === true
|
|
112
|
+
|| parsed.success === true
|
|
113
|
+
|| parsed.ok === true
|
|
114
|
+
|| parsed.submitted === true
|
|
115
|
+
|| parsed.dispatched === true;
|
|
116
|
+
}
|
|
117
|
+
|
|
93
118
|
export async function handleChatHistory(h: CommandHelpers, args: any): Promise<CommandResult> {
|
|
94
119
|
const { agentType, offset, limit } = args;
|
|
95
120
|
const historySessionId = getHistorySessionId(h, args);
|
|
@@ -280,9 +305,8 @@ export async function handleSendChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
280
305
|
try {
|
|
281
306
|
const evalResult = await h.evaluateProviderScript('sendMessage', { MESSAGE: text }, 30000);
|
|
282
307
|
if (evalResult?.result) {
|
|
283
|
-
|
|
284
|
-
if (
|
|
285
|
-
if (parsed?.sent) {
|
|
308
|
+
const parsed = parseMaybeJson(evalResult.result);
|
|
309
|
+
if (didProviderConfirmSend(parsed)) {
|
|
286
310
|
_log(`Extension script sent OK`);
|
|
287
311
|
return _logSendSuccess('extension-script');
|
|
288
312
|
}
|
|
@@ -318,9 +342,8 @@ export async function handleSendChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
318
342
|
if (sendScript) {
|
|
319
343
|
try {
|
|
320
344
|
const result = await targetCdp.evaluate(sendScript, 30000);
|
|
321
|
-
|
|
322
|
-
if (
|
|
323
|
-
if (parsed?.sent) {
|
|
345
|
+
const parsed: any = parseMaybeJson(result);
|
|
346
|
+
if (didProviderConfirmSend(parsed)) {
|
|
324
347
|
_log(`sendMessage script OK`);
|
|
325
348
|
return _logSendSuccess('script');
|
|
326
349
|
}
|
|
@@ -365,9 +388,8 @@ export async function handleSendChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
365
388
|
const matchText = provider.webviewMatchText;
|
|
366
389
|
const matchFn = matchText ? (body: string) => body.includes(matchText) : undefined;
|
|
367
390
|
const wvResult = await targetCdp.evaluateInWebviewFrame(webviewScript, matchFn);
|
|
368
|
-
|
|
369
|
-
if (
|
|
370
|
-
if (wvParsed?.sent) {
|
|
391
|
+
const wvParsed: any = parseMaybeJson(wvResult);
|
|
392
|
+
if (didProviderConfirmSend(wvParsed)) {
|
|
371
393
|
_log(`webviewSendMessage OK`);
|
|
372
394
|
return _logSendSuccess('webview-script');
|
|
373
395
|
}
|
|
@@ -390,9 +412,8 @@ export async function handleSendChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
390
412
|
const matchText = provider.webviewMatchText;
|
|
391
413
|
const matchFn = matchText ? (body: string) => body.includes(matchText) : undefined;
|
|
392
414
|
const wvResult = await targetCdp.evaluateInWebviewFrame(webviewScript, matchFn);
|
|
393
|
-
|
|
394
|
-
if (
|
|
395
|
-
if (wvParsed?.sent) {
|
|
415
|
+
const wvParsed: any = parseMaybeJson(wvResult);
|
|
416
|
+
if (didProviderConfirmSend(wvParsed)) {
|
|
396
417
|
_log(`webviewSendMessage OK`);
|
|
397
418
|
return _logSendSuccess('webview-script');
|
|
398
419
|
}
|
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 {
|