@adhdev/daemon-core 0.7.5 → 0.7.7

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 (43) hide show
  1. package/dist/index.d.mts +164 -38
  2. package/dist/index.d.ts +164 -38
  3. package/dist/index.js +4051 -2547
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +3696 -2192
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/{normalize-tKg8IiDk.d.mts → normalize-auJAPmKy.d.mts} +669 -629
  8. package/dist/{normalize-tKg8IiDk.d.ts → normalize-auJAPmKy.d.ts} +669 -629
  9. package/dist/status/normalize.d.mts +1 -1
  10. package/dist/status/normalize.d.ts +1 -1
  11. package/package.json +5 -1
  12. package/src/agent-stream/forward.ts +6 -0
  13. package/src/boot/daemon-lifecycle.ts +7 -4
  14. package/src/cli-adapter-types.ts +2 -0
  15. package/src/cli-adapters/provider-cli-adapter.ts +148 -11
  16. package/src/cli-adapters/pty-transport.ts +100 -0
  17. package/src/cli-adapters/session-host-transport.ts +392 -0
  18. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +126 -0
  19. package/src/cli-adapters/terminal-backends/types.ts +17 -0
  20. package/src/cli-adapters/terminal-backends/xterm-backend.ts +87 -0
  21. package/src/cli-adapters/terminal-screen.ts +40 -53
  22. package/src/commands/cli-manager.ts +184 -55
  23. package/src/config/config.d.ts +116 -0
  24. package/src/config/workspace-activity.d.ts +22 -0
  25. package/src/config/workspaces.d.ts +84 -0
  26. package/src/daemon/dev-auto-implement.ts +1087 -0
  27. package/src/daemon/dev-cdp-handlers.ts +1003 -0
  28. package/src/daemon/dev-cli-debug.ts +288 -0
  29. package/src/daemon/dev-server-types.ts +45 -0
  30. package/src/daemon/dev-server.ts +121 -1698
  31. package/src/index.ts +5 -1
  32. package/src/providers/cli-provider-instance.ts +13 -1
  33. package/src/providers/contracts.d.ts +408 -0
  34. package/src/providers/contracts.ts +9 -0
  35. package/src/providers/extension-provider-instance.ts +50 -10
  36. package/src/providers/provider-instance-manager.ts +48 -10
  37. package/src/providers/provider-instance.d.ts +142 -0
  38. package/src/providers/provider-instance.ts +23 -1
  39. package/src/shared-types.d.ts +157 -0
  40. package/src/shared-types.ts +14 -0
  41. package/src/status/builders.ts +6 -0
  42. package/src/status/normalize.d.ts +14 -0
  43. package/src/types.d.ts +127 -0
package/src/index.ts CHANGED
@@ -110,7 +110,7 @@ export { ProviderInstanceManager } from './providers/provider-instance-manager.j
110
110
  export { IdeProviderInstance } from './providers/ide-provider-instance.js';
111
111
  export { CliProviderInstance } from './providers/cli-provider-instance.js';
112
112
  export { AcpProviderInstance } from './providers/acp-provider-instance.js';
113
- export type { ProviderModule, CdpTargetFilter } from './providers/contracts.js';
113
+ export type { ProviderModule, CdpTargetFilter, ProviderResumeCapability } from './providers/contracts.js';
114
114
  export { VersionArchive, detectAllVersions } from './providers/version-archive.js';
115
115
  export type { ProviderVersionInfo, VersionHistory } from './providers/version-archive.js';
116
116
 
@@ -120,6 +120,10 @@ export { DevServer } from './daemon/dev-server.js';
120
120
  // ── CLI Adapters ──
121
121
  export { ProviderCliAdapter } from './cli-adapters/provider-cli-adapter.js';
122
122
  export type { CliAdapter } from './cli-adapter-types.js';
123
+ export { NodePtyTransportFactory } from './cli-adapters/pty-transport.js';
124
+ export type { PtyRuntimeTransport, PtyTransportFactory, PtySpawnOptions } from './cli-adapters/pty-transport.js';
125
+ export { SessionHostPtyTransportFactory } from './cli-adapters/session-host-transport.js';
126
+ export type { HostedCliRuntimeDescriptor, CliTransportFactoryParams } from './commands/cli-manager.js';
123
127
 
