@adhdev/daemon-core 0.8.22 → 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.
- package/dist/agent-stream/types.d.ts +3 -0
- package/dist/cli-adapter-types.d.ts +3 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +71 -11
- package/dist/commands/stream-commands.d.ts +1 -0
- package/dist/config/config.d.ts +6 -0
- package/dist/index.js +1162 -307
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1162 -307
- package/dist/index.mjs.map +1 -1
- package/dist/providers/cli-provider-instance.d.ts +6 -0
- package/dist/providers/contracts.d.ts +59 -1
- package/dist/providers/control-effects.d.ts +4 -0
- package/dist/providers/extension-provider-instance.d.ts +9 -0
- package/dist/providers/ide-provider-instance.d.ts +8 -0
- package/dist/shared-types.d.ts +2 -1
- package/node_modules/@adhdev/session-host-core/package.json +1 -1
- package/package.json +3 -2
- package/src/agent-stream/forward.ts +2 -0
- package/src/agent-stream/provider-adapter.ts +5 -15
- package/src/agent-stream/types.ts +4 -0
- package/src/cli-adapter-types.ts +3 -0
- package/src/cli-adapters/provider-cli-adapter.ts +399 -49
- package/src/commands/handler.ts +1 -0
- package/src/commands/stream-commands.ts +99 -8
- package/src/config/config.d.ts +1 -0
- package/src/config/config.ts +9 -0
- package/src/launch.ts +57 -11
- package/src/providers/cli-provider-instance.ts +148 -2
- package/src/providers/contracts.ts +65 -2
- package/src/providers/control-effects.ts +114 -0
- package/src/providers/extension-provider-instance.ts +163 -3
- package/src/providers/ide-provider-instance.ts +181 -2
- package/src/shared-types.d.ts +1 -0
- package/src/shared-types.ts +2 -1
- 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
|
-
|
|
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
|
-
|
|
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 = [];
|
package/src/shared-types.d.ts
CHANGED
|
@@ -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
|
}
|
package/src/shared-types.ts
CHANGED
|
@@ -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) */
|
package/src/status/snapshot.ts
CHANGED
|
@@ -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),
|