@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
@@ -26,6 +26,10 @@ export interface ReadChatResult {
26
26
  inputContent?: string;
27
27
  model?: string;
28
28
  autoApprove?: string;
29
+ /** Explicit dynamic control values returned by the provider */
30
+ controlValues?: Record<string, string | number | boolean>;
31
+ /** Provider-driven UI effects derived from chat state */
32
+ effects?: ProviderEffect[];
29
33
  }
30
34
 
31
35
  import type { ChatMessage } from '../types.js';
@@ -46,6 +50,43 @@ export interface ModalInfo {
46
50
  height?: number;
47
51
  }
48
52
 
53
+ export interface ProviderEffectMessage {
54
+ role?: 'system' | 'assistant' | 'user';
55
+ content: string | ContentBlock[];
56
+ kind?: string;
57
+ senderName?: string;
58
+ }
59
+
60
+ export interface ProviderEffectToast {
61
+ level?: 'info' | 'success' | 'warning';
62
+ message: string;
63
+ }
64
+
65
+ export type ProviderNotificationPreferenceKey = 'disconnect' | 'completion' | 'approval' | 'browser';
66
+ export type ProviderNotificationChannel = 'bubble' | 'toast' | 'browser';
67
+
68
+ export interface ProviderEffectNotification {
69
+ title?: string;
70
+ body: string;
71
+ level?: 'info' | 'success' | 'warning';
72
+ channels?: ProviderNotificationChannel[];
73
+ preferenceKey?: ProviderNotificationPreferenceKey;
74
+ bubbleContent?: string | ContentBlock[];
75
+ }
76
+
77
+ export interface ProviderEffect {
78
+ type: 'message' | 'toast' | 'notification';
79
+ /** Stable dedup key; falls back to a content hash when omitted */
80
+ id?: string;
81
+ /** Default immediate. turn_completed fires only on generating/waiting -> idle transitions. */
82
+ when?: 'immediate' | 'turn_completed';
83
+ /** Default true. False keeps the effect UI-only. */
84
+ persist?: boolean;
85
+ message?: ProviderEffectMessage;
86
+ toast?: ProviderEffectToast;
87
+ notification?: ProviderEffectNotification;
88
+ }
89
+
49
90
  // ─── Rich Content Types (ACP Standard) ─────────────────
50
91
  // Based on ACP SDK v0.16.1 schema types.
51
92
  // All provider categories (ACP, IDE, Extension, CLI) use these as output standard.