124
128
  // ── Installer ──
125
129
  export { getAIExtensions, installExtensions, launchIDE, isExtensionInstalled } from './installer.js';
@@ -11,6 +11,7 @@ import type { ProviderModule } from './contracts.js';
11
11
  import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext } from './provider-instance.js';
12
12
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
13
13
  import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
14
+ import type { PtyTransportFactory } from '../cli-adapters/pty-transport.js';
14
15
  import { StatusMonitor } from './status-monitor.js';
15
16
  import { ChatHistoryWriter } from '../config/chat-history.js';
16
17
  import { LOG } from '../logging/logger.js';
@@ -37,10 +38,11 @@ export class CliProviderInstance implements ProviderInstance {
37
38
  private workingDir: string,
38
39
  private cliArgs: string[] = [],
39
40
  instanceId?: string,
41
+ transportFactory?: PtyTransportFactory,
40
42
  ) {
41
43
  this.type = provider.type;
42
44
  this.instanceId = instanceId || crypto.randomUUID();
43
- this.adapter = new ProviderCliAdapter(provider as any as CliProviderModule, workingDir, cliArgs);
45
+ this.adapter = new ProviderCliAdapter(provider as any as CliProviderModule, workingDir, cliArgs, transportFactory);
44
46
  this.monitor = new StatusMonitor();
45
47
  this.historyWriter = new ChatHistoryWriter();
46
48
  }
