@adhdev/daemon-core 0.8.57 → 0.8.59

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 (75) hide show
  1. package/dist/agent-stream/types.d.ts +3 -4
  2. package/dist/boot/daemon-lifecycle.d.ts +1 -0
  3. package/dist/commands/handler.d.ts +1 -0
  4. package/dist/commands/router.d.ts +1 -0
  5. package/dist/commands/stream-commands.d.ts +3 -0
  6. package/dist/config/chat-history.d.ts +3 -0
  7. package/dist/config/config.d.ts +3 -0
  8. package/dist/config/provider-source-config.d.ts +23 -0
  9. package/dist/config/recent-activity.d.ts +2 -1
  10. package/dist/config/saved-sessions.d.ts +2 -1
  11. package/dist/daemon/dev-server-types.d.ts +1 -0
  12. package/dist/daemon/dev-server.d.ts +4 -0
  13. package/dist/index.d.ts +3 -1
  14. package/dist/index.js +876 -337
  15. package/dist/index.js.map +1 -1
  16. package/dist/index.mjs +875 -337
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/providers/acp-provider-instance.d.ts +8 -2
  19. package/dist/providers/cli-provider-instance.d.ts +10 -0
  20. package/dist/providers/contracts.d.ts +4 -2
  21. package/dist/providers/extension-provider-instance.d.ts +1 -2
  22. package/dist/providers/provider-instance.d.ts +3 -4
  23. package/dist/providers/provider-loader.d.ts +14 -1
  24. package/dist/providers/provider-patch-state.d.ts +23 -0
  25. package/dist/providers/summary-metadata.d.ts +22 -0
  26. package/dist/shared-types.d.ts +15 -9
  27. package/dist/status/snapshot.d.ts +16 -1
  28. package/node_modules/@adhdev/session-host-core/package.json +1 -1
  29. package/package.json +1 -1
  30. package/src/agent-stream/forward.ts +1 -2
  31. package/src/agent-stream/manager.ts +2 -1
  32. package/src/agent-stream/provider-adapter.ts +7 -3
  33. package/src/agent-stream/types.d.ts +3 -4
  34. package/src/agent-stream/types.ts +3 -4
  35. package/src/boot/daemon-lifecycle.ts +13 -3
  36. package/src/commands/cli-manager.ts +10 -5
  37. package/src/commands/handler.ts +3 -0
  38. package/src/commands/router.ts +160 -26
  39. package/src/commands/stream-commands.ts +60 -2
  40. package/src/config/chat-history.ts +39 -0
  41. package/src/config/config.d.ts +3 -0
  42. package/src/config/config.ts +19 -3
  43. package/src/config/provider-source-config.ts +42 -0
  44. package/src/config/recent-activity.d.ts +2 -1
  45. package/src/config/recent-activity.ts +12 -1
  46. package/src/config/saved-sessions.d.ts +2 -1
  47. package/src/config/saved-sessions.ts +12 -2
  48. package/src/daemon/dev-auto-implement.ts +1 -14
  49. package/src/daemon/dev-cli-debug.ts +0 -1
  50. package/src/daemon/dev-server-types.ts +1 -0
  51. package/src/daemon/dev-server.ts +46 -21
  52. package/src/daemon/scaffold-template.ts +8 -1
  53. package/src/index.d.ts +1 -1
  54. package/src/index.ts +4 -0
  55. package/src/providers/acp-provider-instance.d.ts +8 -2
  56. package/src/providers/acp-provider-instance.ts +80 -23
  57. package/src/providers/cli-provider-instance.ts +42 -22
  58. package/src/providers/contracts.d.ts +3 -2
  59. package/src/providers/contracts.ts +7 -4
  60. package/src/providers/control-effects.ts +3 -4
  61. package/src/providers/extension-provider-instance.d.ts +1 -2
  62. package/src/providers/extension-provider-instance.ts +26 -14
  63. package/src/providers/ide-provider-instance.ts +28 -15
  64. package/src/providers/provider-instance.d.ts +3 -4
  65. package/src/providers/provider-instance.ts +6 -7
  66. package/src/providers/provider-loader.d.ts +4 -1
  67. package/src/providers/provider-loader.ts +61 -23
  68. package/src/providers/provider-patch-state.ts +91 -0
  69. package/src/providers/provider-schema.ts +3 -0
  70. package/src/providers/summary-metadata.ts +118 -0
  71. package/src/shared-types.d.ts +15 -9
  72. package/src/shared-types.ts +17 -9
  73. package/src/status/builders.ts +18 -13
  74. package/src/status/reporter.ts +2 -4
  75. package/src/status/snapshot.ts +60 -2
@@ -1,6 +1,8 @@
1
1
  import * as path from 'path';
2
+ import type { ProviderSummaryMetadata } from '../shared-types.js';
2
3
  import type { DaemonState } from './state-store.js';
3
4
  import { expandPath } from './workspaces.js';
5
+ import { normalizePersistedSummaryMetadata } from '../providers/summary-metadata.js';
4
6
 
