@adhdev/daemon-core 0.6.76 → 0.6.79

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.
@@ -71,8 +71,8 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
71
71
  // Alternative: AgentStreamManager (script fail when)
72
72
  if (h.agentStream) {
73
73
  const cdp = h.getCdp();
74
- if (cdp) {
75
- const streams = await h.agentStream.collectAgentStreams(cdp);
74
+ if (cdp && h.currentIdeType) {
75
+ const streams = await h.agentStream.collectAgentStreams(cdp, h.currentIdeType);
76
76
  const stream = streams.find((s: any) => s.agentType === provider.type);
77
77
  if (stream) {
78
78
  h.historyWriter.appendNewMessages(
@@ -197,8 +197,8 @@ export async function handleSendChat(h: CommandHelpers, args: any): Promise<Comm
197
197
  _log(`Extension script error: ${e.message}`);
198
198
  }
199
199
  // Method 2: AgentStreamManager
200
- if (h.agentStream && h.getCdp()) {
201
- const ok = await h.agentStream.sendToAgent(h.getCdp()!, provider.type, text, h.currentIdeType);
200
+ if (h.agentStream && h.getCdp() && h.currentIdeType) {
201
+ const ok = await h.agentStream.sendToAgent(h.getCdp()!, h.currentIdeType, provider.type, text, h.currentIdeType);
202
202
  if (ok) {
203
203
  _log(`AgentStreamManager sent OK`);
204
204
  return _logSendSuccess('agent-stream');
@@ -318,9 +318,9 @@ export async function handleListChats(h: CommandHelpers, args: any): Promise<Com
318
318
  const provider = h.getProvider(args?.agentType);
319
319
 
320
320
  // Extension: via AgentStreamManager
321
- if (provider?.category === 'extension' && h.agentStream && h.getCdp()) {
321
+ if (provider?.category === 'extension' && h.agentStream && h.getCdp() && h.currentIdeType) {
322
322
  try {
323
- const chats = await h.agentStream.listAgentChats(h.getCdp()!, provider.type);
323
+ const chats = await h.agentStream.listAgentChats(h.getCdp()!, h.currentIdeType, provider.type);
324
324
  LOG.info('Command', `[list_chats] Extension: ${chats.length} chats`);
325
325
  return { success: true, chats };
326
326
  } catch (e: any) {
@@ -377,8 +377,8 @@ export async function handleNewChat(h: CommandHelpers, args: any): Promise<Comma
377
377
  return { success: false, error: 'new_chat not supported by this CLI provider' };
378
378
  }
379
379
 
380
- if (provider?.category === 'extension' && h.agentStream && h.getCdp()) {
381
- const ok = await h.agentStream.newAgentSession(h.getCdp()!, provider.type, h.currentIdeType);
380
+ if (provider?.category === 'extension' && h.agentStream && h.getCdp() && h.currentIdeType) {
381
+ const ok = await h.agentStream.newAgentSession(h.getCdp()!, h.currentIdeType, provider.type, h.currentIdeType);
382
382
  return { success: ok };
383
383
  }
384
384
 
@@ -412,8 +412,8 @@ export async function handleSwitchChat(h: CommandHelpers, args: any): Promise<Co
412
412
  if (!sessionId) return { success: false, error: 'sessionId required' };
413
413
  LOG.info('Command', `[switch_chat] sessionId=${sessionId}, ideType=${ideType}`);
414
414
 
415
- if (provider?.category === 'extension' && h.agentStream && h.getCdp()) {
416
- const ok = await h.agentStream.switchAgentSession(h.getCdp()!, provider.type, sessionId);
415
+ if (provider?.category === 'extension' && h.agentStream && h.getCdp() && h.currentIdeType) {
416
+ const ok = await h.agentStream.switchAgentSession(h.getCdp()!, h.currentIdeType, provider.type, sessionId);
417
417
  return { success: ok, result: ok ? 'switched' : 'failed' };
418
418
  }
419
419
 
@@ -681,9 +681,9 @@ export async function handleResolveAction(h: CommandHelpers, args: any): Promise
681
681
  }
682
682
 
683
683
  // 1. Extension: via AgentStreamManager
684
- if (provider?.category === 'extension' && h.agentStream && h.getCdp()) {
684
+ if (provider?.category === 'extension' && h.agentStream && h.getCdp() && h.currentIdeType) {
685
685
  const ok = await h.agentStream.resolveAgentAction(
686
- h.getCdp()!, provider.type, action, h.currentIdeType
686
+ h.getCdp()!, h.currentIdeType, provider.type, action, h.currentIdeType
687
687
  );
688
688
  return { success: ok };
689
689
  }
@@ -97,7 +97,7 @@ export class DaemonCommandHandler implements CommandHelpers {
97
97
  // 1. Try instanceIdMap (UUID → managerKey)
98
98
  const resolved = this._ctx.instanceIdMap?.get(key) || key;
99
99
  // 2. Use findCdpManager (exact + prefix match)
100
- const m = findCdpManager(this._ctx.cdpManagers, resolved.toLowerCase());
100
+ const m = findCdpManager(this._ctx.cdpManagers, resolved);
101
101
  if (m?.isConnected) return m;
102
102
  return null;
103
103
  }
@@ -148,11 +148,11 @@ export class DaemonCommandHandler implements CommandHelpers {
148
148
 
149
149
  // Extension: evaluateInSession
150
150
  if (provider?.category === 'extension') {
151
- let sessionId = this.getExtensionSessionId(provider);
152
- if (!sessionId && this._agentStream) {
153
- await this._agentStream.switchActiveAgent(cdp, provider.type);
154
- await this._agentStream.syncAgentSessions(cdp);
155
- sessionId = this.getExtensionSessionId(provider);
151
+ let sessionId = this.getExtensionSessionId(provider, this._currentIdeType);
152
+ if (!sessionId && this._agentStream && this._currentIdeType) {
153
+ await this._agentStream.switchActiveAgent(cdp, this._currentIdeType, provider.type);
154
+ await this._agentStream.syncAgentSessions(cdp, this._currentIdeType);
155
+ sessionId = this.getExtensionSessionId(provider, this._currentIdeType);
156
156
  }
157
157
  if (!sessionId) return null;
158
158
  const result = await cdp.evaluateInSessionFrame(sessionId, script, timeout);
@@ -194,18 +194,48 @@ export class DaemonCommandHandler implements CommandHelpers {
194
194
 
195
195
  // ─── Private helpers ──────────────────────────────
196
196
 
197
- private getExtensionSessionId(provider: ProviderModule): string | null {
198
- if (provider.category !== 'extension' || !this._agentStream) return null;
199
- const managed = this._agentStream.getManagedAgent(provider.type);
197
+ private getExtensionSessionId(provider: ProviderModule, scopeKey?: string): string | null {
198
+ if (provider.category !== 'extension' || !this._agentStream || !scopeKey) return null;
199
+ const managed = this._agentStream.getManagedAgent(provider.type, scopeKey);
200
200
  return managed?.sessionId || null;
201
201
  }
202
202
 
203
+ private resolveManagerKeyFromInstanceId(instanceId: string): string | undefined {
204
+ const mapped = this._ctx.instanceIdMap?.get(instanceId);
205
+ if (mapped) return mapped;
206
+
207
+ const entries = (this._ctx.instanceManager as any)?.instances?.entries?.();
208
+ if (!entries) return undefined;
209
+
210
+ for (const [instanceKey, instance] of entries as Iterable<[string, any]>) {
211
+ if (typeof instanceKey !== 'string' || !instanceKey.startsWith('ide:')) continue;
212
+
213
+ if (typeof instance?.getInstanceId === 'function' && instance.getInstanceId() === instanceId) {
214
+ const managerKey = instanceKey.slice(4);
215
+ this._ctx.instanceIdMap?.set(instanceId, managerKey);
216
+ return managerKey;
217
+ }
218
+
219
+ if (typeof instance?.getExtensionInstances === 'function') {
220
+ for (const ext of instance.getExtensionInstances() || []) {
221
+ if (typeof ext?.getInstanceId === 'function' && ext.getInstanceId() === instanceId) {
222
+ const managerKey = instanceKey.slice(4);
223
+ this._ctx.instanceIdMap?.set(instanceId, managerKey);
224
+ return managerKey;
225
+ }
226
+ }
227
+ }
228
+ }
229
+
230
+ return undefined;
231
+ }
232
+
203
233
  /** Extract ideType from _targetInstance or explicit ideType */
204
234
  private extractIdeType(args: any): string | undefined {
205
235
  // Also accept explicit ideType from args (P2P input, agentType for extensions)
206
236
  if (args?.ideType) {
207
237
  // UUID → managerKey via instanceIdMap (P2P sends UUID instance IDs)
208
- const mappedKey = this._ctx.instanceIdMap?.get(args.ideType);
238
+ const mappedKey = this.resolveManagerKeyFromInstanceId(args.ideType);
209
239
  if (mappedKey) {
210
240
  return mappedKey;
211
241
  }
@@ -232,8 +262,9 @@ export class DaemonCommandHandler implements CommandHelpers {
232
262
  else if (cliMatch) raw = cliMatch[1];
233
263
  else if (acpMatch) raw = acpMatch[1];
234
264
 
235
- if (this._ctx.instanceIdMap?.has(raw)) {
236
- return this._ctx.instanceIdMap.get(raw)!;
265
+ const mappedKey = this.resolveManagerKeyFromInstanceId(raw);
266
+ if (mappedKey) {
267
+ return mappedKey;
237
268
  }
238
269
 
239
270
  // Direct CDP manager key match (e.g. "cursor", "cursor_remote_vs")
@@ -11,15 +11,15 @@ import { LOG } from '../logging/logger.js';
11
11
  // ─── Agent Stream commands ───────────────────────
12
12
 
13
13
  export async function handleAgentStreamSwitch(h: CommandHelpers, args: any): Promise<CommandResult> {
14
- if (!h.agentStream || !h.getCdp()) return { success: false, error: 'AgentStream or CDP not available' };
14
+ if (!h.agentStream || !h.getCdp() || !h.currentIdeType) return { success: false, error: 'AgentStream or CDP not available' };
15
15
  const agentType = args?.agentType || args?.agent || null;
16
- await h.agentStream.switchActiveAgent(h.getCdp()!, agentType);
16
+ await h.agentStream.switchActiveAgent(h.getCdp()!, h.currentIdeType, agentType);
17
17
  return { success: true, activeAgent: agentType };
18
18
  }
19
19
 
20
20
  export async function handleAgentStreamRead(h: CommandHelpers, args: any): Promise<CommandResult> {
21
- if (!h.agentStream || !h.getCdp()) return { success: false, error: 'AgentStream or CDP not available' };
22
- const streams = await h.agentStream.collectAgentStreams(h.getCdp()!);
21
+ if (!h.agentStream || !h.getCdp() || !h.currentIdeType) return { success: false, error: 'AgentStream or CDP not available' };
22
+ const streams = await h.agentStream.collectAgentStreams(h.getCdp()!, h.currentIdeType);
23
23
  return { success: true, streams };
24
24
  }
25
25
 
@@ -46,52 +46,58 @@ export async function handleAgentStreamSend(h: CommandHelpers, args: any): Promi
46
46
 
47
47
  // CDP-based IDE agent routing
48
48
  if (!h.agentStream || !h.getCdp()) return { success: false, error: 'AgentStream or CDP not available' };
49
- const resolvedAgent = agentType || h.agentStream.activeAgentType;
49
+ const resolvedAgent = agentType || (h.currentIdeType ? h.agentStream.getActiveAgentType(h.currentIdeType) : null);
50
50
  if (!resolvedAgent) return { success: false, error: 'agentType required' };
51
- const ok = await h.agentStream.sendToAgent(h.getCdp()!, resolvedAgent, text, h.currentIdeType);
51
+ if (!h.currentIdeType) return { success: false, error: 'ideType required' };
52
+ const ok = await h.agentStream.sendToAgent(h.getCdp()!, h.currentIdeType, resolvedAgent, text, h.currentIdeType);
52
53
  return { success: ok };
53
54
  }
54
55
 
55
56
  export async function handleAgentStreamResolve(h: CommandHelpers, args: any): Promise<CommandResult> {
56
57
  if (!h.agentStream || !h.getCdp()) return { success: false, error: 'AgentStream or CDP not available' };
57
- const agentType = args?.agentType || args?.agent || h.agentStream.activeAgentType;
58
+ const agentType = args?.agentType || args?.agent || (h.currentIdeType ? h.agentStream.getActiveAgentType(h.currentIdeType) : null);
58
59
  const action = args?.action as 'approve' | 'reject' || 'approve';
59
60
  if (!agentType) return { success: false, error: 'agentType required' };
60
- const ok = await h.agentStream.resolveAgentAction(h.getCdp()!, agentType, action, h.currentIdeType);
61
+ if (!h.currentIdeType) return { success: false, error: 'ideType required' };
62
+ const ok = await h.agentStream.resolveAgentAction(h.getCdp()!, h.currentIdeType, agentType, action, h.currentIdeType);
61
63
  return { success: ok };
62
64
  }
63
65
 
64
66
  export async function handleAgentStreamNew(h: CommandHelpers, args: any): Promise<CommandResult> {
65
67
  if (!h.agentStream || !h.getCdp()) return { success: false, error: 'AgentStream or CDP not available' };
66
- const agentType = args?.agentType || args?.agent || h.agentStream.activeAgentType;
68
+ const agentType = args?.agentType || args?.agent || (h.currentIdeType ? h.agentStream.getActiveAgentType(h.currentIdeType) : null);
67
69
  if (!agentType) return { success: false, error: 'agentType required' };
68
- const ok = await h.agentStream.newAgentSession(h.getCdp()!, agentType, h.currentIdeType);
70
+ if (!h.currentIdeType) return { success: false, error: 'ideType required' };
71
+ const ok = await h.agentStream.newAgentSession(h.getCdp()!, h.currentIdeType, agentType, h.currentIdeType);
69
72
  return { success: ok };
70
73
  }
71
74
 
72
75
  export async function handleAgentStreamListChats(h: CommandHelpers, args: any): Promise<CommandResult> {
73
76
  if (!h.agentStream || !h.getCdp()) return { success: false, error: 'AgentStream or CDP not available' };
74
- const agentType = args?.agentType || args?.agent || h.agentStream.activeAgentType;
77
+ const agentType = args?.agentType || args?.agent || (h.currentIdeType ? h.agentStream.getActiveAgentType(h.currentIdeType) : null);
75
78
  if (!agentType) return { success: false, error: 'agentType required' };
76
- const chats = await h.agentStream.listAgentChats(h.getCdp()!, agentType);
79
+ if (!h.currentIdeType) return { success: false, error: 'ideType required' };
80
+ const chats = await h.agentStream.listAgentChats(h.getCdp()!, h.currentIdeType, agentType);
77
81
  return { success: true, chats };
78
82
  }
79
83
 
80
84
  export async function handleAgentStreamSwitchSession(h: CommandHelpers, args: any): Promise<CommandResult> {
81
85
  if (!h.agentStream || !h.getCdp()) return { success: false, error: 'AgentStream or CDP not available' };
82
- const agentType = args?.agentType || args?.agent || h.agentStream.activeAgentType;
86
+ const agentType = args?.agentType || args?.agent || (h.currentIdeType ? h.agentStream.getActiveAgentType(h.currentIdeType) : null);
83
87
  const sessionId = args?.sessionId || args?.id;
84
88
  if (!agentType || !sessionId) return { success: false, error: 'agentType and sessionId required' };
85
- const ok = await h.agentStream.switchAgentSession(h.getCdp()!, agentType, sessionId);
89
+ if (!h.currentIdeType) return { success: false, error: 'ideType required' };
90
+ const ok = await h.agentStream.switchAgentSession(h.getCdp()!, h.currentIdeType, agentType, sessionId);
86
91
  return { success: ok };
87
92
  }
88
93
 
89
94
  export async function handleAgentStreamFocus(h: CommandHelpers, args: any): Promise<CommandResult> {
90
95
  if (!h.agentStream || !h.getCdp()) return { success: false, error: 'AgentStream or CDP not available' };
91
- const agentType = args?.agentType || args?.agent || h.agentStream.activeAgentType;
96
+ const agentType = args?.agentType || args?.agent || (h.currentIdeType ? h.agentStream.getActiveAgentType(h.currentIdeType) : null);
92
97
  if (!agentType) return { success: false, error: 'agentType required' };
93
98
  await h.agentStream.ensureAgentPanelOpen(agentType, h.currentIdeType);
94
- const ok = await h.agentStream.focusAgentEditor(h.getCdp()!, agentType);
99
+ if (!h.currentIdeType) return { success: false, error: 'ideType required' };
100
+ const ok = await h.agentStream.focusAgentEditor(h.getCdp()!, h.currentIdeType, agentType);
95
101
  return { success: ok };
96
102
  }
97
103
 
@@ -2536,14 +2536,16 @@ export class DevServer {
2536
2536
  lines.push('| Status | When to use | How to detect |');
2537
2537
  lines.push('|---|---|---|');
2538
2538
  lines.push('| `idle` | AI is NOT generating, no approval needed | Default state. No stop button, no spinners, no approval pills/buttons |');
2539
- lines.push('| `generating` | AI is actively streaming/thinking | ANY of: (1) Stop/Cancel button visible, (2) CSS animation (animate-spin/pulse/bounce), (3) floating state text like Thinking/Generating/Sailing, (4) streaming indicator class |');
2539
+ lines.push('| `generating` | AI is actively streaming/thinking | ANY of: (1) Submit button icon SVG changes (e.g. arrow→stop square, fill="none"→fill="currentColor"), (2) Stop/Cancel button visible, (3) CSS animation, (4) Structural markers (aria-labels that only appear during generation) |');
2540
2540
  lines.push('| `waiting_approval` | AI stopped and needs user action | Actionable buttons like Run/Skip/Accept/Reject are visible AND clickable |');
2541
2541
  lines.push('');
2542
2542
  lines.push('### ⚠️ Status Detection Gotchas (MUST READ!)');
2543
- lines.push('1. **FALSE POSITIVES from old messages**: Chat history may contain text like "Command Awaiting Approval" from PAST turns. If you search the entire chat panel for this text, you will get false matches from parent divs whose innerText includes ALL child text. ONLY match small leaf elements (under 80 chars) or use explicit button/pill selectors.');
2544
- lines.push('2. **Awaiting Approval pill without actions**: Some IDEs show a floating pill/banner saying "Awaiting Approval" that is just a scroll-to indicator (not an actual approval dialog). If this pill exists but NO actionable buttons (Run/Skip/Accept/Reject) exist anywhere in the panel, the status should be `idle`, NOT `waiting_approval`.');
2545
- lines.push('3. **generating detection must be multi-signal**: Do NOT rely on just one indicator. Check ALL of: stop buttons, CSS animations, floating state labels, streaming classes. IDEs differ widely.');
2546
- lines.push('4. **activeModal must include actions**: When `status` is `waiting_approval`, the `activeModal` object MUST include a non-empty `actions` array listing the button labels. If you cannot find any action buttons, the status is NOT `waiting_approval`.');
2543
+ lines.push('1. **DO NOT rely on button text/labels in the user\'s language.** OS locale may be Korean, Japanese, etc. Button text like "Cancel" or "Stop" will be localized. Instead, detect STRUCTURAL indicators: SVG icon changes, CSS classes, aria-labels from the extension\'s own React/Radix UI (which stay in English regardless of OS locale).');
2544
+ lines.push('2. **Use sendMessage to CREATE a generating state, then CAPTURE the DOM.** Send a LONG prompt (e.g. "Write an extremely detailed 5000-word essay...") so the AI takes 10+ seconds. Then periodically capture the DOM during generation to find which elements appear/change. Compare idle vs generating DOM snapshots to find reliable structural markers.');
2545
+ lines.push('3. **Look for SVG icon changes in the submit button.** Many IDEs change the submit button icon from an arrow (send) to a square (stop) during generation. Check the SVG `fill` attribute or path data.');
2546
+ lines.push('4. **FALSE POSITIVES from old messages**: Chat history may contain text like "Command Awaiting Approval" from PAST turns. ONLY match small leaf elements (under 80 chars) or use explicit button/pill selectors.');
2547
+ lines.push('5. **Awaiting Approval pill without actions**: Some IDEs show a floating pill/banner that is just a scroll-to indicator. If NO actionable buttons exist, the status should be `idle`, NOT `waiting_approval`.');
2548
+ lines.push('6. **activeModal must include actions**: When `status` is `waiting_approval`, the `activeModal` object MUST include a non-empty `actions` array.');
2547
2549
  lines.push('');
2548
2550
 
2549
2551
  lines.push('## Action');
@@ -2606,10 +2608,10 @@ export class DevServer {
2606
2608
  lines.push(`echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); r=d.get('result',d); r=json.loads(r) if isinstance(r,str) else r; assert r.get('status')=='idle', f'Expected idle, got {r.get(chr(34)+chr(115)+chr(116)+chr(97)+chr(116)+chr(117)+chr(115)+chr(34))}'; print('Step 1 PASS: status=idle')"`);
2607
2609
  lines.push('```');
2608
2610
  lines.push('');
2609
- lines.push('### Step 2: Send a message that triggers generation');
2611
+ lines.push('### Step 2: Send a LONG message that triggers extended generation (10+ seconds)');
2610
2612
  lines.push('```bash');
2611
- lines.push(`curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/scripts/run -H "Content-Type: application/json" -d '{"script": "sendMessage", "type": "${type}", "ideType": "${type}", "args": {"message": "Say hello in one word"}}'`);
2612
- lines.push('sleep 2');
2613
+ lines.push(`curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/scripts/run -H "Content-Type: application/json" -d '{"script": "sendMessage", "type": "${type}", "ideType": "${type}", "args": {"message": "Write an extremely detailed 5000-word essay about the history of artificial intelligence from Alan Turing to 2025. Be very thorough and verbose."}}'`);
2614
+ lines.push('sleep 3');
2613
2615
  lines.push('```');
2614
2616
  lines.push('');
2615
2617
  lines.push('### Step 3: Check generating OR completed');
package/src/index.ts CHANGED
@@ -76,6 +76,8 @@ export type { CommandRouterDeps, CommandRouterResult } from './commands/router.j
76
76
  export { DaemonStatusReporter } from './status/reporter.js';
77
77
  export { buildManagedIdes, buildManagedClis, buildManagedAcps, buildAllManagedEntries, findCdpManager, hasCdpManager, isCdpConnected } from './status/builders.js';
78
78
  export { buildStatusSnapshot } from './status/snapshot.js';
79
+ export { normalizeManagedStatus, isManagedStatusWorking, isManagedStatusWaiting, normalizeActiveChatData } from './status/normalize.js';
80
+ export type { ManagedStatus } from './status/normalize.js';
79
81
  export type { StatusSnapshotOptions, StatusSnapshot } from './status/snapshot.js';
80
82
 
81
83
  // ── Logger ──
@@ -16,6 +16,7 @@ import type {
16
16
  AcpProviderState,
17
17
  ProviderState,
18
18
  } from '../providers/provider-instance.js';
19
+ import { normalizeActiveChatData, normalizeManagedStatus } from './normalize.js';
19
20
 
20
21
  // ─── CDP Manager lookup helpers ──────────────────────
21
22
 
@@ -100,13 +101,13 @@ export function buildManagedIdes(
100
101
  workspace: state.workspace || null,
101
102
  terminals: 0,
102
103
  aiAgents: [],
103
- activeChat: state.activeChat,
104
+ activeChat: normalizeActiveChatData(state.activeChat),
104
105
  chats: [],
105
106
  agentStreams: state.extensions.map((ext) => ({
106
107
  agentType: ext.type,
107
108
  agentName: ext.name,
108
109
  extensionId: ext.type,
109
- status: ext.status || 'idle',
110
+ status: normalizeManagedStatus(ext.status, { activeModal: ext.activeChat?.activeModal || null }),
110
111
  messages: ext.activeChat?.messages || [],
111
112
  inputContent: ext.activeChat?.inputContent || '',
112
113
  activeModal: ext.activeChat?.activeModal || null,
@@ -155,10 +156,10 @@ export function buildManagedClis(
155
156
  instanceId: s.instanceId,
156
157
  cliType: s.type,
157
158
  cliName: s.name,
158
- status: s.status,
159
+ status: normalizeManagedStatus(s.status, { activeModal: s.activeChat?.activeModal || null }),
159
160
  mode: 'terminal' as const,
160
161
  workspace: s.workspace || '',
161
- activeChat: s.activeChat,
162
+ activeChat: normalizeActiveChatData(s.activeChat),
162
163
  }));
163
164
  }
164
165
 
@@ -172,10 +173,10 @@ export function buildManagedAcps(
172
173
  id: s.instanceId,
173
174
  acpType: s.type,
174
175
  acpName: s.name,
175
- status: s.status,
176
+ status: normalizeManagedStatus(s.status, { activeModal: s.activeChat?.activeModal || null }),
176
177
  mode: 'chat' as const,
177
178
  workspace: s.workspace || '',
178
- activeChat: s.activeChat,
179
+ activeChat: normalizeActiveChatData(s.activeChat),
179
180
  currentModel: s.currentModel,
180
181
  currentPlan: s.currentPlan,
181
182
  acpConfigOptions: s.acpConfigOptions,
@@ -0,0 +1,64 @@
1
+ import type { ActiveChatData } from '../providers/provider-instance.js';
2
+
3
+ export type ManagedStatus =
4
+ | 'idle'
5
+ | 'generating'
6
+ | 'waiting_approval'
7
+ | 'error'
8
+ | 'stopped'
9
+ | 'starting'
10
+ | 'panel_hidden'
11
+ | 'not_monitored'
12
+ | 'disconnected';
13
+
14
+ const WORKING_STATUSES = new Set([
15
+ 'generating',
16
+ 'streaming',
17
+ 'loading',
18
+ 'loading_reference',
19
+ 'thinking',
20
+ 'active',
21
+ ]);
22
+
23
+ function hasApprovalButtons(activeModal?: { buttons?: unknown[] | null } | null): boolean {
24
+ return (activeModal?.buttons?.length ?? 0) > 0;
25
+ }
26
+
27
+ export function normalizeManagedStatus(
28
+ status?: string | null,
29
+ opts?: { activeModal?: { buttons?: unknown[] | null } | null },
30
+ ): ManagedStatus {
31
+ if (hasApprovalButtons(opts?.activeModal)) return 'waiting_approval';
32
+
33
+ const normalized = String(status || 'idle').trim().toLowerCase();
34
+ if (normalized === 'waiting_approval') return 'waiting_approval';
35
+ if (WORKING_STATUSES.has(normalized)) return 'generating';
36
+ if (normalized === 'error') return 'error';
37
+ if (normalized === 'stopped') return 'stopped';
38
+ if (normalized === 'starting') return 'starting';
39
+ if (normalized === 'panel_hidden') return 'panel_hidden';
40
+ if (normalized === 'not_monitored') return 'not_monitored';
41
+ if (normalized === 'disconnected') return 'disconnected';
42
+ return 'idle';
43
+ }
44
+
45
+ export function isManagedStatusWorking(status?: string | null): boolean {
46
+ return normalizeManagedStatus(status) === 'generating';
47
+ }
48
+
49
+ export function isManagedStatusWaiting(
50
+ status?: string | null,
51
+ opts?: { activeModal?: { buttons?: unknown[] | null } | null },
52
+ ): boolean {
53
+ return normalizeManagedStatus(status, opts) === 'waiting_approval';
54
+ }
55
+
56
+ export function normalizeActiveChatData<T extends ActiveChatData | null | undefined>(
57
+ activeChat: T,
58
+ ): T {
59
+ if (!activeChat) return activeChat;
60
+ return {
61
+ ...activeChat,
62
+ status: normalizeManagedStatus(activeChat.status, { activeModal: activeChat.activeModal }),
63
+ } as T;
64
+ }