@adhdev/daemon-core 0.8.22 → 0.8.24

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 (35) 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 +1162 -307
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +1162 -307
  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/handler.ts +1 -0
  24. package/src/commands/stream-commands.ts +99 -8
  25. package/src/config/config.d.ts +1 -0
  26. package/src/config/config.ts +9 -0
  27. package/src/launch.ts +57 -11
  28. package/src/providers/cli-provider-instance.ts +148 -2
  29. package/src/providers/contracts.ts +65 -2
  30. package/src/providers/control-effects.ts +114 -0
  31. package/src/providers/extension-provider-instance.ts +163 -3
  32. package/src/providers/ide-provider-instance.ts +181 -2
  33. package/src/shared-types.d.ts +1 -0
  34. package/src/shared-types.ts +2 -1
  35. package/src/status/snapshot.ts +1 -0
@@ -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 = [];
@@ -17,6 +17,8 @@ import { ExtensionProviderInstance } from './extension-provider-instance.js';
17
17
  import { StatusMonitor } from './status-monitor.js';
18
18
  import { ChatHistoryWriter } from '../config/chat-history.js';
19
19
  import { LOG } from '../logging/logger.js';
20
+ import { extractProviderControlValues, normalizeProviderEffects } from './control-effects.js';
21
+ import type { ChatMessage } from '../types.js';
20
22
 
