@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.
Files changed (36) hide show
  1. package/dist/agent-stream/types.d.ts +3 -0
  2. package/dist/cli-adapter-types.d.ts +3 -0
  3. package/dist/cli-adapters/provider-cli-adapter.d.ts +71 -11
  4. package/dist/commands/stream-commands.d.ts +1 -0
  5. package/dist/config/config.d.ts +6 -0
  6. package/dist/index.js +1163 -314
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +1163 -314
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/providers/cli-provider-instance.d.ts +6 -0
  11. package/dist/providers/contracts.d.ts +59 -1
  12. package/dist/providers/control-effects.d.ts +4 -0
  13. package/dist/providers/extension-provider-instance.d.ts +9 -0
  14. package/dist/providers/ide-provider-instance.d.ts +8 -0
  15. package/dist/shared-types.d.ts +2 -1
  16. package/node_modules/@adhdev/session-host-core/package.json +1 -1
  17. package/package.json +3 -2
  18. package/src/agent-stream/forward.ts +2 -0
  19. package/src/agent-stream/provider-adapter.ts +5 -15
  20. package/src/agent-stream/types.ts +4 -0
  21. package/src/cli-adapter-types.ts +3 -0
  22. package/src/cli-adapters/provider-cli-adapter.ts +399 -49
  23. package/src/commands/chat-commands.ts +33 -12
  24. package/src/commands/handler.ts +1 -0
  25. package/src/commands/stream-commands.ts +99 -8
  26. package/src/config/config.d.ts +1 -0
  27. package/src/config/config.ts +9 -0
  28. package/src/launch.ts +57 -11
  29. package/src/providers/cli-provider-instance.ts +148 -2
  30. package/src/providers/contracts.ts +65 -2
  31. package/src/providers/control-effects.ts +114 -0
  32. package/src/providers/extension-provider-instance.ts +163 -3
  33. package/src/providers/ide-provider-instance.ts +181 -2
  34. package/src/shared-types.d.ts +1 -0
  35. package/src/shared-types.ts +2 -1
  36. 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
- let parsed = evalResult.result;
284
- if (typeof parsed === 'string') { try { parsed = JSON.parse(parsed); } catch { } }
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
- let parsed: any = result;
322
- if (typeof result === 'string') { try { parsed = JSON.parse(result); } catch { } }
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
- let wvParsed: any = wvResult;
369
- if (typeof wvResult === 'string') { try { wvParsed = JSON.parse(wvResult); } catch { } }
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
- let wvParsed: any = wvResult;
394
- if (typeof wvResult === 'string') { try { wvParsed = JSON.parse(wvResult); } catch { } }
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
  }
@@ -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
- export async function handleExtensionScript(h: CommandHelpers, args: any, scriptName: string): Promise<CommandResult> {
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 scriptFn = provider.scripts[actualScriptName as keyof typeof provider.scripts] as Function;
119
- // Normalize args: script placeholders use UPPERCASE (${MODE}, ${MODEL}, ${MESSAGE})
120
- // but WebSocket args typically use lowercase. Add uppercase versions of common keys.
121
- const normalizedArgs = { ...args };
122
- for (const key of ['mode', 'model', 'message', 'action', 'button', 'text', 'sessionId']) {
123
- if (key in normalizedArgs && !(key.toUpperCase() in normalizedArgs)) {
124
- normalizedArgs[key.toUpperCase()] = normalizedArgs[key];
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 {
@@ -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;
@@ -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 -f "${appName}" 2>/dev/null`); } catch { }
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 -f "${appName}" 2>/dev/null`); } catch { }
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
- const result = execSync(`pgrep -f "${appName}" 2>/dev/null`, { encoding: 'utf-8' });
169
- return result.trim().length > 0;
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
- for (let i = 0; i < 30; i++) {
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
- if (appName) {
387
- // 'open -a' execution (ensures GUI session)
388
- const openArgs = ['-a', appName, '--args', ...args];
389
- spawn('open', openArgs, { detached: true, stdio: 'ignore' }).unref();
390
- } else if (ide.cliCommand) {
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: undefined, // CLI controls not yet wired from stream
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 {