@@ -82,6 +84,7 @@ export class CliProviderInstance implements ProviderInstance {
82
84
 
83
85
  getState(): ProviderState {
84
86
  const adapterStatus = this.adapter.getStatus();
87
+ const runtime = this.adapter.getRuntimeMetadata();
85
88
 
86
89
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
87
90
 
@@ -114,6 +117,15 @@ export class CliProviderInstance implements ProviderInstance {
114
117
  lastUpdated: Date.now(),
115
118
  settings: this.settings,
116
119
  pendingEvents: this.flushEvents(),
120
+ runtime: runtime ? {
121
+ runtimeId: runtime.runtimeId,
122
+ runtimeKey: runtime.runtimeKey,
123
+ displayName: runtime.displayName,
124
+ workspaceLabel: runtime.workspaceLabel,
125
+ writeOwner: runtime.writeOwner || null,
126
+ attachedClients: runtime.attachedClients || [],
127
+ } : undefined,
128
+ resume: this.provider.resume,
117
129
  };
118
130
  }
119
131
 
@@ -0,0 +1,408 @@
1
+ /**
2
+ * Provider Output Contracts — Output contracts all providers must conform to
3
+ *
4
+ * Design principles:
5
+ * - Only output format is standardized; implementation is free
6
+ * - Common across all categories (cli, ide, extension)
7
+ * - User custom providers use the same contracts
8
+ */
9
+ export interface ReadChatResult {
10
+ messages: ChatMessage[];
11
+ status: AgentStatus;
12
+ activeModal?: ModalInfo | null;
13
+ /** IDE/Extension only: session info */
14
+ id?: string;
15
+ title?: string;
16
+ /** Extension only: additional metadata */
17
+ agentType?: string;
18
+ agentName?: string;
19
+ extensionId?: string;
20
+ /** Status metadata */
21
+ isVisible?: boolean;
22
+ isWelcomeScreen?: boolean;
23
+ inputContent?: string;
24
+ model?: string;
25
+ autoApprove?: string;
26
+ }
27
+ import type { ChatMessage } from '../types.js';
28
+ export type { ChatMessage };
29
+ export type AgentStatus = 'idle' | 'generating' | 'waiting_approval' | 'error' | 'panel_hidden' | 'streaming';
30
+ export interface ModalInfo {
31
+ message: string;
32
+ buttons: string[];
33
+ width?: number;
34
+ height?: number;
35
+ }
36
+ /**
37
+ * ContentBlock — ACP ContentBlock union type
38
+ * Represents displayable content in messages, tool call results, etc.
39
+ */
40
+ export type ContentBlock = TextBlock | ImageBlock | AudioBlock | ResourceLinkBlock | ResourceBlock;
41
+ /** Text content — ACP TextContent */
42
+ export interface TextBlock {
43
+ type: 'text';
44
+ text: string;
45
+ annotations?: ContentAnnotations;
46
+ }
47
+ /** Image content — ACP ImageContent */
48
+ export interface ImageBlock {
49
+ type: 'image';
50
+ data: string;
51
+ mimeType: string;
52
+ uri?: string;
53
+ annotations?: ContentAnnotations;
54
+ }
55
+ /** Audio content — ACP AudioContent */
56
+ export interface AudioBlock {
57
+ type: 'audio';
58
+ data: string;
59
+ mimeType: string;
60
+ annotations?: ContentAnnotations;
61
+ }
62
+ /** Resource link (file reference) — ACP ResourceLink */
63
+ export interface ResourceLinkBlock {
64
+ type: 'resource_link';
65
+ uri: string;
66
+ name: string;
67
+ title?: string;
68
+ description?: string;
69
+ mimeType?: string;
70
+ size?: number;
71
+ annotations?: ContentAnnotations;
72
+ }
73
+ /** Embedded resource (inline file) — ACP EmbeddedResource */
74
+ export interface ResourceBlock {
75
+ type: 'resource';
76
+ resource: TextResourceContents | BlobResourceContents;
77
+ annotations?: ContentAnnotations;
78
+ }
79
+ export interface TextResourceContents {
80
+ uri: string;
81
+ text: string;
82
+ mimeType?: string | null;
83
+ }
84
+ export interface BlobResourceContents {
85
+ uri: string;
86
+ blob: string;
87
+ mimeType?: string | null;
88
+ }
89
+ export interface ContentAnnotations {
90
+ audience?: ('user' | 'assistant')[];
91
+ priority?: number;
92
+ }
93
+ /** Tool call info — ACP ToolCall */
94
+ export interface ToolCallInfo {
95
+ toolCallId: string;
96
+ title: string;
97
+ kind?: ToolKind;
98
+ status?: ToolCallStatus;
99
+ rawInput?: unknown;
100
+ rawOutput?: unknown;
101
+ content?: ToolCallContent[];
102
+ locations?: ToolCallLocation[];
103
+ }
104
+ export type ToolKind = 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'think' | 'fetch' | 'switch_mode' | 'other';
105
+ export type ToolCallStatus = 'pending' | 'in_progress' | 'completed' | 'failed';
106
+ /** Content produced by a tool call — ACP ToolCallContent */
107
+ export type ToolCallContent = {
108
+ type: 'content';
109
+ content: ContentBlock;
110
+ } | {
111
+ type: 'diff';
112
+ path: string;
113
+ oldText?: string;
114
+ newText: string;
115
+ } | {
116
+ type: 'terminal';
117
+ terminalId: string;
118
+ };
119
+ export interface ToolCallLocation {
120
+ path: string;
121
+ line?: number | null;
122
+ }
123
+ /** Normalize content: string → ContentBlock[] */
124
+ export declare function normalizeContent(content: string | ContentBlock[]): ContentBlock[];
125
+ /** Flatten ContentBlock[] → string (backward compat / plain-text extraction) */
126
+ export declare function flattenContent(content: string | ContentBlock[]): string;
127
+ /** SendMessage params — supports rich content (ACP PromptRequest compatible) */
128
+ export interface SendMessageParams {
129
+ /** Shortcut: text-only message */
130
+ text?: string;
131
+ /** Rich content blocks (ACP ContentBlock[]) */
132
+ prompt?: ContentBlock[];
133
+ }
134
+ export interface SendMessageResult {
135
+ sent: boolean;
136
+ error?: string;
137
+ /** When CDP Input API is needed (Lexical editor etc) */
138
+ needsTypeAndSend?: boolean;
139
+ selector?: string;
140
+ }
141
+ export interface ListSessionsResult {
142
+ sessions: SessionInfo[];
143
+ }
144
+ export interface SessionInfo {
145
+ id: string;
146
+ title: string;
147
+ time?: string;
148
+ }
149
+ export interface SwitchSessionResult {
150
+ switched: boolean;
151
+ /** When CDP click coordinates are needed (Antigravity QuickInput etc) */
152
+ action?: 'click';
153
+ clickX?: number;
154
+ clickY?: number;
155
+ error?: string;
156
+ }
157
+ /**
158
+ * Method 1: Script-Click — script calls el.click() directly
159
+ * Cursor Suitable for IDEs using div.cursor-pointer elements.
160
+ */
161
+ export interface ResolveActionScriptClick {
162
+ resolved: boolean;
163
+ clicked?: string;
164
+ available?: string[];
165
+ error?: string;
166
+ }
167
+ /**
168
+ * Method 2: Coordinate-Click — returns coordinates, daemon performs CDP mouse click
169
+ * Antigravity Suitable for IDEs where el.click() does not work.
170
+ */
171
+ export interface ResolveActionCoordinateClick {
172
+ found: boolean;
173
+ text?: string;
174
+ x?: number;
175
+ y?: number;
176
+ w?: number;
177
+ h?: number;
178
+ }
179
+ export type ResolveActionResult = ResolveActionScriptClick | ResolveActionCoordinateClick;
180
+ export type ProviderCategory = 'cli' | 'ide' | 'extension' | 'acp';
181
+ /**
182
+ * Type of object exported by module.exports in provider.js.
183
+ *
184
+ * Each provider.js is fully independent and does not import other providers.
185
+ * Helpers (_helpers/) can be optionally used.
186
+ */
187
+ /**
188
+ * Provider-configurable CDP target filter.
189
+ * Used by DaemonCdpManager to select the correct page/tab to connect to.
190
+ * Without this, the manager uses a hardcoded default filter.
191
+ */
192
+ export interface CdpTargetFilter {
193
+ /** URL must include this string (e.g. 'workbench.html') */
194
+ urlIncludes?: string;
195
+ /** URL must NOT include any of these strings */
196
+ urlExcludes?: string[];
197
+ /** Page title regex pattern for titles to EXCLUDE (e.g. 'Debug Console|Output') */
198
+ titleExcludes?: string;
199
+ }
200
+ export interface ProviderModule {
201
+ /** Unique identifier (e.g. 'cline', 'cursor', 'gemini-cli') */
202
+ type: string;
203
+ /** Display name (e.g. 'Cline', 'Cursor') */
204
+ name: string;
205
+ /** Category: determines execution method */
206
+ category: ProviderCategory;
207
+ /** Alias list — allows users to invoke by alternate names (e.g. ['claude', 'claude-code']) */
208
+ aliases?: string[];
209
+ /** CDP ports [primary, secondary] (IDE category only) */
210
+ cdpPorts?: [number, number];
211
+ /** CDP target filter — controls which page/tab to connect to (IDE category only) */
212
+ targetFilter?: CdpTargetFilter;
213
+ /** CLI command (e.g. 'cursor', 'code') */
214
+ cli?: string;
215
+ /** Display icon */
216
+ icon?: string;
217
+ /** Display name (short name) */
218
+ displayName?: string;
219
+ /** Install instructions (shown when command is missing) */
220
+ install?: string;
221
+ /** Custom version detection command (e.g. 'cursor --version', 'claude -v') */
222
+ versionCommand?: string;
223
+ /** Versions tested by provider maintainer (informational) */
224
+ testedVersions?: string[];
225
+ /** Per-OS process names — used by launch.ts to detect/kill IDE processes */
226
+ processNames?: {
227
+ darwin?: string;
228
+ win32?: string[];
229
+ linux?: string[];
230
+ [key: string]: string | string[] | undefined;
231
+ };
232
+ /** Per-OS install paths — used by detector.ts to detect IDE installation */
233
+ paths?: {
234
+ darwin?: string[];
235
+ win32?: string[];
236
+ linux?: string[];
237
+ [key: string]: string[] | undefined;
238
+ };
239
+ extensionId?: string;
240
+ extensionIdPattern?: RegExp;
241
+ binary?: string;
242
+ spawn?: {
243
+ command: string;
244
+ args?: string[];
245
+ shell?: boolean;
246
+ env?: Record<string, string>;
247
+ };
248
+ patterns?: {
249
+ prompt?: RegExp[];
250
+ generating?: RegExp[];
251
+ approval?: RegExp[];
252
+ ready?: RegExp[];
253
+ };
254
+ cleanOutput?: (raw: string, lastUserInput?: string) => string;
255
+ resume?: ProviderResumeCapability;
256
+ scripts?: ProviderScripts;
257
+ vscodeCommands?: {
258
+ focusPanel?: string;
259
+ openPanel?: string;
260
+ [key: string]: string | undefined;
261
+ };
262
+ inputMethod?: 'cdp-type-and-send' | 'script';
263
+ inputSelector?: string;
264
+ /** webview iframe match text (must be contained in body) */
265
+ webviewMatchText?: string;
266
+ os?: {
267
+ [platform: string]: Partial<Pick<ProviderModule, 'scripts' | 'inputMethod' | 'inputSelector'>>;
268
+ };
269
+ /** Key: semver range string (e.g. '< 1.107.0', '>= 2.0.0') */
270
+ versions?: {
271
+ [versionRange: string]: Partial<Pick<ProviderModule, 'scripts'>> & {
272
+ /**
273
+ * Load scripts from a subdirectory instead of scripts.js root.
274
+ * Path is relative to the provider directory (e.g. 'scripts/legacy').
275
+ * The subdirectory should contain its own scripts.js or individual .js files.
276
+ */
277
+ __dir?: string;
278
+ };
279
+ };
280
+ overrides?: Array<{
281
+ when: {
282
+ os?: string;
283
+ version?: string;
284
+ };
285
+ scripts?: Partial<ProviderScripts>;
286
+ /** Load scripts from a subdirectory for this OS+version combination */
287
+ __dir?: string;
288
+ }>;
289
+ settings?: Record<string, ProviderSettingDef>;
290
+ /** Static options used when agent does not provide configOptions */
291
+ staticConfigOptions?: Array<{
292
+ category: 'model' | 'mode' | 'thought_level' | 'other';
293
+ configId: string;
294
+ defaultValue?: string;
295
+ options: Array<{
296
+ value: string;
297
+ name: string;
298
+ description?: string;
299
+ group?: string;
300
+ }>;
301
+ }>;
302
+ /** Function to convert selected config values to spawn args (applied via process restart when config/* not supported) */
303
+ spawnArgBuilder?: (config: Record<string, string>) => string[];
304
+ /** ACP agent auth methods (multiple supported — in priority order) */
305
+ auth?: AcpAuthMethod[];
306
+ }
307
+ export interface ProviderResumeCapability {
308
+ supported: boolean;
309
+ stopStrategy?: 'command' | 'ctrl_c';
310
+ stopCommand?: string;
311
+ shutdownGraceMs?: number;
312
+ resumeArgs?: string[];
313
+ }
314
+ /** ACP auth method — based on ACP official spec */
315
+ export type AcpAuthMethod = AcpAuthEnvVar | AcpAuthAgent | AcpAuthTerminal;
316
+ /** Environment variable-based auth (API keys etc) */
317
+ export interface AcpAuthEnvVar {
318
+ type: 'env_var';
319
+ id: string;
320
+ name: string;
321
+ vars: Array<{
322
+ name: string;
323
+ label?: string;
324
+ secret?: boolean;
325
+ optional?: boolean;
326
+ }>;
327
+ link?: string;
328
+ }
329
+ /** Agent self-auth (OAuth, browser-based etc) */
330
+ export interface AcpAuthAgent {
331
+ type: 'agent';
332
+ id: string;
333
+ name: string;
334
+ description?: string;
335
+ }
336
+ /** Terminal command-based auth (runs setup command) */
337
+ export interface AcpAuthTerminal {
338
+ type: 'terminal';
339
+ id: string;
340
+ name: string;
341
+ description?: string;
342
+ args?: string[];
343
+ env?: Record<string, string>;
344
+ }
345
+ /**
346
+ * CDP script functions.
347
+ * Each function takes a params object and returns a JS code string for CDP evaluate.
348
+ * The JS execution result must conform to the Output Contract.
349
+ *
350
+ * Custom scripts can be added via index signature in addition to built-in scripts.
351
+ * All scripts can receive params: Record<string, any>,
352
+ * backward compatible with legacy single-argument style (e.g. sendMessage(text)).
353
+ */
354
+ export interface ProviderScripts {
355
+ readChat?: (params?: Record<string, any>) => string;
356
+ sendMessage?: (params?: Record<string, any>) => string;
357
+ listSessions?: (params?: Record<string, any>) => string;
358
+ switchSession?: (params?: Record<string, any>) => string;
359
+ newSession?: (params?: Record<string, any>) => string;
360
+ focusEditor?: (params?: Record<string, any>) => string;
361
+ openPanel?: (params?: Record<string, any>) => string;
362
+ /** List available models → { models: string[], current: string } */
363
+ listModels?: (params?: Record<string, any>) => string;
364
+ /** Change model → { success: boolean } */
365
+ setModel?: (params?: Record<string, any>) => string;
366
+ /** List available modes → { modes: string[], current: string } */
367
+ listModes?: (params?: Record<string, any>) => string;
368
+ /** Change mode → { success: boolean } */
369
+ setMode?: (params?: Record<string, any>) => string;
370
+ /** params: { action: 'approve'|'reject'|'custom', button?: string } */
371
+ resolveAction?: (params?: Record<string, any>) => string;
372
+ webviewResolveAction?: (params?: Record<string, any>) => string;
373
+ listNotifications?: (params?: Record<string, any>) => string;
374
+ dismissNotification?: (params?: Record<string, any>) => string;
375
+ [scriptName: string]: ((params?: Record<string, any>) => string) | undefined;
376
+ }
377
+ /**
378
+ * ProviderLoader.resolve() result: Final provider with OS/version overrides applied
379
+ */
380
+ export interface ResolvedProvider extends ProviderModule {
381
+ /** OS applied during resolve */
382
+ _resolvedOs?: string;
383
+ /** Version applied during resolve */
384
+ _resolvedVersion?: string;
385
+ /** Warning when detected version is not in compatibility matrix */
386
+ _versionWarning?: string;
387
+ }
388
+ /** Setting variable definition declared by provider */
389
+ export interface ProviderSettingDef {
390
+ type: 'boolean' | 'number' | 'string' | 'select';
391
+ default: any;
392
+ /** true = controllable from dashboard UI */
393
+ public: boolean;
394
+ /** UI label */
395
+ label?: string;
396
+ /** UI description */
397
+ description?: string;
398
+ /** Minimum value for number type */
399
+ min?: number;
400
+ /** Maximum value for number type */
401
+ max?: number;
402
+ /** Options for select type */
403
+ options?: string[];
404
+ }
405
+ /** Public settings schema (for dashboard transmission) */
406
+ export interface ProviderSettingSchema extends ProviderSettingDef {
407
+ key: string;
408
+ }
@@ -324,6 +324,7 @@ export interface ProviderModule {
324
324
  ready?: RegExp[];
325
325
  };
326
326
  cleanOutput?: (raw: string, lastUserInput?: string) => string;
327
+ resume?: ProviderResumeCapability;
327
328
 
328
329
  // ─── CDP scripts (ide/extension category) ───
329
330
  scripts?: ProviderScripts;
@@ -388,6 +389,14 @@ export interface ProviderModule {
388
389
  auth?: AcpAuthMethod[];
389
390
  }
390
391
 
392
+ export interface ProviderResumeCapability {
393
+ supported: boolean;
394
+ stopStrategy?: 'command' | 'ctrl_c';
395
+ stopCommand?: string;
396
+ shutdownGraceMs?: number;
397
+ resumeArgs?: string[];
398
+ }
399
+
391
400
  // ─── ACP Auth Types ─────────────────────────────────
392
401
 
393
402
  /** ACP auth method — based on ACP official spec */
@@ -32,6 +32,10 @@ export class ExtensionProviderInstance implements ProviderInstance {
32
32
  // meta
33
33
  private instanceId: string;
34
34
  private ideType: string = '';
35
+ private chatId: string | null = null;
36
+ private chatTitle: string | null = null;
37
+ private agentName: string = '';
38
+ private extensionId: string = '';
35
39
 
36
40
  constructor(provider: ProviderModule) {
37
41
  this.type = provider.type;
@@ -69,8 +73,8 @@ export class ExtensionProviderInstance implements ProviderInstance {
69
73
  category: 'extension',
70
74
  status: this.currentStatus as ProviderState['status'],
71
75
  activeChat: this.messages.length > 0 ? {
72
- id: `${this.type}_session`,
73
- title: this.provider.name,
76
+ id: this.chatId || this.instanceId,
77
+ title: this.chatTitle || this.agentName || this.provider.name,
74
78
  status: this.currentStatus,
75
79
  messages: this.messages,
76
80
  activeModal: this.activeModal,
@@ -94,6 +98,10 @@ export class ExtensionProviderInstance implements ProviderInstance {
94
98
  if (data?.activeModal !== undefined) this.activeModal = data.activeModal;
95
99
  if (data?.model) this.currentModel = data.model;
96
100
  if (data?.mode) this.currentMode = data.mode;
101
+ if (typeof data?.sessionId === 'string' && data.sessionId.trim()) this.chatId = data.sessionId;
102
+ if (typeof data?.title === 'string' && data.title.trim()) this.chatTitle = data.title;
103
+ if (typeof data?.agentName === 'string' && data.agentName.trim()) this.agentName = data.agentName;
104
+ if (typeof data?.extensionId === 'string' && data.extensionId.trim()) this.extensionId = data.extensionId;
97
105
  if (data?.status) {
98
106
  const newStatus = data.status;
99
107
  this.detectTransition(newStatus, data);
@@ -117,12 +125,6 @@ export class ExtensionProviderInstance implements ProviderInstance {
117
125
  }
118
126
 
119
127
  // ─── status transition detect ──────────────────────────────
120
- // NOTE: Extension transitions are TRACKED but NOT emitted as events.
121
- // The parent IdeProviderInstance already emits identical events
122
- // (generating_started, generating_completed, waiting_approval)
123
- // via its own detectAgentTransitions(). Emitting here would cause
124
- // duplicate toasts with slightly different content.
125
-
126
128
  private detectTransition(newStatus: string, data: any): void {
127
129
  const now = Date.now();
128
130
  const agentStatus = (newStatus === 'streaming' || newStatus === 'generating') ? 'generating'
@@ -136,13 +138,44 @@ export class ExtensionProviderInstance implements ProviderInstance {
136
138
  : undefined;
137
139
 
138
140
  if (agentStatus !== this.lastAgentStatus) {
139
- // Track generating start time (for monitor elapsed calculation)
140
141
  if (this.lastAgentStatus === 'idle' && agentStatus === 'generating') {
141
142
  this.generatingStartedAt = now;
143
+ this.pushEvent({
144
+ event: 'agent:generating_started',
145
+ chatTitle: this.resolveChatTitle(data),
146
+ timestamp: now,
147
+ ideType: this.ideType || this.type,
148
+ agentType: this.type,
149
+ agentName: this.agentName || this.provider.name,
150
+ extensionId: this.extensionId || this.type,
151
+ });
152
+ } else if (agentStatus === 'waiting_approval') {
153
+ if (!this.generatingStartedAt) this.generatingStartedAt = now;
154
+ this.pushEvent({
155
+ event: 'agent:waiting_approval',
156
+ chatTitle: this.resolveChatTitle(data),
157
+ timestamp: now,
158
+ ideType: this.ideType || this.type,
159
+ agentType: this.type,
160
+ agentName: this.agentName || this.provider.name,
161
+ extensionId: this.extensionId || this.type,
162
+ modalMessage: data?.activeModal?.message,
163
+ modalButtons: data?.activeModal?.buttons,
164
+ });
142
165
  } else if (agentStatus === 'idle' && (this.lastAgentStatus === 'generating' || this.lastAgentStatus === 'waiting_approval')) {
166
+ const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : 0;
167
+ this.pushEvent({
168
+ event: 'agent:generating_completed',
169
+ chatTitle: this.resolveChatTitle(data),
170
+ duration,
171
+ timestamp: now,
172
+ ideType: this.ideType || this.type,
173
+ agentType: this.type,
174
+ agentName: this.agentName || this.provider.name,
175
+ extensionId: this.extensionId || this.type,
176
+ });
143
177
  this.generatingStartedAt = 0;
144
178
  }
145
- // Do NOT pushEvent for transitions — parent IDE instance handles these
146
179
  this.lastAgentStatus = agentStatus;
147
180
  }
148
181
 
@@ -164,4 +197,11 @@ export class ExtensionProviderInstance implements ProviderInstance {
164
197
  this.events = [];
165
198
  return events;
166
199
  }
200
+
201
+ private resolveChatTitle(data: any): string {
202
+ const title = typeof data?.title === 'string' && data.title.trim()
203
+ ? data.title.trim()
204
+ : this.chatTitle;
205
+ return title || this.agentName || this.provider.name;
206
+ }
167
207
  }
@@ -42,6 +42,27 @@ export class ProviderInstanceManager {
42
42
  }
43
43
  }
44
44
 
45
+ removeByCategory(
46
+ category: 'cli' | 'ide' | 'extension' | 'acp',
47
+ options: { dispose?: boolean } = {},
48
+ ): number {
49
+ const dispose = options.dispose !== false;
50
+ let removed = 0;
51
+ for (const [id, instance] of this.instances) {
52
+ if (instance.category !== category) continue;
53
+ if (dispose) {
54
+ try {
55
+ instance.dispose();
56
+ } catch {
57
+ // noop
58
+ }
59
+ }
60
+ this.instances.delete(id);
61
+ removed += 1;
62
+ }
63
+ return removed;
64
+ }
65
+
45
66
  /**
46
67
  * Import by Instance ID
47
68
  */
@@ -75,16 +96,13 @@ export class ProviderInstanceManager {
75
96
  try {
76
97
  const state = instance.getState();
77
98
  states.push(state);
78
-
79
- // pending events propagation
80
- for (const event of state.pendingEvents) {
81
- for (const listener of this.eventListeners) {
82
- listener({
83
- ...event,
84
- providerType: instance.type,
85
- instanceId: state.instanceId,
86
- targetSessionId: state.instanceId,
87
- providerCategory: state.category,
99
+ this.emitPendingEvents(instance.type, state);
100
+ if (state.category === 'ide') {
101
+ for (const childState of state.extensions) {
102
+ this.emitPendingEvents(childState.type, childState, {
103
+ targetSessionId: childState.instanceId,
104
+ workspaceName: state.workspace || undefined,
105
+ parentSessionId: state.instanceId,
88
106
  });
89
107
  }
90
108
  }
@@ -141,6 +159,26 @@ export class ProviderInstanceManager {
141
159
  this.eventListeners.push(listener);
142
160
  }
143
161
 
162
+ private emitPendingEvents(
163
+ providerType: string,
164
+ state: ProviderState,
165
+ extra: Record<string, unknown> = {},
166
+ ): void {
167
+ for (const event of state.pendingEvents) {
168
+ for (const listener of this.eventListeners) {
169
+ listener({
170
+ ...event,
171
+ providerType,
172
+ instanceId: state.instanceId,
173
+ targetSessionId: state.instanceId,
174
+ providerCategory: state.category,
175
+ workspaceName: state.workspace || undefined,
176
+ ...extra,
177
+ });
178
+ }
179
+ }
180
+ }
181
+
144
182
  /**
145
183
  * Forward event to specific Instance
146
184
  */