@adhdev/daemon-core 0.9.39 → 0.9.41

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.
@@ -15,7 +15,7 @@
15
15
  * 5. dispose() → kill process
16
16
  */
17
17
  import type { ProviderModule, ContentBlock, InputEnvelope } from './contracts.js';
18
- import type { ProviderInstance, AcpProviderState, InstanceContext } from './provider-instance.js';
18
+ import type { ProviderInstance, AcpProviderState, InstanceContext, SessionModalState } from './provider-instance.js';
19
19
  export declare function buildAcpPromptParts(input: InputEnvelope, agentCapabilities?: Record<string, any>): ContentBlock[];
20
20
  export declare class AcpProviderInstance implements ProviderInstance {
21
21
  private cliArgs;
@@ -59,6 +59,7 @@ export declare class AcpProviderInstance implements ProviderInstance {
59
59
  constructor(provider: ProviderModule, workingDir: string, cliArgs?: string[]);
60
60
  init(context: InstanceContext): Promise<void>;
61
61
  onTick(): Promise<void>;
62
+ getSessionModalState(): SessionModalState;
62
63
  getState(): AcpProviderState;
63
64
  onEvent(event: string, data?: any): void;
64
65
  getInstanceId(): string;
@@ -5,7 +5,7 @@
5
5
  * collectCliData() + status transition logic from daemon-status.ts moved here.
6
6
  */
7
7
  import { type ProviderModule } from './contracts.js';
8
- import type { ProviderInstance, ProviderState, InstanceContext, HotChatSessionState } from './provider-instance.js';
8
+ import type { ProviderInstance, ProviderState, InstanceContext, HotChatSessionState, SessionModalState } from './provider-instance.js';
9
9
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
10
10
  import type { PtyTransportFactory } from '../cli-adapters/pty-transport.js';
11
11
  type PersistableCliHistoryMessage = {
@@ -91,6 +91,7 @@ export declare class CliProviderInstance implements ProviderInstance {
91
91
  setPresentationMode(mode: 'terminal' | 'chat'): void;
92
92
  getPresentationMode(): 'terminal' | 'chat';
93
93
  getHotChatSessionState(): HotChatSessionState;
94
+ getSessionModalState(): SessionModalState;
94
95
  updateSettings(newSettings: Record<string, any>): void;
95
96
  onEvent(event: string, data?: any): void;
96
97
  dispose(): void;
@@ -5,7 +5,7 @@
5
5
  * CDP webview discovery + agent stream collection moved here.
6
6
  */
7
7
  import { type ProviderModule } from './contracts.js';
8
- import type { ProviderInstance, ProviderState, InstanceContext } from './provider-instance.js';
8
+ import type { ProviderInstance, ProviderState, InstanceContext, SessionModalState } from './provider-instance.js';
9
9
  export declare class ExtensionProviderInstance implements ProviderInstance {
10
10
  readonly type: string;
11
11
  readonly category: "extension";
@@ -37,6 +37,7 @@ export declare class ExtensionProviderInstance implements ProviderInstance {
37
37
  init(context: InstanceContext): Promise<void>;
38
38
  onTick(): Promise<void>;
39
39
  getState(): ProviderState;
40
+ getSessionModalState(sessionId?: string): SessionModalState | null;
40
41
  onEvent(event: string, data?: any): void;
41
42
  dispose(): void;
42
43
  updateSettings(newSettings: Record<string, any>): void;
@@ -9,7 +9,7 @@
9
9
  * Daemon collects all via a single IDE Instance.getState() call.
10
10
  */
11
11
  import { type ProviderModule } from './contracts.js';
12
- import type { ProviderInstance, ProviderState, InstanceContext } from './provider-instance.js';
12
+ import type { ProviderInstance, ProviderState, InstanceContext, SessionModalState } from './provider-instance.js';
13
13
  import { ExtensionProviderInstance } from './extension-provider-instance.js';
14
14
  export declare class IdeProviderInstance implements ProviderInstance {
15
15
  readonly type: string;
@@ -37,6 +37,7 @@ export declare class IdeProviderInstance implements ProviderInstance {
37
37
  init(context: InstanceContext): Promise<void>;
38
38
  onTick(): Promise<void>;
39
39
  getState(): ProviderState;
40
+ getSessionModalState(sessionId?: string): SessionModalState | null;
40
41
  onEvent(event: string, data?: any): void;
41
42
  dispose(): void;
42
43
  updateSettings(newSettings: Record<string, any>): void;
@@ -7,7 +7,7 @@
7
7
  * 3. Collect overall state
8
8
  * 4. Event collection and propagation
9
9
  */
10
- import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext, HotChatSessionState } from './provider-instance.js';
10
+ import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext, HotChatSessionState, SessionModalState } from './provider-instance.js';
11
11
  export declare class ProviderInstanceManager {
12
12
  private instances;
13
13
  private tickTimer;
@@ -46,6 +46,9 @@ export declare class ProviderInstanceManager {
46
46
  */
47
47
  collectAllStates(): ProviderState[];
48
48
  collectHotChatSessionStates(): HotChatSessionState[];
49
+ getSessionModalState(sessionId: string, options?: {
50
+ instanceKey?: string | null;
51
+ }): SessionModalState | null;
49
52
  /**
50
53
  * Per-category status collect
51
54
  */
@@ -123,6 +123,12 @@ export interface HotChatSessionState {
123
123
  runtimeRestoredFromStorage?: unknown;
124
124
  runtimeRecoveryState?: unknown;
125
125
  }
126
+ export interface SessionModalState {
127
+ id: string;
128
+ status?: unknown;
129
+ title?: unknown;
130
+ activeModal?: unknown;
131
+ }
126
132
  export interface InstanceContext {
127
133
  /** CDP connection (IDE/Extension) */
128
134
  cdp?: {
@@ -159,6 +165,11 @@ export interface ProviderInstance {
159
165
  * parsing here; callers use this on P2P hot flush paths.
160
166
  */
161
167
  getHotChatSessionState?(): HotChatSessionState | HotChatSessionState[] | null;
168
+ /**
169
+ * Return the cheap modal metadata for a single session subscription. This is
170
+ * used on P2P topic flushes and must not invoke rich chat/transcript parsing.
171
+ */
172
+ getSessionModalState?(sessionId?: string): SessionModalState | null;
162
173
  /** Receive event (external → Instance) */
163
174
  onEvent(event: string, data?: any): void;
164
175
  /** Update settings at runtime (called when user changes settings from dashboard) */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.9.39",
3
+ "version": "0.9.41",
4
4
  "description": "ADHDev local session host core \u2014 session registry, protocol, buffers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.39",
3
+ "version": "0.9.41",
4
4
  "description": "ADHDev daemon core \u2014 CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -273,12 +273,10 @@ export class ProviderCliAdapter implements CliAdapter {
273
273
  currentTurnScope: TurnParseScope | null;
274
274
  recentOutputBuffer: string;
275
275
  accumulatedBuffer: string;
276
- accumulatedRawBuffer: string;
277
276
  screenText: string;
278
277
  currentStatus: CliSessionStatus['status'];
279
278
  activeModal: { message: string; buttons: string[] } | null;
280
279
  cliName: string;
281
- lastOutputAt: number;
282
280
  result: any;
283
281
  } | null = null;
284
282
  private lastStatusHotPathParseAt = Number.NEGATIVE_INFINITY;
@@ -352,12 +350,10 @@ export class ProviderCliAdapter implements CliAdapter {
352
350
  && cached.currentTurnScope === this.currentTurnScope
353
351
  && cached.recentOutputBuffer === this.recentOutputBuffer
354
352
  && cached.accumulatedBuffer === this.accumulatedBuffer
355
- && cached.accumulatedRawBuffer === this.accumulatedRawBuffer
356
353
  && cached.screenText === this.lastScreenText
357
354
  && cached.currentStatus === this.currentStatus
358
355
  && cached.activeModal === this.activeModal
359
356
  && cached.cliName === this.cliName
360
- && cached.lastOutputAt === this.lastOutputAt
361
357
  ) {
362
358
  return cached.result;
363
359
  }
@@ -1835,12 +1831,10 @@ export class ProviderCliAdapter implements CliAdapter {
1835
1831
  && cached.currentTurnScope === this.currentTurnScope
1836
1832
  && cached.recentOutputBuffer === this.recentOutputBuffer
1837
1833
  && cached.accumulatedBuffer === this.accumulatedBuffer
1838
- && cached.accumulatedRawBuffer === this.accumulatedRawBuffer
1839
1834
  && cached.screenText === screenText
1840
1835
  && cached.currentStatus === this.currentStatus
1841
1836
  && cached.activeModal === this.activeModal
1842
1837
  && cached.cliName === this.cliName
1843
- && cached.lastOutputAt === this.lastOutputAt
1844
1838
  ) {
1845
1839
  return cached.result;
1846
1840
  }
@@ -2002,12 +1996,10 @@ export class ProviderCliAdapter implements CliAdapter {
2002
1996
  currentTurnScope: this.currentTurnScope,
2003
1997
  recentOutputBuffer: this.recentOutputBuffer,
2004
1998
  accumulatedBuffer: this.accumulatedBuffer,
2005
- accumulatedRawBuffer: this.accumulatedRawBuffer,
2006
1999
  screenText,
2007
2000
  currentStatus: this.currentStatus,
2008
2001
  activeModal: this.activeModal,
2009
2002
  cliName: this.cliName,
2010
- lastOutputAt: this.lastOutputAt,
2011
2003
  result,
2012
2004
  };
2013
2005
  return result;
@@ -173,10 +173,12 @@ export function hydrateCliParsedMessages(
173
173
  });
174
174
  }
175
175
 
176
- function chooseMoreComparableCliMessage(left: CliChatMessage, right: CliChatMessage): CliChatMessage {
177
- const leftComparable = normalizeComparableMessageContent(left.content || '');
178
- const rightComparable = normalizeComparableMessageContent(right.content || '');
179
-
176
+ function chooseMoreComparableCliMessage(
177
+ left: CliChatMessage,
178
+ right: CliChatMessage,
179
+ leftComparable = normalizeComparableMessageContent(left.content || ''),
180
+ rightComparable = normalizeComparableMessageContent(right.content || ''),
181
+ ): CliChatMessage {
180
182
  if (leftComparable && leftComparable === rightComparable) {
181
183
  const leftNewlines = String(left.content || '').split(/\r\n|\n|\r/g).length - 1;
182
184
  const rightNewlines = String(right.content || '').split(/\r\n|\n|\r/g).length - 1;
@@ -187,35 +189,43 @@ function chooseMoreComparableCliMessage(left: CliChatMessage, right: CliChatMess
187
189
  }
188
190
 
189
191
  function dedupeConsecutiveComparableCliMessages(messages: CliChatMessage[]): CliChatMessage[] {
190
- const deduped: CliChatMessage[] = [];
192
+ const deduped: Array<{ message: CliChatMessage; comparable: string }> = [];
191
193
 
192
194
  for (const message of messages) {
193
195
  const current = {
194
196
  ...message,
195
197
  content: typeof message.content === 'string' ? message.content : String(message.content || ''),
196
198
  } as CliChatMessage;
199
+ const currentComparable = normalizeComparableMessageContent(current.content || '');
197
200
  const previous = deduped[deduped.length - 1];
198
201
  if (!previous) {
199
- deduped.push(current);
202
+ deduped.push({ message: current, comparable: currentComparable });
200
203
  continue;
201
204
  }
202
205
 
203
- const previousComparable = normalizeComparableMessageContent(previous.content || '');
204
- const currentComparable = normalizeComparableMessageContent(current.content || '');
205
- const sameRole = previous.role === current.role;
206
- const sameKind = (previous.kind || 'standard') === (current.kind || 'standard');
207
- const sameSender = (previous.senderName || '') === (current.senderName || '');
208
- const comparableMatch = previousComparable && previousComparable === currentComparable;
206
+ const sameRole = previous.message.role === current.role;
207
+ const sameKind = (previous.message.kind || 'standard') === (current.kind || 'standard');
208
+ const sameSender = (previous.message.senderName || '') === (current.senderName || '');
209
+ const comparableMatch = previous.comparable && previous.comparable === currentComparable;
209
210
 
210
211
  if (sameRole && sameKind && sameSender && comparableMatch) {
211
- deduped[deduped.length - 1] = chooseMoreComparableCliMessage(previous, current);
212
+ const selected = chooseMoreComparableCliMessage(
213
+ previous.message,
214
+ current,
215
+ previous.comparable,
216
+ currentComparable,
217
+ );
218
+ deduped[deduped.length - 1] = {
219
+ message: selected,
220
+ comparable: selected === current ? currentComparable : previous.comparable,
221
+ };
212
222
  continue;
213
223
  }
214
224
 
215
- deduped.push(current);
225
+ deduped.push({ message: current, comparable: currentComparable });
216
226
  }
217
227
 
218
- return deduped;
228
+ return deduped.map((entry) => entry.message);
219
229
  }
220
230
 
221
231
  export function normalizeCliParsedMessages(
@@ -178,85 +178,90 @@ function normalizeReadChatMessages(payload: Record<string, any>): ChatMessage[]
178
178
  return normalizeChatMessages(messages);
179
179
  }
180
180
 
181
- function buildReadChatReplayCollapseSignature(message: ChatMessage | null | undefined): string {
182
- if (!message) return '';
183
- const role = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
184
- const kind = typeof message.kind === 'string' ? message.kind.trim().toLowerCase() : 'standard';
185
- const senderName = typeof message.senderName === 'string' ? message.senderName.trim().toLowerCase() : '';
186
- const content = flattenContent(message.content || '').replace(/\s+/g, ' ').trim();
187
- return `${role}:${kind}:${senderName}:${content}`;
188
- }
189
-
190
- function shouldCollapseReadChatReplayDuplicate(message: ChatMessage | null | undefined): boolean {
191
- if (!message) return false;
192
- const role = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
193
- return role === 'assistant' || role === 'system';
181
+ interface ReadChatReplayCollapseInfo {
182
+ role: string;
183
+ kind: string;
184
+ senderName: string;
185
+ content: string;
186
+ signature: string;
187
+ collapsible: boolean;
194
188
  }
195
189
 
196
- function normalizeReadChatReplayText(message: ChatMessage | null | undefined): string {
197
- return flattenContent(message?.content || '').replace(/\s+/g, ' ').trim();
190
+ function normalizeReadChatReplayTextContent(content: ChatMessage['content'] | undefined): string {
191
+ return flattenContent(content || '').replace(/\s+/g, ' ').trim();
198
192
  }
199
193
 
200
- function isStableReadChatAssistantAnswer(message: ChatMessage | null | undefined): boolean {
201
- if (!message) return false;
194
+ function getReadChatReplayCollapseInfo(message: ChatMessage | null | undefined): ReadChatReplayCollapseInfo | null {
195
+ if (!message) return null;
202
196
  const role = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
203
197
  const kind = typeof message.kind === 'string' ? message.kind.trim().toLowerCase() : 'standard';
204
- if (role !== 'assistant') return false;
205
- if (kind && kind !== 'standard') return false;
206
- const content = normalizeReadChatReplayText(message);
207
- if (content.length < 160) return false;
198
+ const senderName = typeof message.senderName === 'string' ? message.senderName.trim().toLowerCase() : '';
199
+ const collapsible = role === 'assistant' || role === 'system';
200
+ if (!collapsible) return { role, kind, senderName, content: '', signature: '', collapsible };
201
+ const content = normalizeReadChatReplayTextContent(message.content);
202
+ return {
203
+ role,
204
+ kind,
205
+ senderName,
206
+ content,
207
+ signature: `${role}:${kind}:${senderName}:${content}`,
208
+ collapsible,
209
+ };
210
+ }
211
+
212
+ function isStableReadChatAssistantAnswerInfo(info: ReadChatReplayCollapseInfo | null): boolean {
213
+ if (!info) return false;
214
+ if (info.role !== 'assistant') return false;
215
+ if (info.kind && info.kind !== 'standard') return false;
216
+ if (info.content.length < 160) return false;
208
217
 
209
218
  // A provider may surface expanded command output as a standard assistant bubble
210
219
  // (for example Claude Code's "Bash command ..." block). That is live work output,
211
220
  // not a stable final answer. Treating it as a terminal answer would hide the
212
221
  // real final response and violate read_chat fidelity.
213
- if (/^(bash|shell|terminal) command\b/i.test(content)) return false;
222
+ if (/^(bash|shell|terminal) command\b/i.test(info.content)) return false;
214
223
  return true;
215
224
  }
216
225
 
217
- function isReplayedAssistantAnswerAfterStableAnswer(
218
- message: ChatMessage | null | undefined,
219
- stableAnswer: ChatMessage | null,
226
+ function isReplayedAssistantAnswerAfterStableAnswerInfo(
227
+ info: ReadChatReplayCollapseInfo | null,
228
+ stableContent: string,
220
229
  ): boolean {
221
- if (!message || !stableAnswer) return false;
222
- const role = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
223
- const kind = typeof message.kind === 'string' ? message.kind.trim().toLowerCase() : 'standard';
224
- if (role !== 'assistant') return false;
225
- if (kind && kind !== 'standard') return false;
226
- const content = normalizeReadChatReplayText(message);
227
- const stableContent = normalizeReadChatReplayText(stableAnswer);
230
+ if (!info || !stableContent) return false;
231
+ if (info.role !== 'assistant') return false;
232
+ if (info.kind && info.kind !== 'standard') return false;
233
+ const content = info.content;
228
234
  if (content.length < 80 || stableContent.length < 80) return false;
229
235
  return content === stableContent || content.startsWith(stableContent) || stableContent.startsWith(content);
230
236
  }
231
237
 
232
- function collapseReplayDuplicatesFromReadChat(messages: ChatMessage[]): ChatMessage[] {
238
+ export function collapseReplayDuplicatesFromReadChat(messages: ChatMessage[]): ChatMessage[] {
233
239
  const collapsed: ChatMessage[] = [];
234
240
  const replaySignaturesInCurrentTurn = new Set<string>();
235
- let stableAssistantAnswerInCurrentTurn: ChatMessage | null = null;
241
+ let stableAssistantAnswerContentInCurrentTurn = '';
242
+ let previousReplaySignature = '';
236
243
 
237
244
  for (const message of messages) {
238
- const role = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
239
- if (role === 'user') {
245
+ const info = getReadChatReplayCollapseInfo(message);
246
+ if (info?.role === 'user') {
240
247
  replaySignaturesInCurrentTurn.clear();
241
- stableAssistantAnswerInCurrentTurn = null;
248
+ stableAssistantAnswerContentInCurrentTurn = '';
249
+ previousReplaySignature = '';
242
250
  }
243
251
 
244
- const signature = buildReadChatReplayCollapseSignature(message);
245
- const previous = collapsed[collapsed.length - 1];
246
- const previousSignature = buildReadChatReplayCollapseSignature(previous);
247
-
248
- if (shouldCollapseReadChatReplayDuplicate(message) && signature) {
249
- if (previousSignature === signature) continue;
250
- if (replaySignaturesInCurrentTurn.has(signature)) continue;
251
- if (isReplayedAssistantAnswerAfterStableAnswer(message, stableAssistantAnswerInCurrentTurn)) continue;
252
+ if (info?.collapsible && info.signature) {
253
+ if (previousReplaySignature === info.signature) continue;
254
+ if (replaySignaturesInCurrentTurn.has(info.signature)) continue;
255
+ if (isReplayedAssistantAnswerAfterStableAnswerInfo(info, stableAssistantAnswerContentInCurrentTurn)) continue;
252
256
  }
253
257
 
254
258
  collapsed.push(message);
255
- if (shouldCollapseReadChatReplayDuplicate(message) && signature) {
256
- replaySignaturesInCurrentTurn.add(signature);
259
+ previousReplaySignature = info?.collapsible ? info.signature : '';
260
+ if (info?.collapsible && info.signature) {
261
+ replaySignaturesInCurrentTurn.add(info.signature);
257
262
  }
258
- if (isStableReadChatAssistantAnswer(message)) {
259
- stableAssistantAnswerInCurrentTurn = message;
263
+ if (isStableReadChatAssistantAnswerInfo(info)) {
264
+ stableAssistantAnswerContentInCurrentTurn = info?.content || '';
260
265
  }
261
266
  }
262
267
 
@@ -369,13 +374,17 @@ function computeReadChatSync(messages: ChatMessage[], cursor: Required<ReadChatC
369
374
  }
370
375
 
371
376
  if (cursor.tailLimit > 0 && knownSignature === lastMessageSignature) {
372
- return {
373
- syncMode: 'noop',
374
- replaceFrom: totalMessages,
375
- messages: [],
376
- totalMessages,
377
- lastMessageSignature,
378
- };
377
+ const requestedTailCount = Math.min(totalMessages, cursor.tailLimit);
378
+ if (knownMessageCount >= requestedTailCount) {
379
+ return {
380
+ syncMode: 'noop',
381
+ replaceFrom: totalMessages,
382
+ messages: [],
383
+ totalMessages,
384
+ lastMessageSignature,
385
+ };
386
+ }
387
+ return buildBoundedTailSync(messages, cursor);
379
388
  }
380
389
 
381
390
  if (knownMessageCount < totalMessages) {
@@ -49,7 +49,7 @@ import {
49
49
  import type { ProviderModule, ContentBlock, InputEnvelope, ToolCallInfo, ToolCallContent as TCC, ToolKind, ToolCallStatus as TCS } from './contracts.js';
50
50
  import { normalizeContent, flattenContent, normalizeInputEnvelope } from './contracts.js';
51
51
  import { assertProviderSupportsDeclaredInput } from './provider-input-support.js';
52
- import type { ProviderInstance, ProviderState, AcpProviderState, ProviderErrorReason, ProviderEvent, InstanceContext } from './provider-instance.js';
52
+ import type { ProviderInstance, ProviderState, AcpProviderState, ProviderErrorReason, ProviderEvent, InstanceContext, SessionModalState } from './provider-instance.js';
53
53
  import { StatusMonitor } from './status-monitor.js';
54
54
  import { buildLegacyModelModeSummaryMetadata } from './summary-metadata.js';
55
55
  import {
@@ -283,6 +283,19 @@ export class AcpProviderInstance implements ProviderInstance {
283
283
  }
284
284
  }
285
285
 
286
+ getSessionModalState(): SessionModalState {
287
+ const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
288
+ return {
289
+ id: this.instanceId,
290
+ status: this.currentStatus,
291
+ title: `${this.provider.name} · ${dirName}`,
292
+ activeModal: this.currentStatus === 'waiting_approval' ? {
293
+ message: this.activeToolCalls.find(t => t.status === 'running')?.name || 'Permission requested',
294
+ buttons: ['Approve', 'Reject'],
295
+ } : null,
296
+ };
297
+ }
298
+
286
299
  getState(): AcpProviderState {
287
300
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
288
301
 
@@ -12,7 +12,7 @@ import * as fs from 'fs';
12
12
  import { createRequire } from 'node:module';
13
13
  import { normalizeInputEnvelope, type ProviderModule, flattenContent } from './contracts.js';
14
14
  import { assertTextOnlyInput } from './provider-input-support.js';
15
- import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext, ProviderErrorReason, HotChatSessionState } from './provider-instance.js';
15
+ import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext, ProviderErrorReason, HotChatSessionState, SessionModalState } from './provider-instance.js';
16
16
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
17
17
  import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
18
18
  import type { PtyRuntimeMetadata, PtyTransportFactory } from '../cli-adapters/pty-transport.js';
@@ -533,6 +533,19 @@ export class CliProviderInstance implements ProviderInstance {
533
533
  };
534
534
  }
535
535
 
536
+ getSessionModalState(): SessionModalState {
537
+ const adapterStatus = this.adapter.getStatus({ allowParse: false });
538
+ const autoApproveActive = adapterStatus.status === 'waiting_approval' && this.shouldAutoApprove();
539
+ const visibleStatus = autoApproveActive ? 'generating' : adapterStatus.status;
540
+ const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
541
+ return {
542
+ id: this.instanceId,
543
+ status: visibleStatus,
544
+ title: dirName,
545
+ activeModal: autoApproveActive ? null : adapterStatus.activeModal,
546
+ };
547
+ }
548
+
536
549
  updateSettings(newSettings: Record<string, any>): void {
537
550
  this.settings = { ...newSettings };
538
551
  this.adapter.updateRuntimeSettings?.(this.settings);
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { flattenContent, type ProviderModule } from './contracts.js';
9
- import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext } from './provider-instance.js';
9
+ import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext, SessionModalState } from './provider-instance.js';
10
10
  import { StatusMonitor } from './status-monitor.js';
11
11
  import { buildPersistedProviderEffectMessage, normalizeProviderEffects } from './control-effects.js';
12
12
  import { ChatHistoryWriter } from '../config/chat-history.js';
@@ -110,6 +110,16 @@ export class ExtensionProviderInstance implements ProviderInstance {
110
110
  };
111
111
  }
112
112
 
113
+ getSessionModalState(sessionId?: string): SessionModalState | null {
114
+ if (sessionId && sessionId !== this.instanceId) return null;
115
+ return {
116
+ id: this.instanceId,
117
+ status: this.currentStatus,
118
+ title: this.chatTitle || this.agentName || this.provider.name,
119
+ activeModal: this.activeModal,
120
+ };
121
+ }
122
+
113
123
  onEvent(event: string, data?: any): void {
114
124
  if (event === 'stream_update') {
115
125
  // Reflect data collected from agent-stream-manager
@@ -12,7 +12,7 @@
12
12
  import * as os from 'os';
13
13
  import * as crypto from 'crypto';
14
14
  import { flattenContent, type ProviderModule } from './contracts.js';
15
- import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext } from './provider-instance.js';
15
+ import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext, SessionModalState } from './provider-instance.js';
16
16
  import { ExtensionProviderInstance } from './extension-provider-instance.js';
17
17
  import { StatusMonitor } from './status-monitor.js';
18
18
  import { ChatHistoryWriter } from '../config/chat-history.js';
@@ -172,6 +172,30 @@ export class IdeProviderInstance implements ProviderInstance {
172
172
  };
173
173
  }
174
174
 
175
+ getSessionModalState(sessionId?: string): SessionModalState | null {
176
+ if (sessionId && sessionId !== this.instanceId) {
177
+ for (const ext of this.extensions.values()) {
178
+ const projected = ext.getSessionModalState?.(sessionId);
179
+ if (projected?.id === sessionId) return projected;
180
+ }
181
+ return null;
182
+ }
183
+
184
+ const autoApproveActive = (
185
+ this.currentStatus === 'waiting_approval'
186
+ || this.cachedChat?.status === 'waiting_approval'
187
+ ) && this.canAutoApprove();
188
+ const visibleStatus = autoApproveActive ? 'generating' : this.currentStatus;
189
+ return {
190
+ id: this.instanceId,
191
+ status: autoApproveActive && this.cachedChat?.status === 'waiting_approval'
192
+ ? 'generating'
193
+ : (this.cachedChat?.status || visibleStatus),
194
+ title: this.cachedChat?.title || this.type,
195
+ activeModal: autoApproveActive ? null : (this.cachedChat?.activeModal || null),
196
+ };
197
+ }
198
+
175
199
  onEvent(event: string, data?: any): void {
176
200
  if (event === 'cdp_connected') {
177
201
  // CDP connection done
@@ -8,7 +8,7 @@
8
8
  * 4. Event collection and propagation
9
9
  */
10
10
 
11
- import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext, HotChatSessionState } from './provider-instance.js';
11
+ import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext, HotChatSessionState, SessionModalState } from './provider-instance.js';
12
12
  import { LOG } from '../logging/logger.js';
13
13
 
14
14
  function projectHotChatSessionStatesFromProviderState(state: ProviderState): HotChatSessionState[] {
@@ -165,6 +165,32 @@ export class ProviderInstanceManager {
165
165
  return sessions;
166
166
  }
167
167
 
168
+ getSessionModalState(sessionId: string, options: { instanceKey?: string | null } = {}): SessionModalState | null {
169
+ if (!sessionId) return null;
170
+ const candidates = [sessionId];
171
+ if (options.instanceKey && options.instanceKey !== sessionId) {
172
+ candidates.push(options.instanceKey);
173
+ }
174
+
175
+ for (const id of candidates) {
176
+ const instance = this.instances.get(id);
177
+ if (!instance?.getSessionModalState) continue;
178
+ try {
179
+ const projected = instance.getSessionModalState(sessionId);
180
+ if (!projected?.id) continue;
181
+ if (projected.id !== sessionId) {
182
+ LOG.warn('InstanceMgr', `[InstanceManager] Ignoring mismatched session modal projection from ${id}: requested=${sessionId} projected=${projected.id}`);
183
+ continue;
184
+ }
185
+ return projected;
186
+ } catch (e) {
187
+ LOG.warn('InstanceMgr', `[InstanceManager] Failed to project session modal metadata from ${id}: ${(e as Error).message}`);
188
+ }
189
+ }
190
+
191
+ return null;
192
+ }
193
+
168
194
  /**
169
195
  * Per-category status collect
170
196
  */
@@ -147,6 +147,13 @@ export interface HotChatSessionState {
147
147
  runtimeRecoveryState?: unknown;
148
148
  }
149
149
 
150
+ export interface SessionModalState {
151
+ id: string;
152
+ status?: unknown;
153
+ title?: unknown;
154
+ activeModal?: unknown;
155
+ }
156
+
150
157
  // ─── ProviderInstance interface ─────────────────
151
158
 
152
159
  export interface InstanceContext {
@@ -191,6 +198,12 @@ export interface ProviderInstance {
191
198
  */
192
199
  getHotChatSessionState?(): HotChatSessionState | HotChatSessionState[] | null;
193
200
 
201
+ /**
202
+ * Return the cheap modal metadata for a single session subscription. This is
203
+ * used on P2P topic flushes and must not invoke rich chat/transcript parsing.
204
+ */
205
+ getSessionModalState?(sessionId?: string): SessionModalState | null;
206
+
194
207
  /** Receive event (external → Instance) */
195
208
  onEvent(event: string, data?: any): void;
196
209