@adhdev/daemon-core 0.7.35 → 0.7.36

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/index.d.mts +29 -3
  2. package/dist/index.d.ts +29 -3
  3. package/dist/index.js +637 -253
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +637 -255
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/{normalize-auJAPmKy.d.mts → normalize-DVI4Lo5I.d.mts} +63 -1
  8. package/dist/{normalize-auJAPmKy.d.ts → normalize-DVI4Lo5I.d.ts} +63 -1
  9. package/dist/status/normalize.d.mts +1 -1
  10. package/dist/status/normalize.d.ts +1 -1
  11. package/node_modules/@adhdev/session-host-core/package.json +1 -1
  12. package/package.json +1 -1
  13. package/src/agent-stream/manager.ts +14 -1
  14. package/src/agent-stream/provider-adapter.ts +45 -3
  15. package/src/agent-stream/types.ts +4 -0
  16. package/src/cli-adapters/terminal-screen.ts +15 -0
  17. package/src/commands/cli-manager.ts +35 -0
  18. package/src/commands/router.ts +44 -1
  19. package/src/commands/workspace-commands.ts +2 -1
  20. package/src/config/config.d.ts +4 -0
  21. package/src/config/config.ts +9 -1
  22. package/src/config/recent-activity.ts +83 -0
  23. package/src/config/workspaces.d.ts +3 -1
  24. package/src/config/workspaces.ts +15 -1
  25. package/src/index.ts +16 -0
  26. package/src/providers/acp-provider-instance.ts +5 -0
  27. package/src/providers/cli-provider-instance.ts +2 -0
  28. package/src/providers/contracts.ts +94 -0
  29. package/src/providers/extension-provider-instance.ts +4 -0
  30. package/src/providers/ide-provider-instance.ts +2 -0
  31. package/src/providers/provider-instance.ts +5 -1
  32. package/src/shared-types.d.ts +35 -0
  33. package/src/shared-types.ts +70 -0
  34. package/src/status/builders.ts +90 -1
  35. package/src/status/reporter.ts +8 -0
  36. package/src/status/snapshot.ts +162 -0
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Unified recent activity — machine-facing "pick up where you left off".
3
+ *
4
+ * Unlike cliHistory or workspaceActivity, this is task/session oriented:
5
+ * - one normalized row shape for IDE / CLI / ACP
6
+ * - deduped by kind + providerType + workspace
7
+ * - optionally linked to a live sessionId when known
8
+ */
9
+
10
+ import * as path from 'path';
11
+ import type { ADHDevConfig } from './config.js';
12
+ import { expandPath } from './workspaces.js';
13
+
14
+ export interface RecentActivityEntry {
15
+ id: string;
16
+ kind: 'ide' | 'cli' | 'acp';
17
+ providerType: string;
18
+ providerName: string;
19
+ workspace?: string | null;
20
+ currentModel?: string;
21
+ sessionId?: string | null;
22
+ title?: string;
23
+ lastUsedAt: number;
24
+ }
25
+
26
+ const MAX_ACTIVITY = 30;
27
+
28
+ function normalizeWorkspace(workspace?: string | null) {
29
+ if (!workspace) return '';
30
+ try {
31
+ return path.resolve(expandPath(workspace));
32
+ } catch {
33
+ return path.resolve(workspace);
34
+ }
35
+ }
36
+
37
+ export function buildRecentActivityKey(entry: Pick<RecentActivityEntry, 'kind' | 'providerType' | 'workspace'>) {
38
+ return `${entry.kind}:${entry.providerType}:${normalizeWorkspace(entry.workspace)}`;
39
+ }
40
+
41
+ export function appendRecentActivity(
42
+ config: ADHDevConfig,
43
+ entry: Omit<RecentActivityEntry, 'id' | 'lastUsedAt'> & { lastUsedAt?: number },
44
+ ): ADHDevConfig {
45
+ const nextEntry: RecentActivityEntry = {
46
+ ...entry,
47
+ workspace: entry.workspace ? normalizeWorkspace(entry.workspace) : undefined,
48
+ id: buildRecentActivityKey(entry),
49
+ lastUsedAt: entry.lastUsedAt || Date.now(),
50
+ };
51
+
52
+ const filtered = (config.recentActivity || []).filter((item) => item.id !== nextEntry.id);
53
+ return {
54
+ ...config,
55
+ recentActivity: [nextEntry, ...filtered].slice(0, MAX_ACTIVITY),
56
+ };
57
+ }
58
+
59
+ export function getRecentActivity(config: ADHDevConfig, limit = 20): RecentActivityEntry[] {
60
+ return [...(config.recentActivity || [])]
61
+ .sort((a, b) => b.lastUsedAt - a.lastUsedAt)
62
+ .slice(0, limit);
63
+ }
64
+
65
+ export function getRecentSessionSeenAt(config: ADHDevConfig, recentKey: string): number {
66
+ return config.recentSessionReads?.[recentKey] || 0;
67
+ }
68
+
69
+ export function markRecentSessionSeen(
70
+ config: ADHDevConfig,
71
+ recentKey: string,
72
+ seenAt = Date.now(),
73
+ ): ADHDevConfig {
74
+ const prev = config.recentSessionReads || {};
75
+ const nextSeenAt = Math.max(prev[recentKey] || 0, seenAt);
76
+ return {
77
+ ...config,
78
+ recentSessionReads: {
79
+ ...prev,
80
+ [recentKey]: nextSeenAt,
81
+ },
82
+ };
83
+ }
@@ -66,7 +66,9 @@ export declare function resolveIdeLaunchWorkspace(args: {
66
66
  useDefaultWorkspace?: boolean;
67
67
  } | undefined, config: ADHDevConfig): string | undefined;
