@adhdev/daemon-core 0.8.69 → 0.8.70

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 (48) hide show
  1. package/dist/agent-stream/manager.d.ts +2 -1
  2. package/dist/agent-stream/provider-adapter.d.ts +6 -2
  3. package/dist/agent-stream/types.d.ts +5 -2
  4. package/dist/commands/stream-commands.d.ts +2 -1
  5. package/dist/config/recent-activity.d.ts +4 -3
  6. package/dist/index.js +286 -64
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +286 -64
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/providers/contracts.d.ts +10 -0
  11. package/dist/providers/extension-provider-instance.d.ts +1 -0
  12. package/dist/providers/open-panel-support.d.ts +6 -0
  13. package/dist/providers/provider-instance.d.ts +2 -1
  14. package/dist/shared-types.d.ts +1 -1
  15. package/dist/types.d.ts +1 -0
  16. package/node_modules/@adhdev/session-host-core/package.json +1 -1
  17. package/package.json +1 -1
  18. package/src/agent-stream/forward.ts +1 -0
  19. package/src/agent-stream/manager.d.ts +2 -1
  20. package/src/agent-stream/manager.ts +20 -5
  21. package/src/agent-stream/provider-adapter.d.ts +3 -2
  22. package/src/agent-stream/provider-adapter.ts +123 -4
  23. package/src/agent-stream/types.d.ts +3 -2
  24. package/src/agent-stream/types.ts +5 -2
  25. package/src/cli-adapters/provider-cli-adapter.ts +3 -1
  26. package/src/commands/cli-manager.ts +12 -0
  27. package/src/commands/handler.ts +4 -2
  28. package/src/commands/router.ts +1 -0
  29. package/src/commands/stream-commands.d.ts +2 -1
  30. package/src/commands/stream-commands.ts +74 -2
  31. package/src/config/recent-activity.ts +25 -15
  32. package/src/daemon/dev-auto-implement.ts +2 -2
  33. package/src/daemon/dev-server.ts +2 -2
  34. package/src/daemon/scaffold-template.ts +12 -5
  35. package/src/providers/contracts.d.ts +10 -0
  36. package/src/providers/contracts.ts +14 -0
  37. package/src/providers/extension-provider-instance.ts +15 -2
  38. package/src/providers/ide-provider-instance.ts +2 -0
  39. package/src/providers/open-panel-support.ts +40 -0
  40. package/src/providers/provider-instance.d.ts +2 -1
  41. package/src/providers/provider-instance.ts +2 -1
  42. package/src/providers/provider-schema.ts +10 -0
  43. package/src/providers/read-chat-contract.ts +1 -0
  44. package/src/shared-types.d.ts +1 -1
  45. package/src/shared-types.ts +1 -0
  46. package/src/status/builders.ts +9 -23
  47. package/src/status/snapshot.ts +2 -2
  48. package/src/types.ts +1 -0
@@ -199,6 +199,16 @@ export interface SwitchSessionResult {
199
199
  clickY?: number;
200
200
  error?: string;
201
201
  }
202
+ export interface FocusEditorResult {
203
+ focused: boolean;
204
+ error?: string;
205
+ }
206
+ export interface OpenPanelResult {
207
+ opened: boolean;
208
+ visible: boolean;
209
+ focused?: boolean;
210
+ error?: string;
211
+ }
202
212
  /**
203
213
  * Method 1: Script-Click — script calls el.click() directly
204
214
  * Cursor Suitable for IDEs using div.cursor-pointer elements.
@@ -29,6 +29,7 @@ export declare class ExtensionProviderInstance implements ProviderInstance {
29
29
  private instanceId;
30
30
  private ideType;
31
31
  private chatId;
32
+ private providerSessionId;
32
33
  private chatTitle;
33
34
  private agentName;
34
35
  private extensionId;
@@ -0,0 +1,6 @@
1
+ import type { ProviderModule } from './contracts.js';
2
+ import type { SessionCapability } from '../shared-types.js';
3
+ export declare const IDE_PROVIDER_SESSION_CAPABILITIES_BASE: SessionCapability[];
4
+ export declare const EXTENSION_PROVIDER_SESSION_CAPABILITIES_BASE: SessionCapability[];
5
+ export declare function providerHasOpenPanelSupport(provider: Pick<ProviderModule, 'category' | 'scripts'>): boolean;
6
+ export declare function getProviderSessionCapabilities(provider: Pick<ProviderModule, 'category' | 'scripts'>, baseCapabilities: SessionCapability[]): SessionCapability[];
@@ -8,7 +8,7 @@
8
8
  * Each Instance manages its own status.
9
9
  */