5
7
  export interface SavedProviderSessionEntry {
6
8
  id: string;
@@ -9,7 +11,7 @@ export interface SavedProviderSessionEntry {
9
11
  providerName: string;
10
12
  providerSessionId: string;
11
13
  workspace?: string | null;
12
- currentModel?: string;
14
+ summaryMetadata?: ProviderSummaryMetadata;
13
15
  title?: string;
14
16
  createdAt: number;
15
17
  lastUsedAt: number;
@@ -46,7 +48,9 @@ export function upsertSavedProviderSession(
46
48
  providerName: entry.providerName,
47
49
  providerSessionId,
48
50
  workspace: entry.workspace ? normalizeWorkspace(entry.workspace) : undefined,
49
- currentModel: entry.currentModel,
51
+ summaryMetadata: normalizePersistedSummaryMetadata({
52
+ summaryMetadata: entry.summaryMetadata,
53
+ }),
50
54
  title: entry.title,
51
55
  createdAt: existing?.createdAt || entry.createdAt || Date.now(),
52
56
  lastUsedAt: entry.lastUsedAt || Date.now(),
@@ -69,5 +73,11 @@ export function getSavedProviderSessions(
69
73
  if (filters?.kind && entry.kind !== filters.kind) return false;
70
74
  return true;
71
75
  })
76
+ .map(entry => ({
77
+ ...entry,
78
+ summaryMetadata: normalizePersistedSummaryMetadata({
79
+ summaryMetadata: entry.summaryMetadata,
80
+ }),
81
+ }))
72
82
  .sort((a, b) => b.lastUsedAt - a.lastUsedAt);
73
83
  }
@@ -141,19 +141,6 @@ export function resolveAutoImplWritableProviderDir(ctx: DevServerContext,
141
141
  return { dir: null, reason: `provider.json not found in writable provider directory: ${desiredDir}` };
142
142
  }
143
143
 
144
- try {
145
- const providerData = JSON.parse(fs.readFileSync(providerJson, 'utf-8'));
146
- if (providerData.disableUpstream !== true) {
147
- providerData.disableUpstream = true;
148
- fs.writeFileSync(providerJson, JSON.stringify(providerData, null, 2));
149
- }
150
- } catch (error) {
151
- return {
152
- dir: null,
153
- reason: `Failed to update provider.json in writable provider directory: ${(error as Error).message}`,
154
- };
155
- }
156
-
157
144
  return { dir: desiredDir };
158
145
  }
159
146
 
@@ -901,7 +888,7 @@ export function buildAutoImplPrompt(ctx: DevServerContext,
901
888
  lines.push('## Required Return Format');
902
889
  lines.push('| Function | Return JSON |');
903
890
  lines.push('|---|---|');
904
- lines.push('| readChat | `{ id, status, title, messages: [{role, content, index, kind?, meta?}], inputContent, activeModal }` — optional `kind`: standard, thought, tool, terminal; optional `meta`: e.g. `{ label, isRunning }` for dashboard |');
891
+ lines.push('| readChat | `{ id, status, title, messages: [{role, content, index, kind?, meta?}], inputContent, activeModal, controlValues?, summaryMetadata? }` — optional `kind`: standard, thought, tool, terminal; prefer explicit `controlValues` for current selections and `summaryMetadata` for compact always-visible UI metadata |');
905
892
  lines.push('| sendMessage | `{ sent: false, needsTypeAndSend: true, selector }` |');
906
893
  lines.push('| resolveAction | `{ resolved: true/false, clicked? }` |');
907
894
  lines.push('| listSessions | `{ sessions: [{ id, title, active, index }] }` |');
@@ -757,7 +757,6 @@ export async function handleCliStatus(ctx: DevServerContext, _req: http.Incoming
757
757
  lastMessage: s.activeChat?.messages?.slice(-1)[0] || null,
758
758
  activeModal: s.activeChat?.activeModal || null,
759
759
  pendingEvents: s.pendingEvents || [],
760
- currentModel: s.currentModel,
761
760
  settings: s.settings,
762
761
  }));
763
762
  ctx.json(res, 200, { instances: result, count: result.length });