21
23
  export class IdeProviderInstance implements ProviderInstance {
22
24
  readonly type: string;
@@ -37,6 +39,8 @@ export class IdeProviderInstance implements ProviderInstance {
37
39
  private monitor: StatusMonitor;
38
40
  private historyWriter: ChatHistoryWriter;
39
41
  private autoApproveBusy = false;
42
+ private appliedEffectKeys = new Set<string>();
43
+ private runtimeMessages: Array<{ key: string; message: ChatMessage }> = [];
40
44
 
41
45
  // IDE meta
42
46
  private ideVersion: string = '';
@@ -115,7 +119,7 @@ export class IdeProviderInstance implements ProviderInstance {
115
119
  id: this.cachedChat.id || 'active_session',
116
120
  title: this.cachedChat.title || this.type,
117
121
  status: this.cachedChat.status || this.currentStatus,
118
- messages: this.cachedChat.messages || [],
122
+ messages: this.mergeConversationMessages(this.cachedChat.messages || []),
119
123
  activeModal: this.cachedChat.activeModal || null,
120
124
  inputContent: this.cachedChat.inputContent || '',
121
125
  } : null,
@@ -136,7 +140,7 @@ export class IdeProviderInstance implements ProviderInstance {
136
140
 
137
141
  onEvent(event: string, data?: any): void {
138
142
  if (event === 'cdp_connected') {
139
- // CDP connection done
143
+ // CDP connection done
140
144
  } else if (event === 'cdp_disconnected') {
141
145
  this.cachedChat = null;
142
146
  this.currentStatus = 'idle';
@@ -158,6 +162,13 @@ export class IdeProviderInstance implements ProviderInstance {
158
162
  for (const ext of this.extensions.values()) {
159
163
  ext.onEvent('stream_reset');
160
164
  }
165
+ } else if (event === 'provider_state_patch' && data && typeof data === 'object') {
166
+ const extType = typeof data.extensionType === 'string' ? data.extensionType : '';
167
+ if (extType && this.extensions.has(extType)) {
168
+ this.extensions.get(extType)!.onEvent('provider_state_patch', data);
169
+ } else {
170
+ this.applyProviderResponse(data, { phase: 'immediate' });
171
+ }
161
172
  }
162
173
  }
163
174
 
@@ -166,6 +177,8 @@ export class IdeProviderInstance implements ProviderInstance {
166
177
  this.lastAgentStatuses.clear();
167
178
  this.generatingStartedAt.clear();
168
179
  this.monitor.reset();
180
+ this.appliedEffectKeys.clear();
181
+ this.runtimeMessages = [];
169
182
  // Child Extension cleanup
170
183
  for (const ext of this.extensions.values()) {
171
184
  ext.dispose();
@@ -173,6 +186,15 @@ export class IdeProviderInstance implements ProviderInstance {
173
186
  this.extensions.clear();
174
187
  }
175
188
 
189
+ updateSettings(newSettings: Record<string, any>): void {
190
+ this.settings = { ...newSettings };
191
+ this.monitor.updateConfig({
192
+ approvalAlert: this.settings.approvalAlert !== false,
193
+ longGeneratingAlert: this.settings.longGeneratingAlert !== false,
194
+ longGeneratingThresholdSec: this.settings.longGeneratingThresholdSec || 180,
195
+ });
196
+ }
197
+
176
198
  // ─── Extension manage ─────────────────────────────
177
199
 
178
200
  /** Extension Instance add */
@@ -298,6 +320,9 @@ export class IdeProviderInstance implements ProviderInstance {
298
320
  }
299
321
  }
300
322
 
323
+ const controlValues = extractProviderControlValues(this.provider.controls, raw);
324
+ if (controlValues) raw.controlValues = controlValues;
325
+
301
326
  this.cachedChat = { ...raw, activeModal };
302
327
  this.detectAgentTransitions(raw, now);
303
328
 
@@ -382,6 +407,12 @@ export class IdeProviderInstance implements ProviderInstance {
382
407
  this.lastAgentStatuses.set(agentKey, agentStatus);
383
408
  }
384
409
 
410
+ this.applyProviderResponse(chatData, {
411
+ phase: (agentStatus === 'idle' && (lastStatus === 'generating' || lastStatus === 'waiting_approval'))
412
+ ? 'turn_completed'
413
+ : 'immediate',
414
+ });
415
+
385
416
  // Auto-approve: when waiting_approval + settings.autoApprove → auto-click approve via CDP
386
417
  if (agentStatus === 'waiting_approval' && this.settings.autoApprove && !this.autoApproveBusy) {
387
418
  this.autoApproveViaScript(chatData);
@@ -399,6 +430,154 @@ export class IdeProviderInstance implements ProviderInstance {
399
430
  if (this.events.length > 50) this.events = this.events.slice(-50);
400
431
  }
401
432
 
433
+ private applyProviderResponse(data: any, options: { phase: 'immediate' | 'turn_completed' }): void {
434
+ if (!data || typeof data !== 'object') return;
435
+
436
+ const controlValues = extractProviderControlValues(this.provider.controls, data);
437
+ if (controlValues) {
438
+ this.cachedChat = {
439
+ ...(this.cachedChat || {}),
440
+ ...data,
441
+ controlValues: { ...(this.cachedChat?.controlValues || {}), ...controlValues },
442
+ };
443
+ }
444
+
445
+ const effects = normalizeProviderEffects(data);
446
+ for (const effect of effects) {
447
+ const effectWhen = effect.when || 'immediate';
448
+ if (effectWhen === 'turn_completed' && options.phase !== 'turn_completed') continue;
449
+ if (effectWhen === 'immediate' && options.phase === 'turn_completed') continue;
450
+
451
+ const effectKey = this.getEffectDedupKey(effect);
452
+ if (this.appliedEffectKeys.has(effectKey)) continue;
453
+ this.appliedEffectKeys.add(effectKey);
454
+
455
+ if (effect.persist !== false) {
456
+ const persisted = this.getPersistedEffectContent(effect);
457
+ if (persisted) this.appendRuntimeSystemMessage(persisted, effectKey);
458
+ }
459
+
460
+ if (effect.type === 'message' && effect.message) {
461
+ this.pushEvent({
462
+ event: 'provider:message',
463
+ timestamp: Date.now(),
464
+ content: typeof effect.message.content === 'string' ? effect.message.content : JSON.stringify(effect.message.content),
465
+ role: effect.message.role || 'system',
466
+ kind: effect.message.kind,
467
+ senderName: effect.message.senderName,
468
+ });
469
+ } else if (effect.type === 'toast' && effect.toast) {
470
+ this.pushEvent({
471
+ event: 'provider:toast',
472
+ effectId: effect.id || effectKey,
473
+ timestamp: Date.now(),
474
+ message: effect.toast.message,
475
+ level: effect.toast.level || 'info',
476
+ });
477
+ } else if (effect.type === 'notification' && effect.notification) {
478
+ this.pushEvent({
479
+ event: 'provider:notification',
480
+ effectId: effect.id || effectKey,
481
+ timestamp: Date.now(),
482
+ title: effect.notification.title,
483
+ message: effect.notification.body,
484
+ content: typeof effect.notification.bubbleContent === 'string'
485
+ ? effect.notification.bubbleContent
486
+ : effect.notification.body,
487
+ level: effect.notification.level || 'info',
488
+ channels: effect.notification.channels || ['toast'],
489
+ preferenceKey: effect.notification.preferenceKey,
490
+ });
491
+ }
492
+ }
493
+ }
494
+
495
+ private appendRuntimeSystemMessage(content: string, dedupKey: string, receivedAt = Date.now()): void {
496
+ const normalizedContent = String(content || '').trim();
497
+ if (!normalizedContent) return;
498
+ if (this.runtimeMessages.some((entry) => entry.key === dedupKey)) return;
499
+ if (!this.cachedChat) {
500
+ this.cachedChat = {
501
+ id: 'active_session',
502
+ title: this.provider.name,
503
+ status: this.currentStatus,
504
+ messages: [],
505
+ activeModal: null,
506
+ inputContent: '',
507
+ };
508
+ }
509
+
510
+ this.runtimeMessages.push({
511
+ key: dedupKey,
512
+ message: {
513
+ role: 'system',
514
+ senderName: 'System',
515
+ content: normalizedContent,
516
+ receivedAt,
517
+ timestamp: receivedAt,
518
+ },
519
+ });
520
+ if (this.runtimeMessages.length > 50) this.runtimeMessages = this.runtimeMessages.slice(-50);
521
+
522
+ this.historyWriter.appendNewMessages(
523
+ this.type,
524
+ [{
525
+ role: 'system',
526
+ senderName: 'System',
527
+ content: normalizedContent,
528
+ kind: 'system',
529
+ receivedAt,
530
+ historyDedupKey: dedupKey,
531
+ }],
532
+ this.cachedChat?.title || this.provider.name,
533
+ this.instanceId,
534
+ this.cachedChat?.id || this.instanceId,
535
+ );
536
+ }
537
+
538
+ private mergeConversationMessages(messages: any[]): ChatMessage[] {
539
+ if (this.runtimeMessages.length === 0) return messages;
540
+ return [...messages, ...this.runtimeMessages.map((entry) => entry.message)]
541
+ .map((message, index) => ({ message, index }))
542
+ .sort((a, b) => {
543
+ const aTime = a.message.receivedAt || a.message.timestamp || 0;
544
+ const bTime = b.message.receivedAt || b.message.timestamp || 0;
545
+ if (aTime !== bTime) return aTime - bTime;
546
+ return a.index - b.index;
547
+ })
548
+ .map((entry) => entry.message);
549
+ }
550
+
551
+ private getPersistedEffectContent(effect: { type: string; message?: { content?: unknown }; toast?: { message?: string }; notification?: { title?: string; body?: string; bubbleContent?: unknown } }): string | null {
552
+ if (effect.type === 'message') {
553
+ return typeof effect.message?.content === 'string'
554
+ ? effect.message.content
555
+ : JSON.stringify(effect.message?.content || '');
556
+ }
557
+ if (effect.type === 'toast') {
558
+ return effect.toast?.message || null;
559
+ }
560
+ if (effect.type === 'notification') {
561
+ if (typeof effect.notification?.bubbleContent === 'string') return effect.notification.bubbleContent;
562
+ if (typeof effect.notification?.title === 'string' && effect.notification.title.trim()) {
563
+ return `${effect.notification.title}\n${effect.notification.body || ''}`.trim();
564
+ }
565
+ return effect.notification?.body || null;
566
+ }
567
+ return null;
568
+ }
569
+
570
+ private getEffectDedupKey(effect: { id?: string; type: string; message?: { content?: unknown }; toast?: { message?: string }; notification?: { title?: string; body?: string } }): string {
571
+ if (effect.id) return `provider_effect:${effect.id}`;
572
+ if (effect.type === 'message') {
573
+ return `provider_effect:message:${typeof effect.message?.content === 'string' ? effect.message.content : JSON.stringify(effect.message?.content || '')}`;
574
+ }
575
+ if (effect.type === 'notification') {
576
+ return `provider_effect:notification:${effect.notification?.title || ''}:${effect.notification?.body || ''}`;
577
+ }
578
+ return `provider_effect:toast:${effect.toast?.message || ''}`;
579
+ }
580
+
402
581
  private flushEvents(): ProviderEvent[] {
403
582
  const events = [...this.events];
404
583
  this.events = [];
@@ -187,6 +187,7 @@ export interface StatusReportPayload {
187
187
  workspaces?: WorkspaceEntry[];
188
188
  defaultWorkspaceId?: string | null;
189
189
  defaultWorkspacePath?: string | null;
190
+ terminalSizingMode?: 'measured' | 'fit';
190
191
  workspaceActivity?: WorkspaceActivity[];
191
192
  recentLaunches?: RecentLaunchEntry[];
192
193
  }
@@ -159,7 +159,7 @@ export interface AcpMode {
159
159
  /** Provider control schema transmitted to frontend */
160
160
  export interface ProviderControlSchema {
161
161
  id: string;
162
- type: 'select' | 'toggle' | 'cycle' | 'slider' | 'action';
162
+ type: 'select' | 'toggle' | 'cycle' | 'slider' | 'action' | 'display';
163
163
  label: string;
164
164
  icon?: string;
165
165
  placement: 'bar' | 'header' | 'menu';
@@ -255,6 +255,7 @@ export interface StatusReportPayload {
255
255
  workspaces?: WorkspaceEntry[];
256
256
  defaultWorkspaceId?: string | null;
257
257
  defaultWorkspacePath?: string | null;
258
+ terminalSizingMode?: 'measured' | 'fit';
258
259
  recentLaunches?: RecentLaunchEntry[];
259
260
  terminalBackend?: TerminalBackendStatus;
260
261
  /** Available providers (present in StatusSnapshot, optional in raw payload) */
@@ -256,6 +256,7 @@ export function buildStatusSnapshot(options: StatusSnapshotOptions): StatusSnaps
256
256
  workspaces: wsState.workspaces,
257
257
  defaultWorkspaceId: wsState.defaultWorkspaceId,
258
258
  defaultWorkspacePath: wsState.defaultWorkspacePath,
259
+ terminalSizingMode: cfg.terminalSizingMode || 'measured',
259
260
  recentLaunches: buildRecentLaunches(recentActivity),
260
261
  terminalBackend,
261
262
  availableProviders: buildAvailableProviders(options.providerLoader),