68
68
  export declare function findWorkspaceByPath(config: ADHDevConfig, rawPath: string): WorkspaceEntry | undefined;
69
- export declare function addWorkspaceEntry(config: ADHDevConfig, rawPath: string, label?: string): {
69
+ export declare function addWorkspaceEntry(config: ADHDevConfig, rawPath: string, label?: string, options?: {
70
+ createIfMissing?: boolean;
71
+ }): {
70
72
  config: ADHDevConfig;
71
73
  entry: WorkspaceEntry;
72
74
  } | {
@@ -208,8 +208,22 @@ export function findWorkspaceByPath(config: ADHDevConfig, rawPath: string): Work
208
208
  return (config.workspaces || []).find(w => path.resolve(expandPath(w.path)) === abs);
209
209
  }
210
210
 
211
- export function addWorkspaceEntry(config: ADHDevConfig, rawPath: string, label?: string): { config: ADHDevConfig; entry: WorkspaceEntry } | { error: string } {
211
+ export function addWorkspaceEntry(
212
+ config: ADHDevConfig,
213
+ rawPath: string,
214
+ label?: string,
215
+ options?: { createIfMissing?: boolean },
216
+ ): { config: ADHDevConfig; entry: WorkspaceEntry } | { error: string } {
212
217
  const abs = expandPath(rawPath);
218
+ const createIfMissing = options?.createIfMissing === true;
219
+ if (!abs) return { error: 'Path required' };
220
+ if (!fs.existsSync(abs) && createIfMissing) {
221
+ try {
222
+ fs.mkdirSync(abs, { recursive: true });
223
+ } catch (e: any) {
224
+ return { error: e?.message || 'Could not create directory' };
225
+ }
226
+ }
213
227
  const v = validateWorkspacePath(abs);
214
228
  if (!v.ok) return { error: v.error };
215
229
 
package/src/index.ts CHANGED
@@ -28,6 +28,7 @@ export type {
28
28
  AvailableProviderInfo,
29
29
  AcpConfigOption,
30
30
  AcpMode,
31
+ ProviderControlSchema,
31
32
  StatusReportPayload,
32
33
  MachineInfo,
33
34
  DetectedIdeInfo,
@@ -42,6 +43,19 @@ export type {
42
43
  ExtensionProviderState,
43
44
  } from './shared-types.js';
44
45
 
46
+ export interface RecentSessionEntry {
47
+ id: string;
48
+ sessionId?: string | null;
49
+ providerType: string;
50
+ providerName: string;
51
+ kind: 'ide' | 'cli' | 'acp';
52
+ title: string;
53
+ workspace?: string | null;
54
+ currentModel?: string;
55
+ status?: 'idle' | 'generating' | 'waiting_approval' | 'error' | 'stopped' | 'starting' | 'panel_hidden' | 'not_monitored' | 'disconnected';
56
+ lastUsedAt: number;
57
+ }
58
+
45
59
  // ── Core Interface ──
46
60
  export type { IDaemonCore, DaemonCoreOptions } from './daemon-core.js';
47
61
 
@@ -49,6 +63,8 @@ export type { IDaemonCore, DaemonCoreOptions } from './daemon-core.js';
49
63
  export { loadConfig, saveConfig, resetConfig, isSetupComplete, addCliHistory, markSetupComplete, updateConfig } from './config/config.js';
50
64
  export { getWorkspaceState } from './config/workspaces.js';
51
65
  export { getWorkspaceActivity } from './config/workspace-activity.js';
66
+ export { appendRecentActivity, getRecentActivity } from './config/recent-activity.js';
67
+ export type { RecentActivityEntry } from './config/recent-activity.js';
52
68
 
53
69
  // ── Detection ──
54
70
  export { detectIDEs } from './detection/ide-detector.js';
@@ -228,6 +228,11 @@ export class AcpProviderInstance implements ProviderInstance {
228
228
  // Error details for dashboard display
229
229
  errorMessage: this.errorMessage || undefined,
230
230
  errorReason: this.errorReason || undefined,
231
+ controlValues: {
232
+ ...(this.currentModel ? { model: this.currentModel } : {}),
233
+ ...(this.currentMode ? { mode: this.currentMode } : {}),
234
+ },
235
+ providerControls: this.provider.controls as any,
231
236
  };
232
237
  }
233
238
 
@@ -126,6 +126,8 @@ export class CliProviderInstance implements ProviderInstance {
126
126
  attachedClients: runtime.attachedClients || [],
127
127
  } : undefined,
128
128
  resume: this.provider.resume,
129
+ controlValues: undefined, // CLI controls not yet wired from stream
130
+ providerControls: this.provider.controls as any,
129
131
  };
130
132
  }
131
133
 
@@ -373,6 +373,10 @@ export interface ProviderModule {
373
373
  // ─── Provider Settings (variables controllable from dashboard) ───
374
374
  settings?: Record<string, ProviderSettingDef>;
375
375
 
376
+ // ─── Provider Controls (interactive controls exposed in chat UI) ───
377
+ /** Dynamic controls declared by provider — rendered in chat panel bar/header */
378
+ controls?: ProviderControlDef[];
379
+
376
380
  // ─── ACP Static Config (for agents without config/* support) ───
377
381
  /** Static options used when agent does not provide configOptions */
378
382
  staticConfigOptions?: Array<{
@@ -515,3 +519,93 @@ export interface ProviderSettingDef {
515
519
  export interface ProviderSettingSchema extends ProviderSettingDef {
516
520
  key: string;
517
521
  }
522
+
523
+ // ─── Provider Controls (interactive chat-level controls) ────────
524
+
525
+ /**
526
+ * Control types:
527
+ * - 'select' — dropdown list (model picker, mode picker)
528
+ * - 'toggle' — on/off switch (compact mode, auto-approve)
529
+ * - 'cycle' — click-to-cycle through options (thinking level: low→med→high)
530
+ * - 'slider' — numeric range (temperature: 0–2)
531
+ * - 'action' — one-shot button (show usage, restart, clear context)
532
+ */
533
+ export type ProviderControlType = 'select' | 'toggle' | 'cycle' | 'slider' | 'action';
534
+
535
+ /**
536
+ * Where the control appears in the chat UI:
537
+ * - 'bar' — thin strip below/above the chat input (always visible)
538
+ * - 'header' — in the agent header area
539
+ * - 'menu' — inside a ⋯ overflow menu
540
+ */
541
+ export type ProviderControlPlacement = 'bar' | 'header' | 'menu';
542
+
543
+ /** Static option for select/cycle controls */
544
+ export interface ProviderControlOption {
545
+ value: string;
546
+ label: string;
547
+ description?: string;
548
+ group?: string;
549
+ }
550
+
551
+ /**
552
+ * ProviderControlDef — A single interactive control declared by a provider.
553
+ *
554
+ * Controls are different from Settings:
555
+ * - Settings: background config, infrequently changed, managed in settings page
556
+ * - Controls: interactive, changed during chat, rendered inside chat panel
557
+ *
558
+ * Each control maps to provider scripts for get/set operations.
559
+ * The frontend renders controls automatically based on this schema —
560
+ * no hardcoded model/mode assumptions needed.
561
+ *
562
+ * For 'action' type:
563
+ * - Renders as a button. On click → calls invokeScript.
564
+ * - No value state. Optionally shows result via toast/inline.
565
+ */
566
+ export interface ProviderControlDef {
567
+ /** Unique identifier (e.g. 'model', 'mode', 'thinking', 'usage') */
568
+ id: string;
569
+ /** Control type */
570
+ type: ProviderControlType;
571
+ /** Display label */
572
+ label: string;
573
+ /** Icon (emoji or icon name) */
574
+ icon?: string;
575
+ /** Where to show this control in the UI */
576
+ placement: ProviderControlPlacement;
577
+
578
+ // ─── Options (for select/cycle) ───
579
+ /** Static options — used when the list is known at definition time */
580
+ options?: ProviderControlOption[];
581
+ /** Dynamic options — load via script at runtime */
582
+ dynamic?: boolean;
583
+ /** Script name to list options (e.g. 'listModels') — required when dynamic=true */
584
+ listScript?: string;
585
+
586
+ // ─── Value (for select/toggle/cycle/slider) ───
587
+ /** Script name to change value (e.g. 'setModel') — required for value-based controls */
588
+ setScript?: string;
589
+ /** Field name in readChat() result to read current value (e.g. 'model', 'mode') */
590
+ readFrom?: string;
591
+ /** Default value */
592
+ defaultValue?: string | number | boolean;
593
+
594
+ // ─── Action (for 'action' type) ───
595
+ /** Script name to invoke (one-shot call, no value) */
596
+ invokeScript?: string;
597
+ /** How to display action result: 'toast' = notification, 'inline' = show in bar, 'none' = silent */
598
+ resultDisplay?: 'toast' | 'inline' | 'none';
599
+
600
+ // ─── Slider-specific ───
601
+ min?: number;
602
+ max?: number;
603
+ step?: number;
604
+
605
+ // ─── Display ───
606
+ /** Sort order within placement group (lower = first) */
607
+ order?: number;
608
+ /** Hide this control when condition not met */
609
+ hidden?: boolean;
610
+ }
611
+
@@ -25,6 +25,7 @@ export class ExtensionProviderInstance implements ProviderInstance {
25
25
  private activeModal: any = null;
26
26
  private currentModel: string = '';
27
27
  private currentMode: string = '';
28
+ private controlValues: Record<string, string | number | boolean> = {};
28
29
  private lastAgentStatus: string = 'idle';
29
30
  private generatingStartedAt: number = 0;
30
31
  private monitor: StatusMonitor;
@@ -82,6 +83,8 @@ export class ExtensionProviderInstance implements ProviderInstance {
82
83
  } : null,
83
84
  currentModel: this.currentModel || undefined,
84
85
  currentPlan: this.currentMode || undefined,
86
+ controlValues: this.controlValues,
87
+ providerControls: this.provider.controls as any,
85
88
  agentStreams: this.agentStreams,
86
89
  instanceId: this.instanceId,
87
90
  lastUpdated: Date.now(),
@@ -98,6 +101,7 @@ export class ExtensionProviderInstance implements ProviderInstance {
98
101
  if (data?.activeModal !== undefined) this.activeModal = data.activeModal;
99
102
  if (data?.model) this.currentModel = data.model;
100
103
  if (data?.mode) this.currentMode = data.mode;
104
+ if (data?.controlValues) this.controlValues = data.controlValues;
101
105
  if (typeof data?.sessionId === 'string' && data.sessionId.trim()) this.chatId = data.sessionId;
102
106
  if (typeof data?.title === 'string' && data.title.trim()) this.chatTitle = data.title;
103
107
  if (typeof data?.agentName === 'string' && data.agentName.trim()) this.agentName = data.agentName;
@@ -125,6 +125,8 @@ export class IdeProviderInstance implements ProviderInstance {
125
125
  currentModel: this.cachedChat?.model || undefined,
126
126
  currentPlan: this.cachedChat?.mode || undefined,
127
127
  currentAutoApprove: this.cachedChat?.autoApprove || undefined,
128
+ controlValues: this.cachedChat?.controlValues || undefined,
129
+ providerControls: this.provider.controls as any,
128
130
  instanceId: this.instanceId,
129
131
  lastUpdated: Date.now(),
130
132
  settings: this.settings,
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import type { ProviderModule, ProviderSettingDef, ProviderResumeCapability } from './contracts.js';
12
- import type { AcpConfigOption, AcpMode } from '../shared-types.js';
12
+ import type { AcpConfigOption, AcpMode, ProviderControlSchema } from '../shared-types.js';
13
13
  import type { ChatMessage } from '../types.js';
14
14
 
15
15
  // ─── ProviderState — Discriminated union by category ─────────────
@@ -83,6 +83,10 @@ interface ProviderStateBase {
83
83
  pendingEvents: ProviderEvent[];
84
84
  runtime?: ProviderRuntimeInfo;
85
85
  resume?: ProviderResumeCapability;
86
+ /** Dynamic control current values */
87
+ controlValues?: Record<string, string | number | boolean>;
88
+ /** Provider-declared controls schema (from provider.controls) */
89
+ providerControls?: ProviderControlSchema[];
86
90
  }
87
91
 
88
92
  /** IDE provider state */
@@ -67,6 +67,8 @@ export interface SessionEntry {
67
67
  currentAutoApprove?: string;
68
68
  acpConfigOptions?: AcpConfigOption[];
69
69
  acpModes?: AcpMode[];
70
+ controlValues?: Record<string, string | number | boolean>;
71
+ providerControls?: ProviderControlSchema[];
70
72
  errorMessage?: string;
71
73
  errorReason?: _ProviderErrorReason;
72
74
  }
@@ -96,6 +98,26 @@ export interface AcpMode {
96
98
  name: string;
97
99
  description?: string;
98
100
  }
101
+ /** Provider control schema (daemon → frontend) */
102
+ export interface ProviderControlSchema {
103
+ id: string;
104
+ type: 'select' | 'toggle' | 'cycle' | 'slider' | 'action';
105
+ label: string;
106
+ icon?: string;
107
+ placement: 'bar' | 'header' | 'menu';
108
+ options?: { value: string; label: string; description?: string; group?: string }[];
109
+ dynamic?: boolean;
110
+ listScript?: string;
111
+ setScript?: string;
112
+ readFrom?: string;
113
+ defaultValue?: string | number | boolean;
114
+ invokeScript?: string;
115
+ resultDisplay?: 'toast' | 'inline' | 'none';
116
+ min?: number;
117
+ max?: number;
118
+ step?: number;
119
+ order?: number;
120
+ }
99
121
  /** Machine hardware/OS info (reported by daemon, displayed by web) */
100
122
  export interface MachineInfo {
101
123
  hostname: string;
@@ -125,6 +147,18 @@ export interface WorkspaceActivity {
125
147
  kind?: string;
126
148
  agentType?: string;
127
149
  }
150
+ export interface RecentSessionEntry {
151
+ id: string;
152
+ sessionId?: string | null;
153
+ providerType: string;
154
+ providerName: string;
155
+ kind: 'ide' | 'cli' | 'acp';
156
+ title: string;
157
+ workspace?: string | null;
158
+ currentModel?: string;
159
+ status?: SessionEntry['status'];
160
+ lastUsedAt: number;
161
+ }
128
162
  export interface StatusReportPayload {
129
163
  /** Daemon instance ID */
130
164
  instanceId: string;
@@ -154,4 +188,5 @@ export interface StatusReportPayload {
154
188
  defaultWorkspaceId?: string | null;
155
189
  defaultWorkspacePath?: string | null;
156
190
  workspaceActivity?: WorkspaceActivity[];
191
+ recentSessions?: RecentSessionEntry[];
157
192
  }
@@ -117,8 +117,17 @@ export interface SessionEntry {
117
117
  currentAutoApprove?: string;
118
118
  acpConfigOptions?: AcpConfigOption[];
119
119
  acpModes?: AcpMode[];
120
+ /** Dynamic control current values (generic key-value) */
121
+ controlValues?: Record<string, string | number | boolean>;
122
+ /** Provider-declared controls schema (transmitted once, cached by frontend) */
123
+ providerControls?: ProviderControlSchema[];
120
124
  errorMessage?: string;
121
125
  errorReason?: _ProviderErrorReason;
126
+ lastUpdated?: number;
127
+ recentKey?: string;
128
+ unread?: boolean;
129
+ lastSeenAt?: number;
130
+ inboxBucket?: RecentSessionBucket;
122
131
  }
123
132
 
124
133
  /** Available provider information */
@@ -145,6 +154,40 @@ export interface AcpMode {
145
154
  description?: string;
146
155
  }
147
156
 
157
+ // ─── Provider Controls Schema (daemon → frontend) ──────────────────
158
+ // Serializable subset of ProviderControlDef — used for dynamic UI rendering
159
+
160
+ /** Provider control schema transmitted to frontend */
161
+ export interface ProviderControlSchema {
162
+ id: string;
163
+ type: 'select' | 'toggle' | 'cycle' | 'slider' | 'action';
164
+ label: string;
165
+ icon?: string;
166
+ placement: 'bar' | 'header' | 'menu';
167
+ /** Static options (for select/cycle) */
168
+ options?: { value: string; label: string; description?: string; group?: string }[];
169
+ /** Dynamic options — frontend should call listScript to load */
170
+ dynamic?: boolean;
171
+ /** Script name to list options */
172
+ listScript?: string;
173
+ /** Script name to change value (value-based controls) */
174
+ setScript?: string;
175
+ /** Field name in readChat result for current value */
176
+ readFrom?: string;
177
+ /** Default value */
178
+ defaultValue?: string | number | boolean;
179
+ /** Script name to invoke (action type) */
180
+ invokeScript?: string;
181
+ /** How to display action result */
182
+ resultDisplay?: 'toast' | 'inline' | 'none';
183
+ /** Slider range */
184
+ min?: number;
185
+ max?: number;
186
+ step?: number;
187
+ /** Sort order */
188
+ order?: number;
189
+ }
190
+
148
191
  // ─── Common Sub-Types (used across StatusReportPayload, BaseDaemonData, etc.) ──
149
192
 
150
193
  /** Machine hardware/OS info (reported by daemon, displayed by web) */
@@ -179,6 +222,31 @@ export interface WorkspaceActivity {
179
222
  agentType?: string;
180
223
  }
181
224
 
225
+ export interface RecentSessionEntry {
226
+ id: string;
227
+ recentKey: string;
228
+ sessionId?: string | null;
229
+ providerType: string;
230
+ providerName: string;
231
+ kind: 'ide' | 'cli' | 'acp';
232
+ title: string;
233
+ workspace?: string | null;
234
+ currentModel?: string;
235
+ status?: SessionEntry['status'];
236
+ lastUsedAt: number;
237
+ unread?: boolean;
238
+ lastSeenAt?: number;
239
+ inboxBucket?: RecentSessionBucket;
240
+ }
241
+
242
+ export type RecentSessionBucket = 'needs_attention' | 'working' | 'task_complete' | 'idle';
243
+
244
+ export interface TerminalBackendStatus {
245
+ backend: 'xterm' | 'ghostty-vt';
246
+ preference: 'auto' | 'xterm' | 'ghostty-vt';
247
+ ghosttyAvailable: boolean;
248
+ }
249
+
182
250
  // ─── Status Report Payload (daemon → server) ────────────────────────
183
251
  // Full payload shape sent via WebSocket status_report
184
252
 
@@ -206,4 +274,6 @@ export interface StatusReportPayload {
206
274
  defaultWorkspaceId?: string | null;
207
275
  defaultWorkspacePath?: string | null;
208
276
  workspaceActivity?: WorkspaceActivity[];
277
+ recentSessions?: RecentSessionEntry[];
278
+ terminalBackend?: TerminalBackendStatus;
209
279
  }
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import type { DaemonCdpManager } from '../cdp/manager.js';
12
- import type { SessionEntry, SessionCapability } from '../shared-types.js';
12
+ import type { SessionEntry, SessionCapability, ProviderControlSchema, AcpConfigOption, AcpMode } from '../shared-types.js';
13
13
  import type {
14
14
  IdeProviderState,
15
15
  CliProviderState,
@@ -75,6 +75,67 @@ export function isCdpConnected(
75
75
  return m?.isConnected ?? false;
76
76
  }
77
77
 
78
+ /**
79
+ * Build legacy controls for providers that haven't been updated to the new schema.
80
+ * Replaces the frontend fallback logic.
81
+ */
82
+ function buildFallbackControls(
83
+ providerControls?: ProviderControlSchema[],
84
+ serverModel?: string,
85
+ serverMode?: string,
86
+ acpConfigOptions?: AcpConfigOption[],
87
+ acpModes?: AcpMode[],
88
+ ): ProviderControlSchema[] {
89
+ if (providerControls && providerControls.length > 0) return providerControls;
90
+ const controls: ProviderControlSchema[] = [];
91
+
92
+ const isAcp = !!(acpConfigOptions || acpModes);
93
+
94
+ // Legacy model control
95
+ const modelFromAcp = acpConfigOptions?.find(c => c.category === 'model');
96
+ if (!isAcp || modelFromAcp) {
97
+ controls.push({
98
+ id: 'model',
99
+ type: 'select',
100
+ label: 'Model',
101
+ icon: '🤖',
102
+ placement: 'bar',
103
+ dynamic: !modelFromAcp,
104
+ listScript: 'listModels',
105
+ setScript: 'setModel',
106
+ readFrom: 'model',
107
+ ...(modelFromAcp && {
108
+ options: modelFromAcp.options.map((o: any) => ({ value: o.value, label: o.name || o.value })),
109
+ }),
110
+ });
111
+ }
112
+
113
+ // Legacy mode control
114
+ const modeFromAcp = acpModes && acpModes.length > 0;
115
+ const thoughtFromAcp = !modeFromAcp && acpConfigOptions?.find((c: any) => c.category !== 'model');
116
+ if (!isAcp || modeFromAcp || thoughtFromAcp) {
117
+ controls.push({
118
+ id: 'mode',
119
+ type: thoughtFromAcp ? 'cycle' : 'select',
120
+ label: thoughtFromAcp ? 'Thinking' : 'Mode',
121
+ icon: thoughtFromAcp ? '🧠' : '⚡',
122
+ placement: 'bar',
123
+ dynamic: !modeFromAcp && !thoughtFromAcp,
124
+ listScript: 'listModes',
125
+ setScript: thoughtFromAcp ? 'setThinkingLevel' : 'setMode',
126
+ readFrom: 'mode',
127
+ ...(modeFromAcp && {
128
+ options: acpModes!.map((m: any) => ({ value: m.id, label: m.name || m.id })),
129
+ }),
130
+ ...(thoughtFromAcp && {
131
+ options: thoughtFromAcp.options.map((o: any) => ({ value: o.value, label: o.name || o.value })),
132
+ }),
133
+ });
134
+ }
135
+
136
+ return controls;
137
+ }
138
+
78
139
  const IDE_SESSION_CAPABILITIES: SessionCapability[] = [
79
140
  'read_chat',
80
141
  'send_message',
@@ -140,8 +201,15 @@ function buildIdeWorkspaceSession(
140
201
  currentModel: state.currentModel,
141
202
  currentPlan: state.currentPlan,
142
203
  currentAutoApprove: state.currentAutoApprove,
204
+ controlValues: state.controlValues,
205
+ providerControls: buildFallbackControls(
206
+ state.providerControls,
207
+ state.currentModel,
208
+ state.currentPlan
209
+ ),
143
210
  errorMessage: state.errorMessage,
144
211
  errorReason: state.errorReason,
212
+ lastUpdated: state.lastUpdated,
145
213
  };
146
214
  }
147
215
 
@@ -166,8 +234,15 @@ function buildExtensionAgentSession(
166
234
  capabilities: EXTENSION_SESSION_CAPABILITIES,
167
235
  currentModel: ext.currentModel,
168
236
  currentPlan: ext.currentPlan,
237
+ controlValues: ext.controlValues,
238
+ providerControls: buildFallbackControls(
239
+ ext.providerControls,
240
+ ext.currentModel,
241
+ ext.currentPlan
242
+ ),
169
243
  errorMessage: ext.errorMessage,
170
244
  errorReason: ext.errorReason,
245
+ lastUpdated: ext.lastUpdated,
171
246
  };
172
247
  }
173
248
 
@@ -193,8 +268,13 @@ function buildCliSession(state: CliProviderState): SessionEntry {
193
268
  resume: state.resume,
194
269
  activeChat,
195
270
  capabilities: PTY_SESSION_CAPABILITIES,
271
+ controlValues: state.controlValues,
272
+ providerControls: buildFallbackControls(
273
+ state.providerControls
274
+ ),
196
275
  errorMessage: state.errorMessage,
197
276
  errorReason: state.errorReason,
277
+ lastUpdated: state.lastUpdated,
198
278
  };
199
279
  }
200
280
 
@@ -218,8 +298,17 @@ function buildAcpSession(state: AcpProviderState): SessionEntry {
218
298
  currentPlan: state.currentPlan,
219
299
  acpConfigOptions: state.acpConfigOptions,
220
300
  acpModes: state.acpModes,
301
+ controlValues: state.controlValues,
302
+ providerControls: buildFallbackControls(
303
+ state.providerControls,
304
+ state.currentModel,
305
+ state.currentPlan,
306
+ state.acpConfigOptions,
307
+ state.acpModes
308
+ ),
221
309
  errorMessage: state.errorMessage,
222
310
  errorReason: state.errorReason,
311
+ lastUpdated: state.lastUpdated,
223
312
  };
224
313
  }
225
314
 
@@ -203,6 +203,14 @@ export class DaemonStatusReporter {
203
203
  currentModel: session.currentModel,
204
204
  currentPlan: session.currentPlan,
205
205
  currentAutoApprove: session.currentAutoApprove,
206
+ recentKey: (session as any).recentKey,
207
+ unread: (session as any).unread,
208
+ lastSeenAt: (session as any).lastSeenAt,
209
+ inboxBucket: (session as any).inboxBucket,
210
+ controlValues: session.controlValues,
211
+ providerControls: session.providerControls,
212
+ acpConfigOptions: session.acpConfigOptions,
213
+ acpModes: session.acpModes,
206
214
  })),
207
215
  p2p: payload.p2p,
208
216
  timestamp: now,