@@ -20,6 +20,7 @@ export interface DevServerContext {
20
20
  readonly cdpManagers: Map<string, DaemonCdpManager>;
21
21
  readonly instanceManager: ProviderInstanceManager | null;
22
22
  readonly cliManager: DaemonCliManager | null;
23
+ readonly onProviderSourceConfigChanged?: (() => Promise<void> | void) | null;
23
24
 
24
25
  // Utilities
25
26
  getCdp(ideType?: string): DaemonCdpManager | null;
@@ -22,6 +22,8 @@ import * as os from 'os';
22
22
  import type { ProviderLoader } from '../providers/provider-loader.js';
23
23
  import type { ProviderCategory, ProviderModule, ProviderScripts, ProviderSettingDef } from '../providers/contracts.js';
24
24
  import { validateProviderDefinition } from '../providers/provider-schema.js';
25
+ import { loadConfig, saveConfig } from '../config/config.js';
26
+ import { parseProviderSourceConfigUpdate } from '../config/provider-source-config.js';
25
27
  import type { ChildProcess } from 'child_process';
26
28
  import type { DaemonCdpManager } from '../cdp/manager.js';
27
29
  import type { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
@@ -100,6 +102,7 @@ export class DevServer implements DevServerContext {
100
102
  public cdpManagers: Map<string, DaemonCdpManager>;
101
103
  public instanceManager: ProviderInstanceManager | null;
102
104
  public cliManager: DaemonCliManager | null;
105
+ public onProviderSourceConfigChanged: (() => Promise<void> | void) | null;
103
106
  private logFn: (msg: string) => void;
104
107
  private sseClients: http.ServerResponse[] = [];
105
108
  private watchScriptPath: string | null = null;
@@ -120,11 +123,13 @@ export class DevServer implements DevServerContext {
120
123
  instanceManager?: ProviderInstanceManager;
121
124
  cliManager?: DaemonCliManager;
122
125
  logFn?: (msg: string) => void;
126
+ onProviderSourceConfigChanged?: () => Promise<void> | void;
123
127
  }) {
124
128
  this.providerLoader = options.providerLoader;
125
129
  this.cdpManagers = options.cdpManagers;
126
130
  this.instanceManager = options.instanceManager || null;
127
131
  this.cliManager = options.cliManager || null;
132
+ this.onProviderSourceConfigChanged = options.onProviderSourceConfigChanged || null;
128
133
  this.logFn = options.logFn || LOG.forComponent('DevServer').asLogFn();
129
134
  }
130
135
 
@@ -140,6 +145,8 @@ export class DevServer implements DevServerContext {
140
145
  }[] = [
141
146
  // Static routes
142
147
  { method: 'GET', pattern: '/api/providers', handler: (q, s) => this.handleListProviders(q, s) },
148
+ { method: 'GET', pattern: '/api/providers/source-config', handler: (q, s) => this.handleGetProviderSourceConfig(q, s) },
149
+ { method: 'POST', pattern: '/api/providers/source-config', handler: (q, s) => this.handleSetProviderSourceConfig(q, s) },
143
150
  { method: 'GET', pattern: '/api/providers/versions', handler: (q, s) => this.handleDetectVersions(q, s) },
144
151
  { method: 'POST', pattern: '/api/providers/reload', handler: (q, s) => this.handleReload(q, s) },
145
152
  { method: 'POST', pattern: '/api/cdp/evaluate', handler: (q, s) => this.handleCdpEvaluate(q, s) },
@@ -273,7 +280,38 @@ export class DevServer implements DevServerContext {
273
280
 
274
281
  private async handleListProviders(_req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
275
282
  const providers = this.providerLoader.getAll().map(toProviderListEntry);
276
- this.json(res, 200, { providers, count: providers.length });
283
+ this.json(res, 200, { providers, count: providers.length, sourceConfig: this.providerLoader.getSourceConfig() });
284
+ }
285
+
286
+ private async handleGetProviderSourceConfig(_req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
287
+ this.json(res, 200, { success: true, sourceConfig: this.providerLoader.getSourceConfig() });
288
+ }
289
+
290
+ private async handleSetProviderSourceConfig(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
291
+ const body = await this.readBody(req);
292
+ const parsed = parseProviderSourceConfigUpdate(body || {});
293
+ if (!parsed.ok) {
294
+ this.json(res, 400, { success: false, error: parsed.error });
295
+ return;
296
+ }
297
+
298
+ const currentConfig = loadConfig();
299
+ const nextConfig = {
300
+ ...currentConfig,
301
+ ...(parsed.updates.providerSourceMode ? { providerSourceMode: parsed.updates.providerSourceMode } : {}),
302
+ ...(Object.prototype.hasOwnProperty.call(parsed.updates, 'providerDir') ? { providerDir: parsed.updates.providerDir } : {}),
303
+ };
304
+ saveConfig(nextConfig);
305
+
306
+ const sourceConfig = this.providerLoader.applySourceConfig({
307
+ sourceMode: nextConfig.providerSourceMode,
308
+ userDir: Object.prototype.hasOwnProperty.call(parsed.updates, 'providerDir') ? parsed.updates.providerDir : this.providerLoader.getSourceConfig().explicitProviderDir || undefined,
309
+ });
310
+ this.providerLoader.reload();
311
+ this.providerLoader.registerToDetector();
312
+ await this.onProviderSourceConfigChanged?.();
313
+
314
+ this.json(res, 200, { success: true, reloaded: true, sourceConfig });
277
315
  }
278
316
 
279
317
  private async handleProviderConfig(type: string, _req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
@@ -994,27 +1032,14 @@ export class DevServer implements DevServerContext {
994
1032
  this.log(`Auto-implement writable copy created: ${desiredDir}`);
995
1033
  }
996
1034
 
997
- const providerJson = path.join(desiredDir, 'provider.json');
998
- if (!fs.existsSync(providerJson)) {
999
- return { dir: null, reason: `provider.json not found in writable provider directory: ${desiredDir}` };
1000
- }
1001
-
1002
- try {
1003
- const providerData = JSON.parse(fs.readFileSync(providerJson, 'utf-8'));
1004
- if (providerData.disableUpstream !== true) {
1005
- providerData.disableUpstream = true;
1006
- fs.writeFileSync(providerJson, JSON.stringify(providerData, null, 2));
1007
- }
1008
- } catch (error) {
1009
- return {
1010
- dir: null,
1011
- reason: `Failed to update provider.json in writable provider directory: ${(error as Error).message}`,
1012
- };
1013
- }
1014
-
1015
- return { dir: desiredDir };
1035
+ const providerJson = path.join(desiredDir, 'provider.json');
1036
+ if (!fs.existsSync(providerJson)) {
1037
+ return { dir: null, reason: `provider.json not found in writable provider directory: ${desiredDir}` };
1016
1038
  }
1017
1039
 
1040
+ return { dir: desiredDir };
1041
+ }
1042
+
1018
1043
 
1019
1044
  private async handleAutoImplement(type: string, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
1020
1045
  return handleAutoImplement(this, type, req, res);
@@ -1186,7 +1211,7 @@ export class DevServer implements DevServerContext {
1186
1211
  lines.push('## Required Return Format');
1187
1212
  lines.push('| Function | Return JSON |');
1188
1213
  lines.push('|---|---|');
1189
- lines.push('| readChat | `{ id, status, title, messages: [{role, content, index, kind?, meta?}], inputContent, activeModal }` — optional `kind`: standard, thought, tool, terminal; optional `meta`: e.g. `{ label, isRunning }` for dashboard |');
1214
+ lines.push('| readChat | `{ id, status, title, messages: [{role, content, index, kind?, meta?}], inputContent, activeModal, controlValues?, summaryMetadata? }` — optional `kind`: standard, thought, tool, terminal; prefer explicit `controlValues` for current selections and `summaryMetadata` for compact always-visible UI metadata |');
1190
1215
  lines.push('| sendMessage | `{ sent: false, needsTypeAndSend: true, selector }` |');
1191
1216
  lines.push('| resolveAction | `{ resolved: true/false, clicked? }` |');
1192
1217
  lines.push('| listSessions | `{ sessions: [{ id, title, active, index }] }` |');
@@ -178,7 +178,11 @@ module.exports.setMode = (params) => {
178
178
  * 5. Approval dialog detection (buttons, modal)
179
179
  * 6. Input field selector
180
180
  *
181
- * { id, status, title, messages[], inputContent, activeModal }
181
+ * Preferred live-state surface:
182
+ * - controlValues: explicit current control selections (model/mode/etc.)
183
+ * - summaryMetadata: compact always-visible metadata for dashboard/recent views
184
+ * Legacy top-level model/mode output is no longer the preferred shape.
185
+ * → { id, status, title, messages[], inputContent, activeModal, controlValues?, summaryMetadata? }
182
186
  */
183
187
  (() => {
184
188
  try {
@@ -206,6 +210,9 @@ module.exports.setMode = (params) => {
206
210
  messages,
207
211
  inputContent,
208
212
  activeModal,
213
+ // TODO: Return explicit selections when available, e.g.
214
+ // controlValues: { model: selectedModel, mode: selectedMode },
215
+ // summaryMetadata: { items: [{ id: 'model', value: selectedModelLabel || selectedModel, shortValue: selectedModel, order: 10 }] },
209
216
  });
210
217
  } catch(e) {
211
218
  return JSON.stringify({ id: '', status: 'error', messages: [], error: e.message });
package/src/index.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Core logic for daemon: CDP, Provider, IDE detection, CLI/ACP adapters and more.
5
5
  */
6
6
  export type { ChatMessage, ExtensionInfo, CommandResult as CoreCommandResult, ProviderConfig, DaemonEvent, StatusResponse, SystemInfo, DetectedIde, ProviderInfo, AgentEntry, } from './types.js';
7
- export type { SessionEntry, CompactSessionEntry, CompactDaemonEntry, SessionTransport, SessionKind, SessionCapability, AgentSessionStream, ReadChatCursor, ReadChatSyncMode, ReadChatSyncResult, TransportTopic, SessionChatTailSubscriptionParams, MachineRuntimeSubscriptionParams, SessionHostDiagnosticsSubscriptionParams, SessionModalSubscriptionParams, DaemonMetadataSubscriptionParams, SessionChatTailUpdate, MachineRuntimeUpdate, SessionHostDiagnosticsUpdate, SessionModalUpdate, DaemonMetadataUpdate, TopicUpdateEnvelope, SubscribeRequest, UnsubscribeRequest, StandaloneWsStatusPayload, AvailableProviderInfo, AcpConfigOption, AcpMode, ProviderControlSchema, StatusReportPayload, MachineInfo, SessionHostDiagnosticsSnapshot, SessionHostRecord, SessionHostWriteOwner, SessionHostAttachedClient, SessionHostLogEntry, SessionHostRequestTrace, SessionHostRuntimeTransition, DetectedIdeInfo, WorkspaceEntry, ProviderState, ProviderStatus, ProviderErrorReason, ActiveChatData, IdeProviderState, CliProviderState, AcpProviderState, ExtensionProviderState, } from './shared-types.js';
7
+ export type { SessionEntry, CompactSessionEntry, CompactDaemonEntry, SessionTransport, SessionKind, SessionCapability, AgentSessionStream, ReadChatCursor, ReadChatSyncMode, ReadChatSyncResult, TransportTopic, SessionChatTailSubscriptionParams, MachineRuntimeSubscriptionParams, SessionHostDiagnosticsSubscriptionParams, SessionModalSubscriptionParams, DaemonMetadataSubscriptionParams, SessionChatTailUpdate, MachineRuntimeUpdate, SessionHostDiagnosticsUpdate, SessionModalUpdate, DaemonMetadataUpdate, TopicUpdateEnvelope, SubscribeRequest, UnsubscribeRequest, StandaloneWsStatusPayload, AvailableProviderInfo, AcpConfigOption, AcpMode, ProviderControlSchema, StatusReportPayload, MachineInfo, SessionHostDiagnosticsSnapshot, SessionHostRecord, SessionHostWriteOwner, SessionHostAttachedClient, SessionHostLogEntry, SessionHostRequestTrace, SessionHostRuntimeTransition, DetectedIdeInfo, WorkspaceEntry, ProviderSummaryItem, ProviderSummaryMetadata, ProviderState, ProviderStatus, ProviderErrorReason, ActiveChatData, IdeProviderState, CliProviderState, AcpProviderState, ExtensionProviderState, } from './shared-types.js';
8
8
  import type { RuntimeWriteOwner as _RuntimeWriteOwner } from './shared-types-extra.js';
9
9
  import type { RuntimeAttachedClient as _RuntimeAttachedClient } from './shared-types-extra.js';
10
10
  import type { RecentLaunchEntry as _RecentLaunchEntry } from './shared-types.js';
package/src/index.ts CHANGED
@@ -66,6 +66,8 @@ export type {
66
66
  SessionHostRuntimeTransition,
67
67
  DetectedIdeInfo,
68
68
  WorkspaceEntry,
69
+ ProviderSummaryItem,
70
+ ProviderSummaryMetadata,
69
71
  ProviderState,
70
72
  ProviderStatus,
71
73
  ProviderErrorReason,
@@ -195,6 +197,8 @@ export { IdeProviderInstance } from './providers/ide-provider-instance.js';
195
197
  export { CliProviderInstance } from './providers/cli-provider-instance.js';
196
198
  export { AcpProviderInstance } from './providers/acp-provider-instance.js';
197
199
  export type { ProviderModule, CdpTargetFilter, ProviderResumeCapability, InputEnvelope, InputPart, MessagePart, ControlListResult, ControlSetResult, ControlInvokeResult } from './providers/contracts.js';
200
+ export type { ProviderSourceConfigSnapshot, ProviderSourceConfigUpdate } from './config/provider-source-config.js';
201
+ export { parseProviderSourceConfigUpdate } from './config/provider-source-config.js';
198
202
  export { normalizeInputEnvelope, normalizeMessageParts, flattenMessageParts } from './providers/io-contracts.js';
199
203
  export { VersionArchive, detectAllVersions } from './providers/version-archive.js';
200
204
  export type { ProviderVersionInfo, VersionHistory } from './providers/version-archive.js';
@@ -34,8 +34,7 @@ export declare class AcpProviderInstance implements ProviderInstance {
34
34
  private lastStatus;
35
35
  private generatingStartedAt;
36
36
  private agentCapabilities;
37
- private currentModel;
38
- private currentMode;
37
+ private currentSelections;
39
38
  private activeToolCalls;
40
39
  private stopReason;
41
40
  private partialContent;
@@ -61,6 +60,13 @@ export declare class AcpProviderInstance implements ProviderInstance {
61
60
  getState(): AcpProviderState;
62
61
  onEvent(event: string, data?: any): void;
63
62
  getInstanceId(): string;
63
+ private resolveConfigOptionLabel;
64
+ private resolveModeLabel;
65
+ private getCurrentSelection;
66
+ private setCurrentSelection;
67
+ private getSelectionControlValues;
68
+ private resolveSelectionLabel;
69
+ private buildSelectionSummaryMetadata;
64
70
  private parseConfigOptions;
65
71
  private parseModes;
66
72
  setConfigOption(category: string, value: string): Promise<void>;
@@ -50,6 +50,7 @@ import type { ProviderModule, ContentBlock, InputEnvelope, InputPart, ToolCallIn
50
50
  import { normalizeContent, flattenContent, normalizeInputEnvelope } from './contracts.js';
51
51
  import type { ProviderInstance, ProviderState, AcpProviderState, ProviderErrorReason, ProviderEvent, InstanceContext } from './provider-instance.js';
52
52
  import { StatusMonitor } from './status-monitor.js';
53
+ import { buildLegacyModelModeSummaryMetadata } from './summary-metadata.js';
53
54
  import { LOG } from '../logging/logger.js';
54
55
 
55
56
  // ─── Internal Display Types (for dashboard) ────────────────────────────
@@ -84,6 +85,8 @@ interface AcpMode {
84
85
  description?: string;
85
86
  }
86
87
 
88
+ type SelectionCategory = 'model' | 'mode';
89
+
87
90
  interface PromptCapabilityFlags {
88
91
  image: boolean;
89
92
  audio: boolean;
@@ -223,8 +226,7 @@ export class AcpProviderInstance implements ProviderInstance {
223
226
  private lastStatus: string = 'starting';
224
227
  private generatingStartedAt = 0;
225
228
  private agentCapabilities: Record<string, any> = {};
226
- private currentModel: string | undefined;
227
- private currentMode: string | undefined;
229
+ private currentSelections: Partial<Record<SelectionCategory, string>> = {};
228
230
  private activeToolCalls: AcpToolCall[] = [];
229
231
  private stopReason: string | null = null;
230
232
  private partialContent = '';
@@ -332,8 +334,6 @@ export class AcpProviderInstance implements ProviderInstance {
332
334
  inputContent: '',
333
335
  },
334
336
  workspace: this.workingDir,
335
- currentModel: this.currentModel,
336
- currentPlan: this.currentMode,
337
337
  instanceId: this.instanceId,
338
338
  lastUpdated: Date.now(),
339
339
  settings: this.settings,
@@ -344,11 +344,9 @@ export class AcpProviderInstance implements ProviderInstance {
344
344
  // Error details for dashboard display
345
345
  errorMessage: this.errorMessage || undefined,
346
346
  errorReason: this.errorReason || undefined,
347
- controlValues: {
348
- ...(this.currentModel ? { model: this.currentModel } : {}),
349
- ...(this.currentMode ? { mode: this.currentMode } : {}),
350
- },
347
+ controlValues: this.getSelectionControlValues(),
351
348
  providerControls: this.provider.controls,
349
+ summaryMetadata: this.buildSelectionSummaryMetadata(),
352
350
  };
353
351
  }
354
352
 
@@ -386,6 +384,61 @@ export class AcpProviderInstance implements ProviderInstance {
386
384
  return this.instanceId;
387
385
  }
388
386
 
387
+ private resolveConfigOptionLabel(category: string, value: string | undefined): string | undefined {
388
+ if (!value) return undefined;
389
+ const option = this.configOptions.find((entry) => entry.category === category);
390
+ return option?.options.find((candidate) => candidate.value === value)?.name || value;
391
+ }
392
+
393
+ private resolveModeLabel(modeId: string | undefined): string | undefined {
394
+ if (!modeId) return undefined;
395
+ return this.availableModes.find((mode) => mode.id === modeId)?.name || modeId;
396
+ }
397
+
398
+ private getCurrentSelection(category: SelectionCategory): string | undefined {
399
+ return this.currentSelections[category];
400
+ }
401
+
402
+ private setCurrentSelection(category: SelectionCategory, value: string | null | undefined): void {
403
+ const normalized = typeof value === 'string' ? value.trim() : '';
404
+ if (normalized) {
405
+ this.currentSelections[category] = normalized;
406
+ return;
407
+ }
408
+ delete this.currentSelections[category];
409
+ }
410
+
411
+ private getSelectionControlValues(): Record<string, string> {
412
+ const model = this.getCurrentSelection('model');
413
+ const mode = this.getCurrentSelection('mode');
414
+ return {
415
+ ...(model ? { model } : {}),
416
+ ...(mode ? { mode } : {}),
417
+ };
418
+ }
419
+
420
+ private resolveSelectionLabel(category: SelectionCategory, value: string | undefined): string | undefined {
421
+ if (!value) return undefined;
422
+ const configLabel = this.resolveConfigOptionLabel(category, value);
423
+ if (configLabel && configLabel !== value) return configLabel;
424
+ if (category === 'mode') {
425
+ const modeLabel = this.resolveModeLabel(value);
426
+ if (modeLabel) return modeLabel;
427
+ }
428
+ return configLabel || value;
429
+ }
430
+
431
+ private buildSelectionSummaryMetadata() {
432
+ const model = this.getCurrentSelection('model');
433
+ const mode = this.getCurrentSelection('mode');
434
+ return buildLegacyModelModeSummaryMetadata({
435
+ model,
436
+ mode,
437
+ modelLabel: this.resolveSelectionLabel('model', model),
438
+ modeLabel: this.resolveSelectionLabel('mode', mode),
439
+ });
440
+ }
441
+
389
442
  // ─── ACP Config Options & Modes ─────────────────────
390
443
 
391
444
  private parseConfigOptions(raw: any): void {
@@ -425,15 +478,17 @@ export class AcpProviderInstance implements ProviderInstance {
425
478
 
426
479
  this.configOptions.push({ category: category as 'model' | 'mode' | 'thought_level' | 'other', configId, currentValue, options: flatOptions });
427
480
 
428
- // Auto-set currentModel/currentMode from config
429
- if (category === 'model' && currentValue) this.currentModel = currentValue;
481
+ // Auto-set current selections from config
482
+ if (category === 'model' || category === 'mode') {
483
+ this.setCurrentSelection(category, currentValue);
484
+ }
430
485
  }
431
486
  }
432
487
 
433
488
  private parseModes(raw: any): void {
434
489
  if (!raw) return;
435
490
  // modes: { currentModeId, availableModes: [{ id, name, description }] }
436
- if (raw.currentModeId) this.currentMode = raw.currentModeId;
491
+ this.setCurrentSelection('mode', raw.currentModeId);
437
492
  if (Array.isArray(raw.availableModes)) {
438
493
  this.availableModes = raw.availableModes.map((m: any) => ({
439
494
  id: m.id, name: m.name || m.id, description: m.description,
@@ -454,8 +509,7 @@ export class AcpProviderInstance implements ProviderInstance {
454
509
  if (this.useStaticConfig) {
455
510
  opt.currentValue = value;
456
511
  this.selectedConfig[opt.configId] = value;
457
- if (category === 'model') this.currentModel = value;
458
- if (category === 'mode') this.currentMode = value;
512
+ if (category === 'model' || category === 'mode') this.setCurrentSelection(category, value);
459
513
  this.log.info(`[${this.type}] Static config ${category} set to: ${value} — restarting agent`);
460
514
  await this.restartWithNewConfig();
461
515
  return;
@@ -476,7 +530,7 @@ export class AcpProviderInstance implements ProviderInstance {
476
530
  });
477
531
  // Update local state
478
532
  opt.currentValue = value;
479
- if (category === 'model') this.currentModel = value;
533
+ if (category === 'model' || category === 'mode') this.setCurrentSelection(category, value);
480
534
  // Response may include updated configOptions
481
535
  if (result?.configOptions) this.parseConfigOptions(result.configOptions);
482
536
  this.log.info(`[${this.type}] Config ${category} set to: ${value} | response: ${JSON.stringify(result)?.slice(0, 300)}`);
@@ -495,7 +549,7 @@ export class AcpProviderInstance implements ProviderInstance {
495
549
  opt.currentValue = modeId;
496
550
  this.selectedConfig[opt.configId] = modeId;
497
551
  }
498
- this.currentMode = modeId;
552
+ this.setCurrentSelection('mode', modeId);
499
553
  this.log.info(`[${this.type}] Static mode set to: ${modeId} — restarting agent`);
500
554
  await this.restartWithNewConfig();
501
555
  return;
@@ -512,7 +566,7 @@ export class AcpProviderInstance implements ProviderInstance {
512
566
  sessionId: this.sessionId,
513
567
  modeId,
514
568
  });
515
- this.currentMode = modeId;
569
+ this.setCurrentSelection('mode', modeId);
516
570
  this.log.info(`[${this.type}] Mode set to: ${modeId}`);
517
571
  } catch (e: any) {
518
572
  const message = e?.message || 'Unknown ACP mode error';
@@ -845,8 +899,8 @@ export class AcpProviderInstance implements ProviderInstance {
845
899
  this.parseModes(result?.modes);
846
900
 
847
901
  // Legacy: models.currentModelId (some agent compat)
848
- if (!this.currentModel && result?.models?.currentModelId) {
849
- this.currentModel = result.models.currentModelId;
902
+ if (!this.getCurrentSelection('model') && result?.models?.currentModelId) {
903
+ this.setCurrentSelection('model', result.models.currentModelId);
850
904
  }
851
905
 
852
906
  // ─── Static config fallback (for agents without config/* support) ───
@@ -862,14 +916,17 @@ export class AcpProviderInstance implements ProviderInstance {
862
916
  });
863
917
  if (defaultVal) {
864
918
  this.selectedConfig[sc.configId] = defaultVal;
865
- if (sc.category === 'model') this.currentModel = defaultVal;
866
- if (sc.category === 'mode') this.currentMode = defaultVal;
919
+ if (sc.category === 'model' || sc.category === 'mode') {
920
+ this.setCurrentSelection(sc.category, defaultVal);
921
+ }
867
922
  }
868
923
  }
869
924
  this.log.info(`[${this.type}] Using static configOptions (${this.configOptions.length} options)`);
870
925
  }
871
926
 
872
- this.log.info(`[${this.type}] Session created: ${this.sessionId}${this.currentModel ? ` (model: ${this.currentModel})` : ''}${this.currentMode ? ` (mode: ${this.currentMode})` : ''}`);
927
+ const currentModel = this.getCurrentSelection('model');
928
+ const currentMode = this.getCurrentSelection('mode');
929
+ this.log.info(`[${this.type}] Session created: ${this.sessionId}${currentModel ? ` (model: ${currentModel})` : ''}${currentMode ? ` (mode: ${currentMode})` : ''}`);
873
930
  if (this.configOptions.length > 0) {
874
931
  this.log.info(`[${this.type}] Config options: ${this.configOptions.map(c => `${c.category}(${c.options.length})`).join(', ')}`);
875
932
  }
@@ -1073,7 +1130,7 @@ export class AcpProviderInstance implements ProviderInstance {
1073
1130
  break;
1074
1131
  }
1075
1132
  case 'current_mode_update': {
1076
- this.currentMode = update.currentModeId;
1133
+ this.setCurrentSelection('mode', update.currentModeId);
1077
1134
  break;
1078
1135
  }
1079
1136
  case 'config_option_update': {
@@ -1162,7 +1219,7 @@ export class AcpProviderInstance implements ProviderInstance {
1162
1219
 
1163
1220
  // Legacy: model info
1164
1221
  if (params.model) {
1165
- this.currentModel = params.model;
1222
+ this.setCurrentSelection('model', params.model);
1166
1223
  }
1167
1224
  }
1168
1225
 
@@ -19,9 +19,10 @@ import { StatusMonitor } from './status-monitor.js';
19
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
+ import { normalizeProviderEffects } from './control-effects.js';
23
23
  import { formatAutoApprovalMessage, pickApprovalButton } from './approval-utils.js';
24
24
  import { getCliScriptCommand, parseCliScriptResult } from './cli-script-results.js';
25
+ import { mergeProviderPatchState, resolveProviderStateSurface } from './provider-patch-state.js';
25
26
 
26
27
  let CachedDatabaseSync: (new (path: string, options?: { readOnly?: boolean }) => {
27
28
  prepare(sql: string): { get(...params: Array<string | number>): unknown };
@@ -55,6 +56,9 @@ export function getForcedNewSessionScriptName(
55
56
  const controls = Array.isArray((provider as any).controls) ? (provider as any).controls : [];
56
57
  for (const control of controls) {
57
58
  if (control?.type !== 'action') continue;
59
+ if (typeof control?.confirmTitle === 'string' && control.confirmTitle.trim()) continue;
60
+ if (typeof control?.confirmMessage === 'string' && control.confirmMessage.trim()) continue;
61
+ if (typeof control?.confirmLabel === 'string' && control.confirmLabel.trim()) continue;
58
62
  const invokeScript = typeof control?.invokeScript === 'string' ? control.invokeScript.trim() : '';
59
63
  if (!invokeScript) continue;
60
64
  const controlId = typeof control?.id === 'string' ? control.id.trim() : '';
@@ -66,6 +70,26 @@ export function getForcedNewSessionScriptName(
66
70
  return null;
67
71
  }
68
72
 
73
+ export async function waitForCliAdapterReady(
74
+ adapter: { isReady?: () => boolean; getStatus?: () => { status?: string } },
75
+ options?: { timeoutMs?: number; pollMs?: number },
76
+ ): Promise<void> {
77
+ const timeoutMs = Math.max(100, options?.timeoutMs ?? 15_000);
78
+ const pollMs = Math.max(10, options?.pollMs ?? 50);
79
+ const deadline = Date.now() + timeoutMs;
80
+
81
+ while (Date.now() < deadline) {
82
+ if (adapter?.isReady?.()) return;
83
+ const status = adapter?.getStatus?.()?.status;
84
+ if (status === 'stopped') {
85
+ throw new Error('CLI runtime stopped before it became ready');
86
+ }
87
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
88
+ }
89
+
90
+ throw new Error(`CLI runtime did not become ready within ${timeoutMs}ms`);
91
+ }
92
+
69
93
  export class CliProviderInstance implements ProviderInstance {
70
94
  readonly type: string;
71
95
  readonly category = 'cli' as const;
@@ -81,6 +105,7 @@ export class CliProviderInstance implements ProviderInstance {
81
105
  private generatingDebouncePending: { chatTitle: string; timestamp: number } | null = null;
82
106
  private lastApprovalEventAt = 0;
83
107
  private controlValues: Record<string, string | number | boolean> = {};
108
+ private summaryMetadata: unknown = undefined;
84
109
  private appliedEffectKeys = new Set<string>();
85
110
  private historyWriter: ChatHistoryWriter;
86
111
  private runtimeMessages: Array<{ key: string; message: ChatMessage }> = [];
@@ -294,21 +319,7 @@ export class CliProviderInstance implements ProviderInstance {
294
319
  ? parsedMessages.slice(-historyMessageCount)
295
320
  : [];
296
321
  }
297
- const controlValues = extractProviderControlValues(this.provider.controls, parsedStatus);
298
- if (controlValues) {
299
- this.controlValues = { ...this.controlValues, ...controlValues };
300
- }
301
322
  const mergedMessages = this.mergeConversationMessages(parsedMessages);
302
- const currentModel = typeof parsedStatus?.model === 'string' && parsedStatus.model.trim()
303
- ? parsedStatus.model.trim()
304
- : (typeof this.controlValues.model === 'string' && this.controlValues.model.trim()
305
- ? this.controlValues.model.trim()
306
- : undefined);
307
- const currentPlan = typeof parsedStatus?.mode === 'string' && parsedStatus.mode.trim()
308
- ? parsedStatus.mode.trim()
309
- : (typeof this.controlValues.mode === 'string' && this.controlValues.mode.trim()
310
- ? this.controlValues.mode.trim()
311
- : undefined);
312
323
 
313
324
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
314
325
 
@@ -336,6 +347,10 @@ export class CliProviderInstance implements ProviderInstance {
336
347
  }
337
348
 
338
349
  this.applyProviderResponse(parsedStatus, { phase: 'immediate' });
350
+ const surface = resolveProviderStateSurface({
351
+ summaryMetadata: this.summaryMetadata as any,
352
+ controlValues: this.controlValues,
353
+ });
339
354
 
340
355
  return {
341
356
  type: this.type,
@@ -354,8 +369,6 @@ export class CliProviderInstance implements ProviderInstance {
354
369
  inputContent: '',
355
370
  },
356
371
  workspace: this.workingDir,
357
- currentModel,
358
- currentPlan,
359
372
  instanceId: this.instanceId,
360
373
  providerSessionId: this.providerSessionId,
361
374
  lastUpdated: Date.now(),
@@ -370,8 +383,9 @@ export class CliProviderInstance implements ProviderInstance {
370
383
  attachedClients: runtime.attachedClients || [],
371
384
  } : undefined,
372
385
  resume: this.provider.resume,
373
- controlValues: this.controlValues,
386
+ controlValues: surface.controlValues,
374
387
  providerControls: this.provider.controls,
388
+ summaryMetadata: surface.summaryMetadata as any,
375
389
  };
376
390
  }
377
391
 
@@ -427,6 +441,7 @@ export class CliProviderInstance implements ProviderInstance {
427
441
  if (!scriptName) return;
428
442
 
429
443
  LOG.info('CLI', `[${this.type}] forcing fresh session launch via script: ${scriptName}`);
444
+ await waitForCliAdapterReady(this.adapter);
430
445
  const raw = await this.adapter.invokeScript(scriptName, {});
431
446
  const parsed = parseCliScriptResult(raw);
432
447
  if (!parsed.success) {
@@ -593,10 +608,14 @@ export class CliProviderInstance implements ProviderInstance {
593
608
  this.adapter.clearHistory();
594
609
  }
595
610
 
596
- const controlValues = extractProviderControlValues(this.provider.controls, data);
597
- if (controlValues) {
598
- this.controlValues = { ...this.controlValues, ...controlValues };
599
- }
611
+ const patchedState = mergeProviderPatchState({
612
+ providerControls: this.provider.controls,
613
+ data,
614
+ currentControlValues: this.controlValues,
615
+ currentSummaryMetadata: this.summaryMetadata,
616
+ });
617
+ this.controlValues = patchedState.controlValues;
618
+ this.summaryMetadata = patchedState.summaryMetadata;
600
619
 
601
620
  const effects = normalizeProviderEffects(data);
602
621
  for (const effect of effects) {
@@ -819,6 +838,7 @@ export class CliProviderInstance implements ProviderInstance {
819
838
  const previousProviderSessionId = this.providerSessionId;
820
839
  this.providerSessionId = nextSessionId;
821
840
  this.historyWriter.promoteHistorySession(this.type, previousHistorySessionId, nextSessionId);
841
+ this.historyWriter.writeSessionStart(this.type, nextSessionId, this.workingDir, this.instanceId);
822
842
  this.adapter.updateRuntimeMeta({ providerSessionId: nextSessionId });
823
843
  this.onProviderSessionResolved?.({
824
844
  instanceId: this.instanceId,