@@ -290,13 +331,35 @@ export interface ProviderModule {
290
331
  versionCommand?: string;
291
332
  /** Versions tested by provider maintainer (informational) */
292
333
  testedVersions?: string[];
293
- /** Per-OS process names — used by launch.ts to detect/kill IDE processes */
334
+ /** Per-OS process names — used by launch.ts to detect/kill IDE processes */
294
335
  processNames?: {
295
336
  darwin?: string;
296
337
  win32?: string[];
297
338
  linux?: string[];
298
339
  [key: string]: string | string[] | undefined;
299
340
  };
341
+ /**
342
+ * IDE launch preferences.
343
+ * Lets each provider choose how its GUI app should be started per platform.
344
+ */
345
+ launch?: {
346
+ /**
347
+ * Preferred launch method by platform.
348
+ * - 'cli': use the IDE CLI wrapper/binary
349
+ * - 'app': use platform app launcher (e.g. `open -a` on macOS)
350
+ * - 'auto': let core choose a sensible default
351
+ */
352
+ prefer?: {
353
+ darwin?: 'auto' | 'cli' | 'app';
354
+ win32?: 'auto' | 'cli' | 'app';
355
+ linux?: 'auto' | 'cli' | 'app';
356
+ [key: string]: 'auto' | 'cli' | 'app' | undefined;
357
+ };
358
+ /**
359
+ * Override how long core waits for CDP to come up after launch.
360
+ */
361
+ cdpStartupTimeoutMs?: number;
362
+ };
300
363
  /** Per-OS install paths — used by detector.ts to detect IDE installation */
301
364
  paths?: {
302
365
  darwin?: string[];
@@ -580,7 +643,7 @@ export interface ProviderSettingSchema extends ProviderSettingDef {
580
643
  * - 'slider' — numeric range (temperature: 0–2)
581
644
  * - 'action' — one-shot button (show usage, restart, clear context)
582
645
  */
583
- export type ProviderControlType = 'select' | 'toggle' | 'cycle' | 'slider' | 'action';
646
+ export type ProviderControlType = 'select' | 'toggle' | 'cycle' | 'slider' | 'action' | 'display';
584
647
 
585
648
  /**
586
649
  * Where the control appears in the chat UI:
@@ -0,0 +1,114 @@
1
+ import type { ProviderControlDef, ProviderEffect } from './contracts.js';
2
+
3
+ export type ProviderControlValue = string | number | boolean;
4
+
5
+ export function extractProviderControlValues(
6
+ controls: ProviderControlDef[] | undefined,
7
+ data: any,
8
+ ): Record<string, ProviderControlValue> | undefined {
9
+ if (!data || typeof data !== 'object') return undefined;
10
+
11
+ const values: Record<string, ProviderControlValue> = {};
12
+ const explicit = data.controlValues;
13
+ if (explicit && typeof explicit === 'object') {
14
+ for (const [key, value] of Object.entries(explicit)) {
15
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
16
+ values[key] = value;
17
+ }
18
+ }
19
+ }
20
+
21
+ for (const ctrl of controls || []) {
22
+ if (!ctrl.readFrom) continue;
23
+ const rawValue = data[ctrl.readFrom];
24
+ if (rawValue === undefined || rawValue === null) continue;
25
+ values[ctrl.id] = normalizeControlValue(rawValue);
26
+ }
27
+
28
+ if (data.model !== undefined && values.model === undefined) values.model = normalizeControlValue(data.model);
29
+ if (data.mode !== undefined && values.mode === undefined) values.mode = normalizeControlValue(data.mode);
30
+
31
+ return Object.keys(values).length > 0 ? values : undefined;
32
+ }
33
+
34
+ export function normalizeProviderEffects(data: any): ProviderEffect[] {
35
+ const rawEffects = Array.isArray(data?.effects) ? data.effects : [];
36
+ const effects: ProviderEffect[] = [];
37
+
38
+ for (const raw of rawEffects) {
39
+ if (!raw || typeof raw !== 'object') continue;
40
+ const type = raw.type;
41
+ if (type === 'message' && raw.message && typeof raw.message === 'object') {
42
+ const content = raw.message.content;
43
+ if (typeof content !== 'string' && !Array.isArray(content)) continue;
44
+ effects.push({
45
+ type: 'message',
46
+ id: typeof raw.id === 'string' ? raw.id : undefined,
47
+ when: raw.when === 'turn_completed' ? 'turn_completed' : 'immediate',
48
+ persist: raw.persist !== false,
49
+ message: {
50
+ role: raw.message.role === 'assistant' || raw.message.role === 'user' ? raw.message.role : 'system',
51
+ content,
52
+ kind: typeof raw.message.kind === 'string' ? raw.message.kind : undefined,
53
+ senderName: typeof raw.message.senderName === 'string' ? raw.message.senderName : undefined,
54
+ },
55
+ });
56
+ continue;
57
+ }
58
+
59
+ if (type === 'toast' && raw.toast && typeof raw.toast.message === 'string') {
60
+ effects.push({
61
+ type: 'toast',
62
+ id: typeof raw.id === 'string' ? raw.id : undefined,
63
+ when: raw.when === 'turn_completed' ? 'turn_completed' : 'immediate',
64
+ persist: raw.persist !== false,
65
+ toast: {
66
+ level: raw.toast.level === 'success' || raw.toast.level === 'warning' ? raw.toast.level : 'info',
67
+ message: raw.toast.message,
68
+ },
69
+ });
70
+ continue;
71
+ }
72
+
73
+ if (type === 'notification' && raw.notification && typeof raw.notification.body === 'string') {
74
+ effects.push({
75
+ type: 'notification',
76
+ id: typeof raw.id === 'string' ? raw.id : undefined,
77
+ when: raw.when === 'turn_completed' ? 'turn_completed' : 'immediate',
78
+ persist: raw.persist !== false,
79
+ notification: {
80
+ title: typeof raw.notification.title === 'string' ? raw.notification.title : undefined,
81
+ body: raw.notification.body,
82
+ level: raw.notification.level === 'success' || raw.notification.level === 'warning' ? raw.notification.level : 'info',
83
+ channels: Array.isArray(raw.notification.channels)
84
+ ? raw.notification.channels.filter((channel: unknown) =>
85
+ channel === 'bubble' || channel === 'toast' || channel === 'browser')
86
+ : undefined,
87
+ preferenceKey: raw.notification.preferenceKey === 'disconnect'
88
+ || raw.notification.preferenceKey === 'completion'
89
+ || raw.notification.preferenceKey === 'approval'
90
+ || raw.notification.preferenceKey === 'browser'
91
+ ? raw.notification.preferenceKey
92
+ : undefined,
93
+ bubbleContent: typeof raw.notification.bubbleContent === 'string' || Array.isArray(raw.notification.bubbleContent)
94
+ ? raw.notification.bubbleContent
95
+ : undefined,
96
+ },
97
+ });
98
+ }
99
+ }
100
+
101
+ return effects;
102
+ }
103
+
104
+ function normalizeControlValue(value: any): ProviderControlValue {
105
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
106
+ return value;
107
+ }
108
+ if (value && typeof value === 'object') {
109
+ if (typeof value.label === 'string') return value.label;
110
+ if (typeof value.name === 'string') return value.name;
111
+ if (typeof value.id === 'string') return value.id;
112
+ }
113
+ return String(value);
114
+ }
@@ -8,6 +8,9 @@
8
8
  import type { ProviderModule } from './contracts.js';
9
9
  import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext } from './provider-instance.js';
10
10
  import { StatusMonitor } from './status-monitor.js';
11
+ import { extractProviderControlValues, normalizeProviderEffects } from './control-effects.js';
12
+ import { ChatHistoryWriter } from '../config/chat-history.js';
13
+ import type { ChatMessage } from '../types.js';
11
14
 
12
15
  export class ExtensionProviderInstance implements ProviderInstance {
13
16
  readonly type: string;
@@ -26,9 +29,12 @@ export class ExtensionProviderInstance implements ProviderInstance {
26
29
  private currentModel: string = '';
27
30
  private currentMode: string = '';
28
31
  private controlValues: Record<string, string | number | boolean> = {};
32
+ private appliedEffectKeys = new Set<string>();
33
+ private runtimeMessages: Array<{ key: string; message: ChatMessage }> = [];
29
34
  private lastAgentStatus: string = 'idle';
30
35
  private generatingStartedAt: number = 0;
31
36
  private monitor: StatusMonitor;
37
+ private historyWriter: ChatHistoryWriter;
32
38
 
33
39
  // meta
34
40
  private instanceId: string;
@@ -43,6 +49,7 @@ export class ExtensionProviderInstance implements ProviderInstance {
43
49
  this.provider = provider;
44
50
  this.instanceId = crypto.randomUUID();
45
51
  this.monitor = new StatusMonitor();
52
+ this.historyWriter = new ChatHistoryWriter();
46
53
  }
47
54
 
48
55
  // ─── Lifecycle ──────────────────────────────────
@@ -73,11 +80,11 @@ export class ExtensionProviderInstance implements ProviderInstance {
73
80
  name: this.provider.name,
74
81
  category: 'extension',
75
82
  status: this.currentStatus as ProviderState['status'],
76
- activeChat: this.messages.length > 0 ? {
83
+ activeChat: (this.messages.length > 0 || this.runtimeMessages.length > 0) ? {
77
84
  id: this.chatId || this.instanceId,
78
85
  title: this.chatTitle || this.agentName || this.provider.name,
79
86
  status: this.currentStatus,
80
- messages: this.messages,
87
+ messages: this.mergeConversationMessages(this.messages),
81
88
  activeModal: this.activeModal,
82
89
  inputContent: '',
83
90
  } : null,
@@ -101,7 +108,8 @@ export class ExtensionProviderInstance implements ProviderInstance {
101
108
  if (data?.activeModal !== undefined) this.activeModal = data.activeModal;
102
109
  if (data?.model) this.currentModel = data.model;
103
110
  if (data?.mode) this.currentMode = data.mode;
104
- if (data?.controlValues) this.controlValues = data.controlValues;
111
+ const controlValues = extractProviderControlValues(this.provider.controls, data) || data?.controlValues;
112
+ if (controlValues) this.controlValues = controlValues;
105
113
  if (typeof data?.sessionId === 'string' && data.sessionId.trim()) this.chatId = data.sessionId;
106
114
  if (typeof data?.title === 'string' && data.title.trim()) this.chatTitle = data.title;
107
115
  if (typeof data?.agentName === 'string' && data.agentName.trim()) this.agentName = data.agentName;
@@ -115,6 +123,8 @@ export class ExtensionProviderInstance implements ProviderInstance {
115
123
  this.resetStreamState();
116
124
  } else if (event === 'extension_connected') {
117
125
  this.ideType = data?.ideType || '';
126
+ } else if (event === 'provider_state_patch' && data && typeof data === 'object') {
127
+ this.applyProviderResponse(data, { phase: 'immediate' });
118
128
  // Maintain instanceId UUID — do not overwrite
119
129
  }
120
130
  }
@@ -123,6 +133,17 @@ export class ExtensionProviderInstance implements ProviderInstance {
123
133
  this.agentStreams = [];
124
134
  this.messages = [];
125
135
  this.monitor.reset();
136
+ this.appliedEffectKeys.clear();
137
+ this.runtimeMessages = [];
138
+ }
139
+
140
+ updateSettings(newSettings: Record<string, any>): void {
141
+ this.settings = { ...newSettings };
142
+ this.monitor.updateConfig({
143
+ approvalAlert: this.settings.approvalAlert !== false,
144
+ longGeneratingAlert: this.settings.longGeneratingAlert !== false,
145
+ longGeneratingThresholdSec: this.settings.longGeneratingThresholdSec || 180,
146
+ });
126
147
  }
127
148
 
128
149
  /** Query UUID instanceId */
@@ -143,6 +164,7 @@ export class ExtensionProviderInstance implements ProviderInstance {
143
164
  ? `${lastMsg?.role || ''}:${typeof lastMsg?.content === 'string' ? lastMsg.content : JSON.stringify(lastMsg?.content || '')}`.slice(-2000)
144
165
  : undefined;
145
166
 
167
+ const previousStatus = this.lastAgentStatus;
146
168
  if (agentStatus !== this.lastAgentStatus) {
147
169
  if (this.lastAgentStatus === 'idle' && agentStatus === 'generating') {
148
170
  this.generatingStartedAt = now;
@@ -185,6 +207,12 @@ export class ExtensionProviderInstance implements ProviderInstance {
185
207
  this.lastAgentStatus = agentStatus;
186
208
  }
187
209
 
210
+ this.applyProviderResponse(data, {
211
+ phase: (agentStatus === 'idle' && (previousStatus === 'generating' || previousStatus === 'waiting_approval'))
212
+ ? 'turn_completed'
213
+ : 'immediate',
214
+ });
215
+
188
216
  // Monitor check (cooldown based notification) — keep monitor events (long_generating etc)
189
217
  const agentKey = `${this.type}:ext`;
190
218
  const monitorEvents = this.monitor.check(agentKey, agentStatus, now, progressFingerprint);
@@ -198,6 +226,138 @@ export class ExtensionProviderInstance implements ProviderInstance {
198
226
  if (this.events.length > 50) this.events = this.events.slice(-50);
199
227
  }
200
228
 
229
+ private applyProviderResponse(data: any, options: { phase: 'immediate' | 'turn_completed' }): void {
230
+ if (!data || typeof data !== 'object') return;
231
+
232
+ const controlValues = extractProviderControlValues(this.provider.controls, data);
233
+ if (controlValues) this.controlValues = { ...this.controlValues, ...controlValues };
234
+
235
+ const effects = normalizeProviderEffects(data);
236
+ for (const effect of effects) {
237
+ const effectWhen = effect.when || 'immediate';
238
+ if (effectWhen === 'turn_completed' && options.phase !== 'turn_completed') continue;
239
+ if (effectWhen === 'immediate' && options.phase === 'turn_completed') continue;
240
+
241
+ const effectKey = this.getEffectDedupKey(effect);
242
+ if (this.appliedEffectKeys.has(effectKey)) continue;
243
+ this.appliedEffectKeys.add(effectKey);
244
+
245
+ if (effect.persist !== false) {
246
+ const persisted = this.getPersistedEffectContent(effect);
247
+ if (persisted) this.appendRuntimeSystemMessage(persisted, effectKey);
248
+ }
249
+
250
+ if (effect.type === 'message' && effect.message) {
251
+ this.pushEvent({
252
+ event: 'provider:message',
253
+ timestamp: Date.now(),
254
+ content: typeof effect.message.content === 'string' ? effect.message.content : JSON.stringify(effect.message.content),
255
+ role: effect.message.role || 'system',
256
+ kind: effect.message.kind,
257
+ senderName: effect.message.senderName,
258
+ });
259
+ } else if (effect.type === 'toast' && effect.toast) {
260
+ this.pushEvent({
261
+ event: 'provider:toast',
262
+ effectId: effect.id || effectKey,
263
+ timestamp: Date.now(),
264
+ message: effect.toast.message,
265
+ level: effect.toast.level || 'info',
266
+ });
267
+ } else if (effect.type === 'notification' && effect.notification) {
268
+ this.pushEvent({
269
+ event: 'provider:notification',
270
+ effectId: effect.id || effectKey,
271
+ timestamp: Date.now(),
272
+ title: effect.notification.title,
273
+ message: effect.notification.body,
274
+ content: typeof effect.notification.bubbleContent === 'string'
275
+ ? effect.notification.bubbleContent
276
+ : effect.notification.body,
277
+ level: effect.notification.level || 'info',
278
+ channels: effect.notification.channels || ['toast'],
279
+ preferenceKey: effect.notification.preferenceKey,
280
+ });
281
+ }
282
+ }
283
+ }
284
+
285
+ private appendRuntimeSystemMessage(content: string, dedupKey: string, receivedAt = Date.now()): void {
286
+ const normalizedContent = String(content || '').trim();
287
+ if (!normalizedContent) return;
288
+ if (this.runtimeMessages.some((entry) => entry.key === dedupKey)) return;
289
+
290
+ this.runtimeMessages.push({
291
+ key: dedupKey,
292
+ message: {
293
+ role: 'system',
294
+ senderName: 'System',
295
+ content: normalizedContent,
296
+ receivedAt,
297
+ timestamp: receivedAt,
298
+ },
299
+ });
300
+ if (this.runtimeMessages.length > 50) this.runtimeMessages = this.runtimeMessages.slice(-50);
301
+
302
+ this.historyWriter.appendNewMessages(
303
+ this.type,
304
+ [{
305
+ role: 'system',
306
+ senderName: 'System',
307
+ content: normalizedContent,
308
+ kind: 'system',
309
+ receivedAt,
310
+ historyDedupKey: dedupKey,
311
+ }],
312
+ this.chatTitle || this.agentName || this.provider.name,
313
+ this.instanceId,
314
+ this.chatId || this.instanceId,
315
+ );
316
+ }
317
+
318
+ private mergeConversationMessages(messages: any[]): ChatMessage[] {
319
+ if (this.runtimeMessages.length === 0) return messages;
320
+ return [...messages, ...this.runtimeMessages.map((entry) => entry.message)]
321
+ .map((message, index) => ({ message, index }))
322
+ .sort((a, b) => {
323
+ const aTime = a.message.receivedAt || a.message.timestamp || 0;
324
+ const bTime = b.message.receivedAt || b.message.timestamp || 0;
325
+ if (aTime !== bTime) return aTime - bTime;
326
+ return a.index - b.index;
327
+ })
328
+ .map((entry) => entry.message);
329
+ }
330
+
331
+ private getPersistedEffectContent(effect: { type: string; message?: { content?: unknown }; toast?: { message?: string }; notification?: { title?: string; body?: string; bubbleContent?: unknown } }): string | null {
332
+ if (effect.type === 'message') {
333
+ return typeof effect.message?.content === 'string'
334
+ ? effect.message.content
335
+ : JSON.stringify(effect.message?.content || '');
336
+ }
337
+ if (effect.type === 'toast') {
338
+ return effect.toast?.message || null;
339
+ }
340
+ if (effect.type === 'notification') {
341
+ if (typeof effect.notification?.bubbleContent === 'string') return effect.notification.bubbleContent;
342
+ if (typeof effect.notification?.title === 'string' && effect.notification.title.trim()) {
343
+ return `${effect.notification.title}\n${effect.notification.body || ''}`.trim();
344
+ }
345
+ return effect.notification?.body || null;
346
+ }
347
+ return null;
348
+ }
349
+
350
+ private getEffectDedupKey(effect: { id?: string; type: string; message?: { content?: unknown }; toast?: { message?: string }; notification?: { title?: string; body?: string } }): string {
351
+ if (effect.id) return `provider_effect:${effect.id}`;
352
+ if (effect.type === 'message') {
353
+ return `provider_effect:message:${typeof effect.message?.content === 'string' ? effect.message.content : JSON.stringify(effect.message?.content || '')}`;
354
+ }
355
+ if (effect.type === 'notification') {
356
+ return `provider_effect:notification:${effect.notification?.title || ''}:${effect.notification?.body || ''}`;
357
+ }
358
+ return `provider_effect:toast:${effect.toast?.message || ''}`;
359
+ }
360
+
201
361
  private flushEvents(): ProviderEvent[] {
202
362
  const events = [...this.events];
203
363
  this.events = [];