10
10
  import type { ProviderResumeCapability } from './contracts.js';
11
- import type { AcpConfigOption, AcpMode, ProviderControlSchema, ProviderSummaryMetadata } from '../shared-types.js';
11
+ import type { AcpConfigOption, AcpMode, ProviderControlSchema, ProviderSummaryMetadata, SessionCapability } from '../shared-types.js';
12
12
  import type { ChatMessage } from '../types.js';
13
13
  export type ProviderStatus = 'idle' | 'generating' | 'waiting_approval' | 'error' | 'stopped' | 'starting';
14
14
  export interface ProviderRuntimeWriteOwner {
@@ -66,6 +66,7 @@ interface ProviderStateBase {
66
66
  pendingEvents: ProviderEvent[];
67
67
  runtime?: ProviderRuntimeInfo;
68
68
  resume?: ProviderResumeCapability;
69
+ sessionCapabilities?: SessionCapability[];
69
70
  /** Dynamic control current values */
70
71
  controlValues?: Record<string, string | number | boolean>;
71
72
  /** Provider-declared controls schema (from provider.controls) */
@@ -225,7 +225,7 @@ export type UnsubscribeRequest = {
225
225
  export type StandaloneWsStatusPayload = StatusReportPayload;
226
226
  export type SessionTransport = 'cdp-page' | 'cdp-webview' | 'pty' | 'acp';
227
227
  export type SessionKind = 'workspace' | 'agent';
228
- export type SessionCapability = 'read_chat' | 'send_message' | 'new_session' | 'list_sessions' | 'switch_session' | 'resolve_action' | 'terminal_io' | 'resize_terminal' | 'change_model' | 'set_mode' | 'set_thought_level';
228
+ export type SessionCapability = 'read_chat' | 'send_message' | 'new_session' | 'list_sessions' | 'switch_session' | 'resolve_action' | 'open_panel' | 'terminal_io' | 'resize_terminal' | 'change_model' | 'set_mode' | 'set_thought_level';
229
229
  import type { RuntimeWriteOwner, RuntimeAttachedClient, SessionStatus } from './shared-types-extra.js';
230
230
  export type { RuntimeWriteOwner, RuntimeAttachedClient, SessionStatus } from './shared-types-extra.js';
231
231
  export interface SessionEntry {
package/dist/types.d.ts CHANGED
@@ -29,6 +29,7 @@ export interface ChatMessage {
29
29
  index?: number;
30
30
  timestamp?: number;
31
31
  receivedAt?: number;
32
+ _turnKey?: string;
32
33
  /** Tool calls associated with this message */
33
34
  toolCalls?: ToolCallInfo[];
34
35
  /** Optional: fiber metadata */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.8.69",
3
+ "version": "0.8.70",
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.8.69",
3
+ "version": "0.8.70",
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",
@@ -34,6 +34,7 @@ export function forwardAgentStreamsToIdeInstance(
34
34
  summaryMetadata: stream.summaryMetadata || undefined,
35
35
  effects: stream.effects || undefined,
36
36
  sessionId: stream.sessionId || stream.instanceId || undefined,
37
+ providerSessionId: stream.providerSessionId || stream.sessionId || undefined,
37
38
  title: stream.title || stream.agentName || undefined,
38
39
  agentType: stream.agentType || undefined,
39
40
  agentName: stream.agentName || undefined,
@@ -52,7 +52,8 @@ export declare class DaemonAgentStreamManager {
52
52
  newSession(cdp: DaemonCdpManager, sessionId: string): Promise<boolean>;
53
53
  listSessionChats(cdp: DaemonCdpManager, sessionId: string): Promise<AgentChatListItem[]>;
54
54
  switchConversation(cdp: DaemonCdpManager, sessionId: string, conversationId: string): Promise<boolean>;
55
- focusSession(cdp: DaemonCdpManager, sessionId: string): Promise<boolean>;
55
+ selectSession(cdp: DaemonCdpManager, sessionId: string): Promise<boolean>;
56
+ openSessionPanel(cdp: DaemonCdpManager, sessionId: string): Promise<boolean>;
56
57
  getConnectedSessions(parentSessionId?: string): string[];
57
58
  getManagedSession(sessionId: string): ManagedAgent | undefined;
58
59
  dispose(cdpManagers: Map<string, DaemonCdpManager>): Promise<void>;
@@ -322,20 +322,35 @@ export class DaemonAgentStreamManager {
322
322
  }
323
323
  }
324
324
 
325
- async focusSession(cdp: DaemonCdpManager, sessionId: string): Promise<boolean> {
325
+ async selectSession(cdp: DaemonCdpManager, sessionId: string): Promise<boolean> {
326
326
  const target = this.getSessionTarget(sessionId);
327
327
  if (!target?.parentSessionId) return false;
328
328
  await this.setActiveSession(cdp, target.parentSessionId, sessionId);
329
329
  await this.syncActiveSession(cdp, target.parentSessionId);
330
+ return this.managedBySessionId.has(sessionId);
331
+ }
332
+
333
+ async openSessionPanel(cdp: DaemonCdpManager, sessionId: string): Promise<boolean> {
334
+ const selected = await this.selectSession(cdp, sessionId);
335
+ if (!selected) return false;
330
336
  const agent = this.managedBySessionId.get(sessionId);
331
- if (!agent || typeof agent.adapter.focusEditor !== 'function') return false;
337
+ if (!agent) return false;
338
+ let panelVisible = false;
339
+ let editorFocused = false;
332
340
  try {
333
341
  const evaluate: AgentEvaluateFn = (expr, timeout) =>
334
342
  cdp.evaluateInSessionFrame(agent.cdpSessionId, expr, timeout);
335
- await agent.adapter.focusEditor(evaluate);
336
- return true;
343
+ if (typeof agent.adapter.openPanel === 'function') {
344
+ const result = await agent.adapter.openPanel(evaluate);
345
+ panelVisible = result.visible;
346
+ }
347
+ if (typeof agent.adapter.focusEditor === 'function') {
348
+ const result = await agent.adapter.focusEditor(evaluate);
349
+ editorFocused = result.focused;
350
+ }
351
+ return panelVisible || editorFocused;
337
352
  } catch (e) {
338
- this.logFn(`[AgentStream] focusEditor(${sessionId}) error: ${(e as Error).message}`);
353
+ this.logFn(`[AgentStream] openPanel(${sessionId}) error: ${(e as Error).message}`);
339
354
  return false;
340
355
  }
341
356
  }
@@ -5,7 +5,7 @@
5
5
  * Auto-configured using provider.js scripts + metadata.
6
6
  */
7
7
  import type { IAgentStreamAdapter, AgentStreamState, AgentChatListItem, AgentEvaluateFn } from './types.js';
8
- import type { ProviderModule } from '../providers/contracts.js';
8
+ import type { ProviderModule, FocusEditorResult, OpenPanelResult } from '../providers/contracts.js';
9
9
  export declare class ProviderStreamAdapter implements IAgentStreamAdapter {
10
10
  readonly agentType: string;
11
11
  readonly agentName: string;
@@ -31,6 +31,7 @@ export declare class ProviderStreamAdapter implements IAgentStreamAdapter {
31
31
  newSession(evaluate: AgentEvaluateFn): Promise<void>;
32
32
  listChats(evaluate: AgentEvaluateFn): Promise<AgentChatListItem[]>;
33
33
  switchSession(evaluate: AgentEvaluateFn, sessionId: string): Promise<boolean>;
34
- focusEditor(evaluate: AgentEvaluateFn): Promise<void>;
34
+ focusEditor(evaluate: AgentEvaluateFn): Promise<FocusEditorResult>;
35
+ openPanel(evaluate: AgentEvaluateFn): Promise<OpenPanelResult>;
35
36
  private errorState;
36
37
  }
@@ -11,7 +11,7 @@ import type {
11
11
  AgentChatListItem,
12
12
  AgentEvaluateFn,
13
13
  } from './types.js';
14
- import type { ProviderModule, ProviderScripts } from '../providers/contracts.js';
14
+ import type { ProviderModule, ProviderScripts, FocusEditorResult, OpenPanelResult } from '../providers/contracts.js';
15
15
  import { extractProviderControlValues, normalizeProviderEffects } from '../providers/control-effects.js';
16
16
  import { validateReadChatResultPayload } from '../providers/read-chat-contract.js';
17
17
  import { resolveProviderStateSurface } from '../providers/provider-patch-state.js';
@@ -57,6 +57,110 @@ export class ProviderStreamAdapter implements IAgentStreamAdapter {
57
57
  }
58
58
  }
59
59
 
60
+ private getOptionalError(raw: unknown): string | undefined {
61
+ return typeof raw === 'string' && raw.trim() ? raw.trim() : undefined;
62
+ }
63
+
64
+ private normalizeFocusEditorResult(raw: unknown): FocusEditorResult {
65
+ const data = this.parseMaybeJson(raw);
66
+ if (data === true) return { focused: true };
67
+ if (data === false || data == null) return { focused: false };
68
+ if (typeof data === 'string') {
69
+ const trimmed = data.trim();
70
+ const normalized = trimmed.toLowerCase();
71
+ if (
72
+ normalized === 'true'
73
+ || normalized === 'ok'
74
+ || normalized === 'success'
75
+ || normalized === 'focused'
76
+ || normalized === 'visible'
77
+ ) {
78
+ return { focused: true };
79
+ }
80
+ if (
81
+ normalized === 'false'
82
+ || normalized === 'not_found'
83
+ || normalized === 'not found'
84
+ || normalized === 'missing'
85
+ || normalized === 'panel_hidden'
86
+ || normalized === 'hidden'
87
+ ) {
88
+ return { focused: false };
89
+ }
90
+ return { focused: false, ...(trimmed ? { error: trimmed } : {}) };
91
+ }
92
+ if (data && typeof data === 'object') {
93
+ const error = this.getOptionalError(data.error);
94
+ if (data.focused === true || data.success === true || data.ok === true || data.visible === true) {
95
+ return { focused: true };
96
+ }
97
+ if (data.focused === false || data.success === false || data.ok === false || error) {
98
+ return { focused: false, ...(error ? { error } : {}) };
99
+ }
100
+ }
101
+ return { focused: false };
102
+ }
103
+
104
+ private normalizeOpenPanelResult(raw: unknown): OpenPanelResult {
105
+ const data = this.parseMaybeJson(raw);
106
+ if (data === true) return { opened: true, visible: true };
107
+ if (data === false || data == null) return { opened: false, visible: false };
108
+ if (typeof data === 'string') {
109
+ const trimmed = data.trim();
110
+ const normalized = trimmed.toLowerCase();
111
+ if (
112
+ normalized === 'true'
113
+ || normalized === 'ok'
114
+ || normalized === 'opened'
115
+ || normalized === 'open'
116
+ || normalized === 'success'
117
+ ) {
118
+ return { opened: true, visible: true };
119
+ }
120
+ if (normalized === 'visible') {
121
+ return { opened: false, visible: true };
122
+ }
123
+ if (normalized === 'focused') {
124
+ return { opened: false, visible: true, focused: true };
125
+ }
126
+ if (
127
+ normalized === 'false'
128
+ || normalized === 'panel_hidden'
129
+ || normalized === 'hidden'
130
+ || normalized === 'not_found'
131
+ || normalized === 'not found'
132
+ || normalized === 'missing'
133
+ ) {
134
+ return { opened: false, visible: false };
135
+ }
136
+ return { opened: false, visible: false, ...(trimmed ? { error: trimmed } : {}) };
137
+ }
138
+ if (data && typeof data === 'object') {
139
+ const error = this.getOptionalError(data.error);
140
+ const focused = data.focused === true;
141
+ const visible = data.visible === true
142
+ || data.opened === true
143
+ || focused
144
+ || data.success === true
145
+ || data.ok === true;
146
+ if (visible) {
147
+ return {
148
+ opened: data.opened === true,
149
+ visible: true,
150
+ ...(focused ? { focused: true } : {}),
151
+ };
152
+ }
153
+ if (data.opened === false || data.visible === false || data.success === false || data.ok === false || error) {
154
+ return {
155
+ opened: false,
156
+ visible: false,
157
+ ...(error ? { error } : {}),
158
+ };
159
+ }
160
+ }
161
+ return { opened: false, visible: false };
162
+ }
163
+
60
164
  private summarizeRaw(raw: unknown): string {
61
165
  try {
62
166
  if (typeof raw === 'string') return raw.replace(/\s+/g, ' ').trim().slice(0, 240);
@@ -167,6 +271,13 @@ export class ProviderStreamAdapter implements IAgentStreamAdapter {
167
271
  if (typeof validated.title === 'string' && validated.title.trim()) {
168
272
  state.title = validated.title.trim();
169
273
  }
274
+ const providerSessionId = typeof validated.providerSessionId === 'string' && validated.providerSessionId.trim()
275
+ ? validated.providerSessionId.trim()
276
+ : '';
277
+ if (providerSessionId) {
278
+ state.sessionId = providerSessionId;
279
+ state.providerSessionId = providerSessionId;
280
+ }
170
281
  const controlValues = extractProviderControlValues(this.provider.controls, validated);
171
282
  const surface = resolveProviderStateSurface({
172
283
  controlValues,
@@ -303,10 +414,18 @@ export class ProviderStreamAdapter implements IAgentStreamAdapter {
303
414
  return false;
304
415
  }
305
416
 
306
- async focusEditor(evaluate: AgentEvaluateFn): Promise<void> {
417
+ async focusEditor(evaluate: AgentEvaluateFn): Promise<FocusEditorResult> {
307
418
  const script = this.callScript('focusEditor');
308
- if (!script) return;
309
- await evaluate(script);
419
+ if (!script) return { focused: false };
420
+ const raw = await evaluate(script);
421
+ return this.normalizeFocusEditorResult(raw);
422
+ }
423
+
424
+ async openPanel(evaluate: AgentEvaluateFn): Promise<OpenPanelResult> {
425
+ const script = this.callScript('openPanel');
426
+ if (!script) return { opened: false, visible: false };
427
+ const raw = await evaluate(script);
428
+ return this.normalizeOpenPanelResult(raw);
310
429
  }
311
430
 
312
431
  private errorState(message: string): AgentStreamState {
@@ -4,7 +4,7 @@
4
4
  * Agent stream types.
5
5
  * No vscode dependency — can be used as-is.
6
6
  */
7
- import type { ProviderEffect } from '../providers/contracts.js';
7
+ import type { ProviderEffect, FocusEditorResult, OpenPanelResult } from '../providers/contracts.js';
8
8
  import type { ProviderSummaryMetadata } from '../shared-types.js';
9
9
  /** Agent chat message */
10
10
  export interface AgentChatMessage {
@@ -61,7 +61,8 @@ export interface IAgentStreamAdapter {
61
61
  newSession(evaluate: AgentEvaluateFn): Promise<void>;
62
62
  listChats?(evaluate: AgentEvaluateFn): Promise<AgentChatListItem[]>;
63
63
  switchSession?(evaluate: AgentEvaluateFn, sessionId: string): Promise<boolean>;
64
- focusEditor?(evaluate: AgentEvaluateFn): Promise<void>;
64
+ focusEditor?(evaluate: AgentEvaluateFn): Promise<FocusEditorResult>;
65
+ openPanel?(evaluate: AgentEvaluateFn): Promise<OpenPanelResult>;
65
66
  setProvider?(provider: any): void;
66
67
  }
67
68
  export type AgentEvaluateFn = (expression: string, timeoutMs?: number) => Promise<unknown>;
@@ -5,7 +5,7 @@
5
5
  * No vscode dependency — can be used as-is.
6
6
  */
7
7
 
8
- import type { ProviderEffect } from '../providers/contracts.js';
8
+ import type { ProviderEffect, FocusEditorResult, OpenPanelResult } from '../providers/contracts.js';
9
9
  import type { ProviderSummaryMetadata } from '../shared-types.js';
10
10
  import type { ChatMessageKind } from '../providers/chat-message-normalization.js';
11
11
 
@@ -38,6 +38,8 @@ export interface AgentStreamState {
38
38
  agentType: string;
39
39
  agentName: string;
40
40
  extensionId: string;
41
+ sessionId?: string;
42
+ providerSessionId?: string;
41
43
  status: 'idle' | 'streaming' | 'waiting_approval' | 'error' | 'disconnected' | 'panel_hidden' | 'not_monitored';
42
44
  messages: AgentChatMessage[];
43
45
  inputContent: string;
@@ -74,7 +76,8 @@ export interface IAgentStreamAdapter {
74
76
  newSession(evaluate: AgentEvaluateFn): Promise<void>;
75
77
  listChats?(evaluate: AgentEvaluateFn): Promise<AgentChatListItem[]>;
76
78
  switchSession?(evaluate: AgentEvaluateFn, sessionId: string): Promise<boolean>;
77
- focusEditor?(evaluate: AgentEvaluateFn): Promise<void>;
79
+ focusEditor?(evaluate: AgentEvaluateFn): Promise<FocusEditorResult>;
80
+ openPanel?(evaluate: AgentEvaluateFn): Promise<OpenPanelResult>;
78
81
  setProvider?(provider: any): void;
79
82
  }
80
83
 
@@ -1458,7 +1458,9 @@ export class ProviderCliAdapter implements CliAdapter {
1458
1458
  }
1459
1459
  }
1460
1460
  if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.currentStatus})`);
1461
- if (this.isWaitingForResponse) return;
1461
+ if (this.isWaitingForResponse) {
1462
+ throw new Error(`${this.cliName} is still processing the previous prompt`);
1463
+ }
1462
1464
  const blockingModal = this.activeModal || this.getStartupConfirmationModal(this.terminalScreen.getText() || '');
1463
1465
  if (blockingModal || this.currentStatus === 'waiting_approval') {
1464
1466
  throw new Error(`${this.cliName} is awaiting confirmation before it can accept a prompt`);
@@ -883,6 +883,18 @@ export class DaemonCliManager {
883
883
  // UUID session target based search priority
884
884
  const found = this.findAdapter(cliType, { instanceKey: args?.targetSessionId, dir });
885
885
  if (found) {
886
+ // If we got here via fuzzy match (no targetSessionId, no dir), check for ambiguity.
887
+ // If multiple sessions of the same type exist, refuse to stop without a targetSessionId.
888
+ if (!args?.targetSessionId && !dir) {
889
+ const matchCount = [...this.adapters.values()].filter((a) => a.cliType === cliType).length;
890
+ if (matchCount > 1) {
891
+ return {
892
+ success: false,
893
+ error: `Multiple ${cliType} sessions running — provide targetSessionId to stop a specific session`,
894
+ code: 'AMBIGUOUS_SESSION',
895
+ };
896
+ }
897
+ }
886
898
  await this.stopSessionWithMode(found.key, mode);
887
899
  } else {
888
900
  console.log(colorize('yellow', ` ⚠ No adapter found for ${cliType}`));
@@ -413,7 +413,8 @@ export class DaemonCommandHandler implements CommandHelpers {
413
413
  'change_model',
414
414
  'set_thought_level',
415
415
  'resolve_action',
416
- 'focus_session',
416
+ 'select_session',
417
+ 'open_panel',
417
418
  'pty_input',
418
419
  'pty_resize',
419
420
  'invoke_provider_script',
@@ -494,7 +495,8 @@ export class DaemonCommandHandler implements CommandHelpers {
494
495
  case 'refresh_scripts': return this.handleRefreshScripts(args);
495
496
 
496
497
  // ─── Stream commands (stream-commands.ts) ───────────
497
- case 'focus_session': return Stream.handleFocusSession(this, args);
498
+ case 'select_session': return Stream.handleSelectSession(this, args);
499
+ case 'open_panel': return Stream.handleOpenPanel(this, args);
498
500
 
499
501
  // ─── PTY Raw I/O (stream-commands.ts) ─────────
500
502
  case 'pty_input': return Stream.handlePtyInput(this, args);
@@ -718,6 +718,7 @@ export class DaemonCommandRouter {
718
718
  sessionId,
719
719
  typeof args?.seenAt === 'number' ? args.seenAt : Date.now(),
720
720
  completionMarker,
721
+ targetSession?.providerSessionId,
721
722
  );
722
723
  if (READ_DEBUG_ENABLED) {
723
724
  LOG.info('RecentRead', `mark_session_seen sessionId=${sessionId} seenAt=${String(args?.seenAt || '')} prevSeenAt=${String(prevSeenAt)} nextSeenAt=${String(next.sessionReads?.[sessionId] || 0)} marker=${completionMarker || '-'}`);
@@ -3,7 +3,8 @@
3
3
  * IDE Extension Settings, Extension Script Execution
4
4
  */
5
5
  import type { CommandResult, CommandHelpers } from './handler.js';
6
- export declare function handleFocusSession(h: CommandHelpers, args: any): Promise<CommandResult>;
6
+ export declare function handleSelectSession(h: CommandHelpers, args: any): Promise<CommandResult>;
7
+ export declare function handleOpenPanel(h: CommandHelpers, args: any): Promise<CommandResult>;
7
8
  export declare function handlePtyInput(h: CommandHelpers, args: any): CommandResult;
8
9
  export declare function handlePtyResize(h: CommandHelpers, args: any): CommandResult;
9
10
  export declare function handleGetProviderSettings(h: CommandHelpers, args: any): CommandResult;
@@ -28,14 +28,86 @@ function getCliPresentationMode(h: CommandHelpers, targetSessionId?: string): 't
28
28
  return mode === 'chat' || mode === 'terminal' ? mode : null;
29
29
  }
30
30
 
31
- export async function handleFocusSession(h: CommandHelpers, args: any): Promise<CommandResult> {
31
+ function normalizeOpenPanelCommandResult(result: CommandResult): { opened: boolean; visible: boolean; focused: boolean } {
32
+ const payload = Object.prototype.hasOwnProperty.call(result, 'result') ? result.result : result;
33
+ if (payload === true) return { opened: true, visible: true, focused: false };
34
+ if (!payload) return { opened: false, visible: false, focused: false };
35
+ if (typeof payload === 'string') {
36
+ const normalized = payload.trim().toLowerCase();
37
+ if (normalized === 'visible') return { opened: false, visible: true, focused: false };
38
+ if (normalized === 'focused') return { opened: false, visible: true, focused: true };
39
+ if (normalized === 'opened' || normalized === 'open' || normalized === 'true' || normalized === 'ok' || normalized === 'success') {
40
+ return { opened: true, visible: true, focused: false };
41
+ }
42
+ return { opened: false, visible: false, focused: false };
43
+ }
44
+ if (typeof payload === 'object') {
45
+ const record = payload as Record<string, unknown>;
46
+ return {
47
+ opened: record.opened === true,
48
+ visible: record.visible === true || record.opened === true || record.focused === true,
49
+ focused: record.focused === true,
50
+ };
51
+ }
52
+ return { opened: false, visible: false, focused: false };
53
+ }
54
+
55
+ function normalizeFocusEditorCommandResult(result: CommandResult): { focused: boolean } {
56
+ const payload = Object.prototype.hasOwnProperty.call(result, 'result') ? result.result : result;
57
+ if (payload === true) return { focused: true };
58
+ if (!payload) return { focused: false };
59
+ if (typeof payload === 'string') {
60
+ const normalized = payload.trim().toLowerCase();
61
+ return { focused: normalized === 'focused' || normalized === 'visible' || normalized === 'true' || normalized === 'ok' || normalized === 'success' };
62
+ }
63
+ if (typeof payload === 'object') {
64
+ const record = payload as Record<string, unknown>;
65
+ return { focused: record.focused === true || record.visible === true || record.success === true || record.ok === true };
66
+ }
67
+ return { focused: false };
68
+ }
69
+
70
+ export async function handleSelectSession(h: CommandHelpers, args: any): Promise<CommandResult> {
32
71
  if (!h.agentStream || !h.getCdp()) return { success: false, error: 'AgentStream or CDP not available' };
33
72
  const sessionId = args?.targetSessionId || h.currentSession?.sessionId;
34
73
  if (!sessionId) return { success: false, error: 'targetSessionId required' };
35
- const ok = await h.agentStream.focusSession(h.getCdp()!, sessionId);
74
+ const ok = await h.agentStream.selectSession(h.getCdp()!, sessionId);
36
75
  return { success: ok };
37
76
  }
38
77
 
78
+ export async function handleOpenPanel(h: CommandHelpers, args: any): Promise<CommandResult> {
79
+ const cdp = h.getCdp();
80
+ if (!cdp) return { success: false, error: 'AgentStream or CDP not available' };
81
+ const sessionId = args?.targetSessionId || h.currentSession?.sessionId;
82
+ if (!sessionId) return { success: false, error: 'targetSessionId required' };
83
+
84
+ const currentTransport = h.currentSession?.transport;
85
+ const shouldUseAgentStream = !!h.agentStream
86
+ && currentTransport !== 'cdp-page'
87
+ && currentTransport !== 'pty'
88
+ && currentTransport !== 'acp';
89
+ if (shouldUseAgentStream) {
90
+ const ok = await h.agentStream.openSessionPanel(cdp, sessionId);
91
+ return { success: ok };
92
+ }
93
+
94
+ const openResult = await executeProviderScript(h, args, 'openPanel');
95
+ if (!openResult.success) return openResult;
96
+ const revealState = normalizeOpenPanelCommandResult(openResult);
97
+
98
+ let focusState = { focused: false };
99
+ const focusResult = await executeProviderScript(h, args, 'focusEditor');
100
+ if (focusResult.success) {
101
+ focusState = normalizeFocusEditorCommandResult(focusResult);
102
+ }
103
+
104
+ return {
105
+ ...openResult,
106
+ ...(focusState.focused ? { focused: true } : {}),
107
+ success: revealState.visible || focusState.focused,
108
+ };
109
+ }
110
+
39
111
  // ─── PTY Raw I/O ──────────────────────────────────
40
112
 
41
113
  export function handlePtyInput(h: CommandHelpers, args: any): CommandResult {
@@ -83,12 +83,20 @@ export function getRecentActivity(state: DaemonState, limit = 20): RecentActivit
83
83
  .slice(0, limit);
84
84
  }
85
85
 
86
- export function getSessionSeenAt(state: DaemonState, sessionId: string): number {
87
- return state.sessionReads?.[sessionId] || 0;
86
+ export function buildSessionReadStateKey(sessionId: string, providerSessionId?: string | null): string {
87
+ const normalizedProviderSessionId = typeof providerSessionId === 'string' ? providerSessionId.trim() : '';
88
+ if (normalizedProviderSessionId) return `provider:${normalizedProviderSessionId}`;
89
+ return sessionId;
88
90
  }
89
91
 
90
- export function getSessionSeenMarker(state: DaemonState, sessionId: string): string {
91
- return state.sessionReadMarkers?.[sessionId] || '';
92
+ export function getSessionSeenAt(state: DaemonState, sessionId: string, providerSessionId?: string | null): number {
93
+ const providerKey = buildSessionReadStateKey(sessionId, providerSessionId);
94
+ return state.sessionReads?.[providerKey] || state.sessionReads?.[sessionId] || 0;
95
+ }
96
+
97
+ export function getSessionSeenMarker(state: DaemonState, sessionId: string, providerSessionId?: string | null): string {
98
+ const providerKey = buildSessionReadStateKey(sessionId, providerSessionId);
99
+ return state.sessionReadMarkers?.[providerKey] || state.sessionReadMarkers?.[sessionId] || '';
92
100
  }
93
101
 
94
102
  export function markSessionSeen(
@@ -96,22 +104,24 @@ export function markSessionSeen(
96
104
  sessionId: string,
97
105
  seenAt = Date.now(),
98
106
  completionMarker?: string | null,
107
+ providerSessionId?: string | null,
99
108
  ): DaemonState {
100
109
  const prev = state.sessionReads || {};
101
- const nextSeenAt = Math.max(prev[sessionId] || 0, seenAt);
102
110
  const prevMarkers = state.sessionReadMarkers || {};
103
111
  const nextMarker = typeof completionMarker === 'string' ? completionMarker : '';
112
+ const readKeys = Array.from(new Set([
113
+ sessionId,
114
+ buildSessionReadStateKey(sessionId, providerSessionId),
115
+ ].filter(Boolean)));
116
+ const nextSessionReads = { ...prev };
117
+ const nextSessionReadMarkers = { ...prevMarkers };
118
+ for (const key of readKeys) {
119
+ nextSessionReads[key] = Math.max(prev[key] || 0, seenAt);
120
+ if (nextMarker) nextSessionReadMarkers[key] = nextMarker;
121
+ }
104
122
  return {
105
123
  ...state,
106
- sessionReads: {
107
- ...prev,
108
- [sessionId]: nextSeenAt,
109
- },
110
- sessionReadMarkers: nextMarker
111
- ? {
112
- ...prevMarkers,
113
- [sessionId]: nextMarker,
114
- }
115
- : prevMarkers,
124
+ sessionReads: nextSessionReads,
125
+ sessionReadMarkers: nextMarker ? nextSessionReadMarkers : prevMarkers,
116
126
  };
117
127
  }
@@ -898,8 +898,8 @@ export function buildAutoImplPrompt(ctx: DevServerContext,
898
898
  lines.push('| setModel | `{ success: true/false }` |');
899
899
  lines.push('| listModes | `{ modes: [{ name, id }], current }` |');
900
900
  lines.push('| setMode | `{ success: true/false }` |');
901
- lines.push('| focusEditor | `{ focused: true/false }` |');
902
- lines.push('| openPanel | `{ opened: true/false }` |');
901
+ lines.push('| focusEditor | `{ focused: true/false, error? }` |');
902
+ lines.push('| openPanel | `{ opened: true/false, visible: true/false, focused?: true, error? }` |');
903
903
  lines.push('');
904
904
 
905
905
  // ── readChat.status lifecycle spec ──
@@ -1221,8 +1221,8 @@ export class DevServer implements DevServerContext {
1221
1221
  lines.push('| setModel | `{ success: true/false }` |');
1222
1222
  lines.push('| listModes | `{ modes: [{ name, id }], current }` |');
1223
1223
  lines.push('| setMode | `{ success: true/false }` |');
1224
- lines.push('| focusEditor | `{ focused: true/false }` |');
1225
- lines.push('| openPanel | `{ opened: true/false }` |');
1224
+ lines.push('| focusEditor | `{ focused: true/false, error? }` |');
1225
+ lines.push('| openPanel | `{ opened: true/false, visible: true/false, focused?: true, error? }` |');
1226
1226
  lines.push('');
1227
1227
 
1228
1228
  // ── readChat.status lifecycle spec ──