@adhdev/daemon-core 0.5.7 → 0.5.16

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/src/launch.ts CHANGED
@@ -101,7 +101,7 @@ async function isCdpActive(port: number): Promise<boolean> {
101
101
  }
102
102
 
103
103
  /** Kill IDE process (graceful → force) */
104
- async function killIdeProcess(ideId: string): Promise<boolean> {
104
+ export async function killIdeProcess(ideId: string): Promise<boolean> {
105
105
  const plat = os.platform();
106
106
  const appName = getMacAppIdentifiers()[ideId];
107
107
  const winProcesses = getWinProcessNames()[ideId];
@@ -158,7 +158,7 @@ async function killIdeProcess(ideId: string): Promise<boolean> {
158
158
  }
159
159
 
160
160
  /** Check if IDE process is running */
161
- function isIdeRunning(ideId: string): boolean {
161
+ export function isIdeRunning(ideId: string): boolean {
162
162
  const plat = os.platform();
163
163
 
164
164
  try {
@@ -43,10 +43,11 @@ import {
43
43
  type KillTerminalResponse,
44
44
  type SessionUpdate,
45
45
  type ToolCallStatus,
46
+ type SessionConfigOption,
46
47
  } from '@agentclientprotocol/sdk';
47
48
  import type { ProviderModule, ContentBlock, ToolCallInfo, ToolCallContent as TCC, ToolKind, ToolCallStatus as TCS } from './contracts.js';
48
49
  import { normalizeContent, flattenContent } from './contracts.js';
49
- import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext } from './provider-instance.js';
50
+ import type { ProviderInstance, ProviderState, AcpProviderState, ProviderErrorReason, ProviderEvent, InstanceContext } from './provider-instance.js';
50
51
  import { StatusMonitor } from './status-monitor.js';
51
52
  import { LOG } from '../logging/logger.js';
52
53
 
@@ -118,7 +119,7 @@ export class AcpProviderInstance implements ProviderInstance {
118
119
 
119
120
  // Error tracking
120
121
  private errorMessage: string | null = null;
121
- private errorReason: 'not_installed' | 'auth_failed' | 'spawn_error' | 'init_failed' | 'crash' | null = null;
122
+ private errorReason: ProviderErrorReason | null = null;
122
123
  private stderrBuffer: string[] = [];
123
124
  private spawnedAt = 0;
124
125
 
@@ -170,7 +171,7 @@ export class AcpProviderInstance implements ProviderInstance {
170
171
  }
171
172
  }
172
173
 
173
- getState(): ProviderState {
174
+ getState(): AcpProviderState {
174
175
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
175
176
 
176
177
  // Recent 50 messages
@@ -214,7 +215,7 @@ export class AcpProviderInstance implements ProviderInstance {
214
215
  } : null,
215
216
  inputContent: '',
216
217
  },
217
- workingDir: this.workingDir,
218
+ workspace: this.workingDir,
218
219
  currentModel: this.currentModel,
219
220
  currentPlan: this.currentMode,
220
221
  instanceId: this.instanceId,
@@ -225,9 +226,9 @@ export class AcpProviderInstance implements ProviderInstance {
225
226
  acpConfigOptions: this.configOptions,
226
227
  acpModes: this.availableModes,
227
228
  // Error details for dashboard display
228
- errorMessage: this.errorMessage,
229
- errorReason: this.errorReason,
230
- } as any;
229
+ errorMessage: this.errorMessage || undefined,
230
+ errorReason: this.errorReason || undefined,
231
+ };
231
232
  }
232
233
 
233
234
  onEvent(event: string, data?: any): void {
@@ -295,7 +296,7 @@ export class AcpProviderInstance implements ProviderInstance {
295
296
  }
296
297
  }
297
298
 
298
- this.configOptions.push({ category: category as any, configId, currentValue, options: flatOptions });
299
+ this.configOptions.push({ category: category as 'model' | 'mode' | 'thought_level' | 'other', configId, currentValue, options: flatOptions });
299
300
 
300
301
  // Auto-set currentModel/currentMode from config
301
302
  if (category === 'model' && currentValue) this.currentModel = currentValue;
@@ -490,7 +491,7 @@ export class AcpProviderInstance implements ProviderInstance {
490
491
  if (pattern.test(text)) {
491
492
  if (/ENOENT|command not found|not recognized/i.test(text)) {
492
493
  this.errorReason = 'not_installed';
493
- this.errorMessage = `Command '${command}' not found. Install: ${(this.provider as any).install || 'check documentation'}`;
494
+ this.errorMessage = `Command '${command}' not found. Install: ${this.provider.install || 'check documentation'}`;
494
495
  } else {
495
496
  this.errorReason = 'auth_failed';
496
497
  this.errorMessage = text.slice(0, 300);
@@ -511,7 +512,7 @@ export class AcpProviderInstance implements ProviderInstance {
511
512
  if (!this.errorReason) {
512
513
  if (code === 127) {
513
514
  this.errorReason = 'not_installed';
514
- this.errorMessage = `Command '${command}' not found (exit code 127). Install: ${(this.provider as any).install || 'check documentation'}`;
515
+ this.errorMessage = `Command '${command}' not found (exit code 127). Install: ${this.provider.install || 'check documentation'}`;
515
516
  } else if (elapsed < 3000) {
516
517
  // 3-second crash → likely install/auth issue
517
518
  this.errorReason = this.stderrBuffer.length > 0 ? 'crash' : 'spawn_error';
@@ -533,7 +534,7 @@ export class AcpProviderInstance implements ProviderInstance {
533
534
  this.log.error(`[${this.type}] Process spawn error: ${err.message}`);
534
535
  if (err.message.includes('ENOENT')) {
535
536
  this.errorReason = 'not_installed';
536
- this.errorMessage = `Command '${command}' not found. Install: ${(this.provider as any).install || 'check documentation'}`;
537
+ this.errorMessage = `Command '${command}' not found. Install: ${this.provider.install || 'check documentation'}`;
537
538
  } else {
538
539
  this.errorReason = 'spawn_error';
539
540
  this.errorMessage = err.message;
@@ -751,10 +752,10 @@ export class AcpProviderInstance implements ProviderInstance {
751
752
  if (contentBlocks && contentBlocks.length > 0) {
752
753
  // Rich content — forward ContentBlock[] as ACP prompt parts
753
754
  promptParts = contentBlocks.map(b => {
754
- if (b.type === 'text') return { type: 'text', text: (b as any).text };
755
- if (b.type === 'image') return { type: 'image', data: (b as any).data, mimeType: (b as any).mimeType };
756
- if (b.type === 'resource_link') return { type: 'resource_link', uri: (b as any).uri, name: (b as any).name };
757
- if (b.type === 'resource') return { type: 'resource', resource: (b as any).resource };
755
+ if (b.type === 'text') return { type: 'text', text: b.text };
756
+ if (b.type === 'image') return { type: 'image', data: b.data, mimeType: b.mimeType };
757
+ if (b.type === 'resource_link') return { type: 'resource_link', uri: b.uri, name: b.name };
758
+ if (b.type === 'resource') return { type: 'resource', resource: b.resource };
758
759
  return { type: 'text', text: flattenContent([b]) };
759
760
  });
760
761
  } else {
@@ -828,35 +829,32 @@ export class AcpProviderInstance implements ProviderInstance {
828
829
  private handleSessionUpdate(params: SessionNotification): void {
829
830
  if (!params) return;
830
831
 
831
- const update = params.update as SessionUpdate & Record<string, any>;
832
- this.log.debug(`[${this.type}] sessionUpdate: ${update.sessionUpdate} | keys=${Object.keys(update).join(',')}`);
832
+ const update = params.update;
833
+ this.log.debug(`[${this.type}] sessionUpdate: ${update.sessionUpdate}`);
833
834
 
834
835
  switch (update.sessionUpdate) {
835
836
  case 'agent_message_chunk': {
836
837
  const content = update.content;
837
- if (content?.type === 'text' && (content as any).text) {
838
- this.partialContent += (content as any).text;
839
- } else if (content?.type === 'image') {
840
- // Collect image block
838
+ if (content.type === 'text') {
839
+ this.partialContent += content.text;
840
+ } else if (content.type === 'image') {
841
841
  this.partialBlocks.push({
842
842
  type: 'image',
843
- data: (content as any).data || '',
844
- mimeType: (content as any).mimeType || 'image/png',
845
- uri: (content as any).uri,
843
+ data: content.data,
844
+ mimeType: content.mimeType,
846
845
  });
847
- } else if (content?.type === 'resource_link') {
846
+ } else if (content.type === 'resource_link') {
848
847
  this.partialBlocks.push({
849
848
  type: 'resource_link',
850
- uri: (content as any).uri || '',
851
- name: (content as any).name || 'resource',
852
- title: (content as any).title,
853
- mimeType: (content as any).mimeType,
854
- size: (content as any).size,
849
+ uri: content.uri,
850
+ name: content.name || 'resource',
851
+ title: content.title ?? undefined,
852
+ mimeType: content.mimeType ?? undefined,
855
853
  });
856
- } else if (content?.type === 'resource') {
854
+ } else if (content.type === 'resource') {
857
855
  this.partialBlocks.push({
858
856
  type: 'resource',
859
- resource: (content as any).resource,
857
+ resource: content.resource,
860
858
  });
861
859
  }
862
860
  this.currentStatus = 'generating';
@@ -868,57 +866,57 @@ export class AcpProviderInstance implements ProviderInstance {
868
866
  break;
869
867
  }
870
868
  case 'tool_call': {
871
- // New tool call — collect as ToolCallInfo
872
- const tcId = (update as any).toolCallId || `tc_${Date.now()}`;
873
- const tcTitle = (update as any).title || 'unknown';
874
- const tcKind = (update as any).kind as ToolKind | undefined;
875
- const tcStatus = this.mapToolCallStatus((update as any).status);
869
+ // New tool call — ACP SDK ToolCall has all fields typed
870
+ const tcId = update.toolCallId || `tc_${Date.now()}`;
871
+ const tcTitle = update.title || 'unknown';
872
+ const tcKind = update.kind as ToolKind | undefined;
873
+ const tcStatus = this.mapToolCallStatus(update.status);
876
874
 
877
875
  this.activeToolCalls.push({
878
876
  id: tcId,
879
877
  name: tcTitle,
880
878
  status: tcStatus,
881
- input: (update as any).rawInput ? (typeof (update as any).rawInput === 'string' ? (update as any).rawInput : JSON.stringify((update as any).rawInput)) : undefined,
879
+ input: update.rawInput ? (typeof update.rawInput === 'string' ? update.rawInput : JSON.stringify(update.rawInput)) : undefined,
882
880
  });
883
881
 
884
882
  // Also collect as ToolCallInfo for rich content
885
- const acpStatus = (update as any).status as string || 'in_progress';
883
+ const acpStatus = update.status || 'in_progress';
886
884
  this.turnToolCalls.push({
887
885
  toolCallId: tcId,
888
886
  title: tcTitle,
889
887
  kind: tcKind,
890
888
  status: acpStatus as TCS,
891
- rawInput: (update as any).rawInput,
892
- content: this.convertToolCallContent((update as any).content),
893
- locations: (update as any).locations,
889
+ rawInput: update.rawInput,
890
+ content: this.convertToolCallContent(update.content),
891
+ locations: update.locations,
894
892
  });
895
893
  break;
896
894
  }
897
895
  case 'tool_call_update': {
898
- // Update existing tool call
899
- const toolCallId = (update as any).toolCallId;
896
+ // Update existing tool call — ACP SDK ToolCallUpdate typed
897
+ const toolCallId = update.toolCallId;
900
898
  const existing = this.activeToolCalls.find(t => t.id === toolCallId);
901
899
  if (existing) {
902
- if ((update as any).status) existing.status = this.mapToolCallStatus((update as any).status);
903
- if ((update as any).rawOutput) existing.output = typeof (update as any).rawOutput === 'string' ? (update as any).rawOutput : JSON.stringify((update as any).rawOutput);
900
+ if (update.status) existing.status = this.mapToolCallStatus(update.status);
901
+ if (update.rawOutput) existing.output = typeof update.rawOutput === 'string' ? update.rawOutput : JSON.stringify(update.rawOutput);
904
902
  }
905
903
  // Update ToolCallInfo too
906
904
  const tcInfo = this.turnToolCalls.find(t => t.toolCallId === toolCallId);
907
905
  if (tcInfo) {
908
- if ((update as any).status) tcInfo.status = (update as any).status as TCS;
909
- if ((update as any).rawOutput) tcInfo.rawOutput = (update as any).rawOutput;
910
- if ((update as any).content) tcInfo.content = this.convertToolCallContent((update as any).content);
911
- if ((update as any).locations) tcInfo.locations = (update as any).locations;
906
+ if (update.status) tcInfo.status = update.status as TCS;
907
+ if (update.rawOutput) tcInfo.rawOutput = update.rawOutput;
908
+ if (update.content) tcInfo.content = this.convertToolCallContent(update.content);
909
+ if (update.locations) tcInfo.locations = update.locations;
912
910
  }
913
911
  break;
914
912
  }
915
913
  case 'current_mode_update': {
916
- this.currentMode = (update as any).currentModeId;
914
+ this.currentMode = update.currentModeId;
917
915
  break;
918
916
  }
919
917
  case 'config_option_update': {
920
- if ((update as any).configOptions) {
921
- this.parseConfigOptions((update as any).configOptions);
918
+ if (update.configOptions) {
919
+ this.parseConfigOptions(update.configOptions);
922
920
  }
923
921
  break;
924
922
  }
@@ -930,7 +928,7 @@ export class AcpProviderInstance implements ProviderInstance {
930
928
  break;
931
929
  default:
932
930
  // Unknown update type — try legacy parsing for backward compatibility
933
- this.handleLegacyUpdate(update as any);
931
+ this.handleLegacyUpdate(update);
934
932
  break;
935
933
  }
936
934
  }
@@ -1051,13 +1049,13 @@ export class AcpProviderInstance implements ProviderInstance {
1051
1049
  return { ...b, text: b.text.slice(0, -3) };
1052
1050
  }
1053
1051
  return b;
1054
- }).filter(b => b.type !== 'text' || (b as any).text.trim());
1052
+ }).filter(b => b.type !== 'text' || (b.type === 'text' && b.text.trim()));
1055
1053
 
1056
1054
  if (finalBlocks.length > 0) {
1057
1055
  this.messages.push({
1058
1056
  role: 'assistant',
1059
1057
  content: finalBlocks.length === 1 && finalBlocks[0].type === 'text'
1060
- ? (finalBlocks[0] as any).text // single text → string (backward compat)
1058
+ ? (finalBlocks[0] as {type: 'text', text: string}).text // single text → string (backward compat)
1061
1059
  : finalBlocks,
1062
1060
  timestamp: Date.now(),
1063
1061
  toolCalls: this.turnToolCalls.length > 0 ? [...this.turnToolCalls] : undefined,
@@ -26,6 +26,8 @@ export class CliProviderInstance implements ProviderInstance {
26
26
  private generatingStartedAt: number = 0;
27
27
  private settings: Record<string, any> = {};
28
28
  private monitor: StatusMonitor;
29
+ private generatingDebounceTimer: NodeJS.Timeout | null = null;
30
+ private generatingDebouncePending: { chatTitle: string; timestamp: number } | null = null;
29
31
  private historyWriter: ChatHistoryWriter;
30
32
  readonly instanceId: string;
31
33
 
@@ -126,7 +128,7 @@ export class CliProviderInstance implements ProviderInstance {
126
128
  activeModal: adapterStatus.activeModal,
127
129
  inputContent: '',
128
130
  },
129
- workingDir: this.workingDir,
131
+ workspace: this.workingDir,
130
132
  instanceId: this.instanceId,
131
133
  lastUpdated: Date.now(),
132
134
  settings: this.settings,
@@ -160,8 +162,24 @@ export class CliProviderInstance implements ProviderInstance {
160
162
  LOG.info('CLI', `[${this.type}] status: ${this.lastStatus} → ${newStatus}`);
161
163
  if (this.lastStatus === 'idle' && newStatus === 'generating') {
162
164
  this.generatingStartedAt = now;
163
- this.pushEvent({ event: 'agent:generating_started', chatTitle, timestamp: now });
165
+ // Defer the generating_started event if idle comes back within 1s,
166
+ // the whole started→completed pair was a false positive from PTY noise
167
+ if (this.generatingDebounceTimer) clearTimeout(this.generatingDebounceTimer);
168
+ this.generatingDebouncePending = { chatTitle, timestamp: now };
169
+ this.generatingDebounceTimer = setTimeout(() => {
170
+ if (this.generatingDebouncePending) {
171
+ this.pushEvent({ event: 'agent:generating_started', ...this.generatingDebouncePending });
172
+ this.generatingDebouncePending = null;
173
+ }
174
+ this.generatingDebounceTimer = null;
175
+ }, 1000);
164
176
  } else if (newStatus === 'waiting_approval') {
177
+ // Flush pending generating_started if debounce still pending
178
+ if (this.generatingDebouncePending) {
179
+ if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
180
+ this.pushEvent({ event: 'agent:generating_started', ...this.generatingDebouncePending });
181
+ this.generatingDebouncePending = null;
182
+ }
165
183
  if (!this.generatingStartedAt) this.generatingStartedAt = now;
166
184
  const modal = adapterStatus.activeModal;
167
185
  LOG.info('CLI', `[${this.type}] approval modal: "${modal?.message?.slice(0, 80) ?? 'none'}"`);
@@ -172,10 +190,20 @@ export class CliProviderInstance implements ProviderInstance {
172
190
  });
173
191
  } else if (newStatus === 'idle' && (this.lastStatus === 'generating' || this.lastStatus === 'waiting_approval')) {
174
192
  const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : 0;
175
- LOG.info('CLI', `[${this.type}] completed in ${duration}s`);
176
- this.pushEvent({ event: 'agent:generating_completed', chatTitle, duration, timestamp: now });
193
+ // If debounce still pending (generating lasted < 1s), cancel both events
194
+ if (this.generatingDebouncePending) {
195
+ LOG.info('CLI', `[${this.type}] suppressed short generating (${now - this.generatingStartedAt}ms)`);
196
+ if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
197
+ this.generatingDebouncePending = null;
198
+ } else {
199
+ LOG.info('CLI', `[${this.type}] completed in ${duration}s`);
200
+ this.pushEvent({ event: 'agent:generating_completed', chatTitle, duration, timestamp: now });
201
+ }
177
202
  this.generatingStartedAt = 0;
178
203
  } else if (newStatus === 'stopped') {
204
+ // Cancel any pending debounce
205
+ if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
206
+ this.generatingDebouncePending = null;
179
207
  this.pushEvent({ event: 'agent:stopped', chatTitle, timestamp: now });
180
208
  }
181
209
  this.lastStatus = newStatus;
@@ -25,28 +25,11 @@ export interface ReadChatResult {
25
25
  isWelcomeScreen?: boolean;
26
26
  inputContent?: string;
27
27
  model?: string;
28
- mode?: string;
29
28
  autoApprove?: string;
30
29
  }
31
30
 
32
- export interface ChatMessage {
33
- role: 'user' | 'assistant' | 'system';
34
- /** Plain text (legacy) or rich content blocks (ACP standard) */
35
- content: string | ContentBlock[];
36
- /** Optional: unique message ID */
37
- id?: string;
38
- /** Optional: order index */
39
- index?: number;
40
- /** Optional: timestamp */
41
- timestamp?: number;
42
- /** Optional: receivedAt (assigned by daemon) */
43
- receivedAt?: number;
44
- /** Optional: tool calls associated with this message */
45
- toolCalls?: ToolCallInfo[];
46
- /** Optional: fiber metadata */
47
- _type?: string;
48
- _sub?: string;
49
- }
31
+ import type { ChatMessage } from '../types.js';
32
+ export type { ChatMessage };
50
33
 
51
34
  export type AgentStatus =
52
35
  | 'idle'
@@ -124,13 +107,13 @@ export interface ResourceBlock {
124
107
  export interface TextResourceContents {
125
108
  uri: string;
126
109
  text: string;
127
- mimeType?: string;
110
+ mimeType?: string | null;
128
111
  }
129
112
 
130
113
  export interface BlobResourceContents {
131
114
  uri: string;
132
115
  blob: string; // base64-encoded
133
- mimeType?: string;
116
+ mimeType?: string | null;
134
117
  }
135
118
 
136
119
  export interface ContentAnnotations {
@@ -163,7 +146,7 @@ export type ToolCallContent =
163
146
 
164
147
  export interface ToolCallLocation {
165
148
  path: string;
166
- line?: number;
149
+ line?: number | null;
167
150
  }
168
151
 
169
152
  // ─── Content Helpers ────────────────────────────────────
@@ -40,8 +40,7 @@ export class IdeProviderInstance implements ProviderInstance {
40
40
  // IDE meta
41
41
  private ideVersion: string = '';
42
42
  private instanceId: string;
43
- private workspaceFolders: { name: string; path: string }[] = [];
44
- private activeFile: string | null = null;
43
+ private workspace: string = '';
45
44
 
46
45
  // ─── Child Extension Instances ────────────────────
47
46
  private extensions = new Map<string, ExtensionProviderInstance>();
@@ -119,8 +118,7 @@ export class IdeProviderInstance implements ProviderInstance {
119
118
  activeModal: this.cachedChat.activeModal || null,
120
119
  inputContent: this.cachedChat.inputContent || '',
121
120
  } : null,
122
- workspaceFolders: this.workspaceFolders,
123
- activeFile: this.activeFile,
121
+ workspace: this.workspace || null,
124
122
  extensions: extensionStates,
125
123
  cdpConnected: cdp?.isConnected || false,
126
124
  currentModel: this.cachedChat?.model || undefined,
@@ -135,15 +133,10 @@ export class IdeProviderInstance implements ProviderInstance {
135
133
 
136
134
  onEvent(event: string, data?: any): void {
137
135
  if (event === 'cdp_connected') {
138
- // CDP connectiondone
136
+ // CDP connection done
139
137
  } else if (event === 'cdp_disconnected') {
140
138
  this.cachedChat = null;
141
139
  this.currentStatus = 'idle';
142
- } else if (event === 'extension_data') {
143
- if (data?.workspaceFolders) this.workspaceFolders = data.workspaceFolders;
144
- if (data?.activeFile) this.activeFile = data.activeFile;
145
- if (data?.ideVersion) this.ideVersion = data.ideVersion;
146
- if (data?.instanceId) this.instanceId = data.instanceId;
147
140
  } else if (event === 'stream_update') {
148
141
  // Forward to Extension
149
142
  const extType = data?.extensionType;
@@ -211,6 +204,11 @@ export class IdeProviderInstance implements ProviderInstance {
211
204
  return [...this.extensions.values()];
212
205
  }
213
206
 
207
+ /** Set workspace from daemon launch context */
208
+ setWorkspace(workspace: string): void {
209
+ this.workspace = workspace;
210
+ }
211
+
214
212
  // ─── CDP readChat ───────────────────────────────
215
213
 
216
214
  private async readChat(): Promise<void> {
@@ -9,57 +9,94 @@
9
9
  */
10
10
 
11
11
  import type { ProviderModule, ProviderSettingDef } from './contracts.js';
12
+ import type { AcpConfigOption, AcpMode } from '../shared-types.js';
13
+ import type { ChatMessage } from '../types.js';
12
14
 
13
- // ─── ProviderState — Standard status structure ─────────────
15
+ // ─── ProviderState — Discriminated union by category ─────────────
14
16
 
15
- export interface ProviderState {
17
+ export type ProviderStatus = 'idle' | 'generating' | 'waiting_approval' | 'error' | 'stopped' | 'starting';
18
+
19
+ export interface ActiveChatData {
20
+ id: string;
21
+ title: string;
22
+ status: string;
23
+ messages: ChatMessage[];
24
+ activeModal: { message: string; buttons: string[] } | null;
25
+ inputContent?: string;
26
+ }
27
+
28
+ /** Standardized error reasons across all provider categories */
29
+ export type ProviderErrorReason =
30
+ | 'not_installed' // CLI/ACP binary not found
31
+ | 'auth_failed' // Authentication/API key error
32
+ | 'spawn_error' // Process spawn failure
33
+ | 'init_failed' // Initialization/handshake failure
34
+ | 'crash' // Unexpected process crash
35
+ | 'timeout' // Operation timeout
36
+ | 'cdp_error' // CDP connection failure (IDE)
37
+ | 'disconnected'; // Connection lost
38
+
39
+ /** Common fields shared by all provider categories */
40
+ interface ProviderStateBase {
16
41
  /** Provider type (e.g. 'gemini-cli', 'cursor', 'cline') */
17
42
  type: string;
18
43
  /** Provider Display name */
19
44
  name: string;
20
- /** category */
21
- category: 'cli' | 'ide' | 'extension' | 'acp';
22
45
  /** current status */
23
- status: 'idle' | 'generating' | 'waiting_approval' | 'error' | 'stopped' | 'starting';
24
-
25
- /** CLI mode: terminal = PTY stream, chat = parsed conversation */
26
- mode?: 'terminal' | 'chat';
27
-
46
+ status: ProviderStatus;
28
47
  /** chat data */
29
- activeChat: {
30
- id: string;
31
- title: string;
32
- status: string;
33
- messages: { role: string; content: string; timestamp?: number }[];
34
- activeModal: { message: string; buttons: string[] } | null;
35
- inputContent?: string;
36
- } | null;
37
-
38
- /** IDE/Extension -only */
39
- workspaceFolders?: { name: string; path: string }[];
40
- activeFile?: string | null;
41
- agentStreams?: any[];
42
- cdpConnected?: boolean;
43
- /** IDE child Extension Instance status */
44
- extensions?: ProviderState[];
45
-
46
- /** CLI -only */
47
- workingDir?: string;
48
-
49
- /** Runtime info (real-time detection from IDE/CLI) */
48
+ activeChat: ActiveChatData | null;
49
+ /** Workspace — project path or name (all categories) */
50
+ workspace?: string | null;
51
+ /** Runtime info (real-time detection) */
50
52
  currentModel?: string;
51
53
  currentPlan?: string;
52
- currentAutoApprove?: string;
53
-
54
+ /** Error details (when status === 'error') */
55
+ errorMessage?: string;
56
+ errorReason?: ProviderErrorReason;
54
57
  /** meta */
55
58
  instanceId: string;
56
59
  lastUpdated: number;
57
60
  settings: Record<string, any>;
58
-
59
61
  /** Event queue (cleared after daemon collects) */
60
62
  pendingEvents: ProviderEvent[];
61
63
  }
62
64
 
65
+ /** IDE provider state */
66
+ export interface IdeProviderState extends ProviderStateBase {
67
+ category: 'ide';
68
+ cdpConnected: boolean;
69
+ /** IDE child Extension Instance status */
70
+ extensions: ProviderState[];
71
+ currentAutoApprove?: string;
72
+ }
73
+
74
+ /** CLI provider state */
75
+ export interface CliProviderState extends ProviderStateBase {
76
+ category: 'cli';
77
+ /** terminal = PTY stream, chat = parsed conversation */
78
+ mode: 'terminal' | 'chat';
79
+ }
80
+
81
+ /** ACP provider state */
82
+ export interface AcpProviderState extends ProviderStateBase {
83
+ category: 'acp';
84
+ mode: 'chat';
85
+ /** ACP config options (model/mode selection) */
86
+ acpConfigOptions?: AcpConfigOption[];
87
+ /** ACP available modes */
88
+ acpModes?: AcpMode[];
89
+ }
90
+
91
+ /** Extension provider state */
92
+ export interface ExtensionProviderState extends ProviderStateBase {
93
+ category: 'extension';
94
+ agentStreams?: any[];
95
+ }
96
+
97
+ /** Discriminated union — switch on `.category` */
98
+ export type ProviderState = IdeProviderState | CliProviderState | AcpProviderState | ExtensionProviderState;
99
+
63
100
  export interface ProviderEvent {
64
101
  event: string;
65
102
  timestamp: number;