@adhdev/daemon-core 0.7.39 → 0.7.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.
@@ -26,7 +26,16 @@ export declare class CliProviderInstance implements ProviderInstance {
26
26
  private lastApprovalEventAt;
27
27
  private historyWriter;
28
28
  readonly instanceId: string;
29
- constructor(provider: ProviderModule, workingDir: string, cliArgs?: string[], instanceId?: string, transportFactory?: PtyTransportFactory);
29
+ private launchMode;
30
+ private resolvedOutputFormat;
31
+ constructor(provider: ProviderModule, workingDir: string, cliArgs?: string[], instanceId?: string, transportFactory?: PtyTransportFactory, launchModeId?: string);
32
+ /**
33
+ * Determine output rendering format from:
34
+ * 1. launchMode.outputFormat (explicit override)
35
+ * 2. launchOptions[].outputFormatMap — check actual args for matching values
36
+ * 3. Default: 'terminal'
37
+ */
38
+ private resolveOutputFormat;
30
39
  init(context: InstanceContext): Promise<void>;
31
40
  onTick(): Promise<void>;
32
41
  getState(): ProviderState;
@@ -245,6 +245,20 @@ export interface ProviderModule {
245
245
  shell?: boolean;
246
246
  env?: Record<string, string>;
247
247
  };
248
+ /**
249
+ * Configurable options shown at session launch time (schema declaration).
250
+ * The frontend renders these as a launch config UI.
251
+ * Values are passed to launchArgBuilder to produce the final args.
252
+ */
253
+ launchOptions?: ProviderLaunchOption[];
254
+ /**
255
+ * Builds extra spawn args from user-selected launch option values.
256
+ * Called with the merged defaults + mode preset + user overrides.
257
+ * When defined, takes precedence over launchMode.extraArgs.
258
+ */
259
+ launchArgBuilder?: (options: Record<string, string | boolean | number>) => string[];
260
+ /** Named presets — shortcuts that set launchOption values in bulk */
261
+ launchModes?: ProviderLaunchMode[];
248
262
  patterns?: {
249
263
  prompt?: RegExp[];
250
264
  generating?: RegExp[];
@@ -306,6 +320,71 @@ export interface ProviderModule {
306
320
  /** ACP agent auth methods (multiple supported — in priority order) */
307
321
  auth?: AcpAuthMethod[];
308
322
  }
323
+ export type ProviderLaunchOptionType = 'select' | 'boolean' | 'string' | 'number';
324
+ /**
325
+ * A single configurable option exposed at session launch time.
326
+ * Providers declare these so the frontend can render a launch config UI.
327
+ * The final args are built by launchArgBuilder(selectedValues).
328
+ *
329
+ * Example — Claude Code:
330
+ * { id: 'outputFormat', type: 'select', options: [
331
+ * { value: 'terminal', label: 'Terminal' },
332
+ * { value: 'stream-json', label: 'Chat' },
333
+ * ], default: 'terminal' }
334
+ */
335
+ export interface ProviderLaunchOption {
336
+ /** Unique identifier — key used in launchArgBuilder options map */
337
+ id: string;
338
+ /** Display label */
339
+ name: string;
340
+ description?: string;
341
+ type: ProviderLaunchOptionType;
342
+ /** Options for 'select' type */
343
+ options?: {
344
+ value: string;
345
+ label: string;
346
+ description?: string;
347
+ }[];
348
+ /** Default value — applied when not explicitly set */
349
+ default?: string | boolean | number;
350
+ /**
351
+ * Maps specific values of this option to a frontend rendering hint.
352
+ * e.g. { 'stream-json': 'stream-json' } tells the UI to switch to Chat mode.
353
+ */
354
+ outputFormatMap?: Record<string, 'terminal' | 'stream-json'>;
355
+ }
356
+ /**
357
+ * A named launch preset — shorthand for a specific set of launchOption values.
358
+ * When selected, its `options` are merged with user-configured values before
359
+ * calling launchArgBuilder. Falls back to `extraArgs` if no launchArgBuilder defined.
360
+ */
361
+ export interface ProviderLaunchMode {
362
+ /** Unique mode identifier (e.g. 'terminal', 'chat') */
363
+ id: string;
364
+ /** Display name */
365
+ name: string;
366
+ description?: string;
367
+ /**
368
+ * Preset option values — merged over defaults before calling launchArgBuilder.
369
+ * Keys correspond to ProviderLaunchOption.id.
370
+ */
371
+ options?: Record<string, string | boolean | number>;
372
+ /**
373
+ * Fallback: raw args appended when no launchArgBuilder is defined.
374
+ * Use launchArgBuilder + options for anything more than trivial cases.
375
+ */
376
+ extraArgs?: string[];
377
+ /** Env var overrides applied on top of spawn.env */
378
+ env?: Record<string, string>;
379
+ /**
380
+ * Output rendering hint (shorthand when not using launchOptions/outputFormatMap).
381
+ * - 'terminal' — raw PTY stream (default)
382
+ * - 'stream-json' — structured JSON events → chat messages
383
+ */
384
+ outputFormat?: 'terminal' | 'stream-json';
385
+ /** Whether this is the default mode when none is specified */
386
+ default?: boolean;
387
+ }
309
388
  export interface ProviderResumeCapability {
310
389
  supported: boolean;
311
390
  stopStrategy?: 'command' | 'ctrl_c';
@@ -86,6 +86,8 @@ export interface CliProviderState extends ProviderStateBase {
86
86
  category: 'cli';
87
87
  /** terminal = PTY stream, chat = parsed conversation */
88
88
  mode: 'terminal' | 'chat';
89
+ /** Active launch mode id (e.g. 'chat') — undefined means default terminal */
90
+ launchMode?: string;
89
91
  }
90
92
  /** ACP provider state */
91
93
  export interface AcpProviderState extends ProviderStateBase {
@@ -17,7 +17,7 @@ export interface RuntimeAttachedClient {
17
17
  type: 'daemon' | 'web' | 'local-terminal';
18
18
  readOnly: boolean;
19
19
  }
20
- /** Session status union (used by SessionEntry.status, RecentSessionEntry.status, etc.) */
20
+ /** Session status union (used by SessionEntry.status, legacy recent-launch metadata, etc.) */
21
21
  export type SessionStatus = 'idle' | 'generating' | 'waiting_approval' | 'error' | 'stopped' | 'starting' | 'panel_hidden' | 'not_monitored' | 'disconnected';
22
22
  /** Inbox bucket categories for recent sessions */
23
23
  export type RecentSessionBucket = 'needs_attention' | 'working' | 'task_complete' | 'idle';
@@ -51,6 +51,10 @@ export interface SessionEntry {
51
51
  runtimeKey?: string;
52
52
  runtimeDisplayName?: string;
53
53
  runtimeWorkspaceLabel?: string;
54
+ /** CLI only: active launch mode id (e.g. 'terminal', 'chat') */
55
+ launchMode?: string;
56
+ /** CLI only: output rendering mode derived from launchMode.outputFormat */
57
+ mode?: 'terminal' | 'chat';
54
58
  runtimeWriteOwner?: RuntimeWriteOwner | null;
55
59
  runtimeAttachedClients?: RuntimeAttachedClient[];
56
60
  resume?: ProviderResumeCapability;
@@ -69,7 +73,6 @@ export interface SessionEntry {
69
73
  errorMessage?: string;
70
74
  errorReason?: _ProviderErrorReason;
71
75
  lastUpdated?: number;
72
- recentKey?: string;
73
76
  unread?: boolean;
74
77
  lastSeenAt?: number;
75
78
  inboxBucket?: RecentSessionBucket;
@@ -161,22 +164,15 @@ export interface DetectedIdeInfo {
161
164
  export type { RecentSessionBucket, TerminalBackendStatus } from './shared-types-extra.js';
162
165
  import type { RecentSessionBucket } from './shared-types-extra.js';
163
166
  import type { TerminalBackendStatus } from './shared-types-extra.js';
164
- export interface RecentSessionEntry {
167
+ export interface RecentLaunchEntry {
165
168
  id: string;
166
- recentKey: string;
167
- sessionId?: string | null;
168
169
  providerType: string;
169
170
  providerName: string;
170
171
  kind: 'ide' | 'cli' | 'acp';
171
- title: string;
172
+ title?: string;
172
173
  workspace?: string | null;
173
174
  currentModel?: string;
174
- status?: SessionEntry['status'];
175
- lastUsedAt: number;
176
- unread?: boolean;
177
- lastSeenAt?: number;
178
- inboxBucket?: RecentSessionBucket;
179
- surfaceHidden?: boolean;
175
+ lastLaunchedAt: number;
180
176
  }
181
177
  export interface StatusReportPayload {
182
178
  /** Unique daemon instance identifier */
@@ -206,7 +202,7 @@ export interface StatusReportPayload {
206
202
  workspaces?: WorkspaceEntry[];
207
203
  defaultWorkspaceId?: string | null;
208
204
  defaultWorkspacePath?: string | null;
209
- recentSessions?: RecentSessionEntry[];
205
+ recentLaunches?: RecentLaunchEntry[];
210
206
  terminalBackend?: TerminalBackendStatus;
211
207
  /** Available providers (present in StatusSnapshot, optional in raw payload) */
212
208
  availableProviders?: AvailableProviderInfo[];
@@ -35,4 +35,17 @@ export interface StatusSnapshotOptions {
35
35
  export interface StatusSnapshot extends StatusReportPayload {
36
36
  availableProviders: AvailableProviderInfo[];
37
37
  }
38
+ export declare function getSessionCompletionMarker(session: {
39
+ activeChat?: {
40
+ messages?: Array<{
41
+ role?: string;
42
+ id?: string;
43
+ index?: number;
44
+ timestamp?: number | string;
45
+ receivedAt?: number | string;
46
+ createdAt?: number | string;
47
+ _turnKey?: string;
48
+ }> | null;
49
+ } | null;
50
+ }): string;
38
51
  export declare function buildStatusSnapshot(options: StatusSnapshotOptions): StatusSnapshot;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.7.39",
3
+ "version": "0.7.41",
4
4
  "description": "ADHDev local session host core — 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.7.39",
3
+ "version": "0.7.41",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -60,6 +60,7 @@ export interface HostedCliRuntimeDescriptor {
60
60
  cliType: string;
61
61
  workspace: string;
62
62
  cliArgs?: string[];
63
+ launchMode?: string;
63
64
  }
64
65
 
65
66
  const chalkApi: any = (chalk as any)?.yellow
@@ -178,12 +179,13 @@ export class DaemonCliManager {
178
179
  provider: any,
179
180
  settings: Record<string, any>,
180
181
  attachExisting = false,
182
+ launchModeId?: string,
181
183
  ): Promise<void> {
182
184
  const instanceManager = this.deps.getInstanceManager();
183
185
  const sessionRegistry = this.deps.getSessionRegistry?.() || null;
184
186
  if (!instanceManager) throw new Error('InstanceManager not available');
185
187
  const transportFactory = this.getTransportFactory(key, normalizedType, resolvedDir, cliArgs, attachExisting);
186
- const cliInstance = new CliProviderInstance(provider, resolvedDir, cliArgs, key, transportFactory);
188
+ const cliInstance = new CliProviderInstance(provider, resolvedDir, cliArgs, key, transportFactory, launchModeId);
187
189
  try {
188
190
  await instanceManager.addInstance(key, cliInstance, {
189
191
  serverConn: this.deps.getServerConn(),
@@ -212,7 +214,7 @@ export class DaemonCliManager {
212
214
 
213
215
  // ─── Session start/management ──────────────────────────────
214
216
 
215
- async startSession(cliType: string, workingDir: string, cliArgs?: string[], initialModel?: string): Promise<void> {
217
+ async startSession(cliType: string, workingDir: string, cliArgs?: string[], initialModel?: string, launchMode?: string, launchOptionValues?: Record<string, string | boolean | number>): Promise<void> {
216
218
  const trimmed = (workingDir || '').trim();
217
219
  if (!trimmed) throw new Error('working directory required');
218
220
  const resolvedDir = trimmed.startsWith('~')
@@ -318,6 +320,40 @@ export class DaemonCliManager {
318
320
  console.log(colorize('cyan', ` 📦 Using provider: ${provider.name} (${provider.type})`));
319
321
  }
320
322
 
323
+ // ─── Resolve launch options → extra args ───
324
+ let resolvedCliArgs = cliArgs;
325
+ let resolvedLaunchMode = launchMode;
326
+
327
+ const activeMode = provider?.launchModes?.length
328
+ ? (launchMode
329
+ ? provider.launchModes.find((m: any) => m.id === launchMode)
330
+ : provider.launchModes.find((m: any) => m.default))
331
+ : undefined;
332
+
333
+ if (activeMode) {
334
+ resolvedLaunchMode = activeMode.id;
335
+ }
336
+
337
+ if (provider?.launchArgBuilder) {
338
+ // Build option values: schema defaults → mode preset → user args?.launchOptionValues
339
+ const defaults: Record<string, string | boolean | number> = {};
340
+ for (const opt of (provider.launchOptions || [])) {
341
+ if (opt.default !== undefined) defaults[opt.id] = opt.default;
342
+ }
343
+ const modeOptions: Record<string, string | boolean | number> = activeMode?.options || {};
344
+ const userOptions: Record<string, string | boolean | number> = launchOptionValues || {};
345
+ const merged = { ...defaults, ...modeOptions, ...userOptions };
346
+ const extraArgs = provider.launchArgBuilder(merged);
347
+ if (extraArgs.length) {
348
+ resolvedCliArgs = [...(cliArgs || []), ...extraArgs];
349
+ console.log(colorize('cyan', ` 🚀 Launch options applied: ${extraArgs.join(' ')}`));
350
+ }
351
+ } else if (activeMode?.extraArgs?.length) {
352
+ // Fallback: simple extraArgs from mode (no launchArgBuilder)
353
+ resolvedCliArgs = [...(cliArgs || []), ...activeMode.extraArgs];
354
+ console.log(colorize('cyan', ` 🚀 Launch mode '${activeMode.name}': appending args ${activeMode.extraArgs.join(' ')}`));
355
+ }
356
+
321
357
  // If InstanceManager exists, manage as CliProviderInstance unified
322
358
  const instanceManager = this.deps.getInstanceManager();
323
359
  if (provider && instanceManager) {
@@ -327,15 +363,16 @@ export class DaemonCliManager {
327
363
  normalizedType,
328
364
  cliType,
329
365
  resolvedDir,
330
- cliArgs,
366
+ resolvedCliArgs,
331
367
  resolvedProvider,
332
368
  {},
333
369
  false,
370
+ resolvedLaunchMode,
334
371
  );
335
372
  console.log(colorize('green', ` ✓ CLI started: ${cliInfo.displayName} v${cliInfo.version || 'unknown'} in ${resolvedDir}`));
336
373
  } else {
337
374
  // Fallback: InstanceManager without directly adapter manage
338
- const adapter = this.createAdapter(cliType, resolvedDir, cliArgs, key, false);
375
+ const adapter = this.createAdapter(cliType, resolvedDir, resolvedCliArgs, key, false);
339
376
  try {
340
377
  await adapter.spawn();
341
378
  } catch (spawnErr: any) {
@@ -458,6 +495,7 @@ export class DaemonCliManager {
458
495
  resolvedProvider,
459
496
  {},
460
497
  true,
498
+ record.launchMode,
461
499
  );
462
500
  restored += 1;
463
501
  LOG.info('CLI', `♻ Restored hosted runtime: ${record.runtimeKey || record.runtimeId} (${record.displayName || record.workspace})`);
@@ -537,7 +575,7 @@ export class DaemonCliManager {
537
575
  const launchSource = resolved.source;
538
576
  if (!cliType) throw new Error('cliType required');
539
577
 
540
- await this.startSession(cliType, dir, args?.cliArgs, args?.initialModel);
578
+ await this.startSession(cliType, dir, args?.cliArgs, args?.initialModel, args?.launchMode, args?.launchOptionValues);
541
579
 
542
580
  // On startSession success, new UUID key exists in adapters (last added item)
543
581
  let newKey: string | null = null;
@@ -18,12 +18,14 @@ import type { ProviderInstanceManager } from '../providers/provider-instance-man
18
18
  import { launchWithCdp, killIdeProcess, isIdeRunning } from '../launch.js';
19
19
  import { loadConfig, saveConfig, updateConfig } from '../config/config.js';
20
20
  import { resolveIdeLaunchWorkspace } from '../config/workspaces.js';
21
- import { appendRecentActivity, buildRecentActivityKey, markRecentSessionSeen } from '../config/recent-activity.js';
21
+ import { appendRecentActivity, markSessionSeen } from '../config/recent-activity.js';
22
22
  import { detectIDEs } from '../detection/ide-detector.js';
23
23
  import { SessionRegistry } from '../sessions/registry.js';
24
24
  import { LOG } from '../logging/logger.js';
25
25
  import { logCommand } from '../logging/command-log.js';
26
26
  import { getRecentLogs, LOG_PATH } from '../logging/logger.js';
27
+ import { buildSessionEntries } from '../status/builders.js';
28
+ import { getSessionCompletionMarker } from '../status/snapshot.js';
27
29
  import * as fs from 'fs';
28
30
 
29
31
  // ─── Types ───
@@ -61,6 +63,7 @@ const CHAT_COMMANDS = [
61
63
  'send_chat', 'new_chat', 'switch_chat', 'set_mode',
62
64
  'change_model',
63
65
  ];
66
+ const READ_DEBUG_ENABLED = process.argv.includes('--dev') || process.env.ADHDEV_READ_DEBUG === '1';
64
67
 
65
68
  export class DaemonCommandRouter {
66
69
  private deps: CommandRouterDeps;
@@ -265,28 +268,35 @@ export class DaemonCommandRouter {
265
268
  return { success: true, userName: name };
266
269
  }
267
270
 
268
- case 'mark_recent_seen': {
269
- const kind = args?.kind;
270
- const providerType = args?.providerType;
271
- if (!kind || !providerType) {
272
- return { success: false, error: 'kind and providerType are required' };
271
+ case 'mark_session_seen': {
272
+ const sessionId = args?.sessionId;
273
+ if (!sessionId || typeof sessionId !== 'string') {
274
+ return { success: false, error: 'sessionId is required' };
273
275
  }
274
- const recentKey = args?.recentKey || buildRecentActivityKey({
275
- kind,
276
- providerType,
277
- workspace: args?.workspace || null,
278
- });
279
- const next = markRecentSessionSeen(
280
- loadConfig(),
281
- recentKey,
276
+ const currentConfig = loadConfig();
277
+ const prevSeenAt = currentConfig.sessionReads?.[sessionId] || 0;
278
+ const sessionEntries = buildSessionEntries(
279
+ this.deps.instanceManager.collectAllStates(),
280
+ this.deps.cdpManagers as Map<string, any>,
281
+ );
282
+ const targetSession = sessionEntries.find((entry) => entry.id === sessionId);
283
+ const completionMarker = targetSession ? getSessionCompletionMarker(targetSession) : '';
284
+ const next = markSessionSeen(
285
+ currentConfig,
286
+ sessionId,
282
287
  typeof args?.seenAt === 'number' ? args.seenAt : Date.now(),
288
+ completionMarker,
283
289
  );
290
+ if (READ_DEBUG_ENABLED) {
291
+ LOG.info('RecentRead', `mark_session_seen sessionId=${sessionId} seenAt=${String(args?.seenAt || '')} prevSeenAt=${String(prevSeenAt)} nextSeenAt=${String(next.sessionReads?.[sessionId] || 0)} marker=${completionMarker || '-'}`);
292
+ }
284
293
  saveConfig(next);
285
294
  this.deps.onStatusChange?.();
286
295
  return {
287
296
  success: true,
288
- recentKey,
289
- seenAt: next.recentSessionReads?.[recentKey] || Date.now(),
297
+ sessionId,
298
+ seenAt: next.sessionReads?.[sessionId] || Date.now(),
299
+ completionMarker,
290
300
  };
291
301
  }
292
302
 
@@ -59,8 +59,10 @@ export interface ADHDevConfig {
59
59
 
60
60
  /** Unified recent activity across IDE / CLI / ACP launch flows */
61
61
  recentActivity?: RecentActivityEntry[];
62
- /** Last seen timestamps for machine-facing recent/session entries */
63
- recentSessionReads?: Record<string, number>;
62
+ /** Last seen timestamps for live sessions, keyed by sessionId */
63
+ sessionReads?: Record<string, number>;
64
+ /** Last seen completion marker for live sessions, keyed by sessionId */
65
+ sessionReadMarkers?: Record<string, string>;
64
66
 
65
67
  // Machine nickname (user-customizable label for this machine)
66
68
  machineNickname: string | null;
@@ -122,7 +124,8 @@ const DEFAULT_CONFIG: ADHDevConfig = {
122
124
  workspaces: [],
123
125
  defaultWorkspaceId: null,
124
126
  recentActivity: [],
125
- recentSessionReads: {},
127
+ sessionReads: {},
128
+ sessionReadMarkers: {},
126
129
  machineNickname: null,
127
130
  machineId: undefined,
128
131
  machineSecret: null,
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Unified recent activity — machine-facing "pick up where you left off".
2
+ * Unified recent activity — launcher-facing "pick up where you launched".
3
3
  *
4
- * Unlike cliHistory or workspaceActivity, this is task/session oriented:
4
+ * Unlike live session state, this is launch oriented:
5
5
  * - one normalized row shape for IDE / CLI / ACP
6
6
  * - deduped by kind + providerType + workspace
7
- * - optionally linked to a live sessionId when known
7
+ * - used only for quick-launch shortcuts
8
8
  */
9
9
 
10
10
  import * as path from 'path';
@@ -18,7 +18,6 @@ export interface RecentActivityEntry {
18
18
  providerName: string;
19
19
  workspace?: string | null;
20
20
  currentModel?: string;
21
- sessionId?: string | null;
22
21
  title?: string;
23
22
  lastUsedAt: number;
24
23
  }
@@ -62,22 +61,35 @@ export function getRecentActivity(config: ADHDevConfig, limit = 20): RecentActiv
62
61
  .slice(0, limit);
63
62
  }
64
63
 
65
- export function getRecentSessionSeenAt(config: ADHDevConfig, recentKey: string): number {
66
- return config.recentSessionReads?.[recentKey] || 0;
64
+ export function getSessionSeenAt(config: ADHDevConfig, sessionId: string): number {
65
+ return config.sessionReads?.[sessionId] || 0;
67
66
  }
68
67
 
69
- export function markRecentSessionSeen(
68
+ export function getSessionSeenMarker(config: ADHDevConfig, sessionId: string): string {
69
+ return config.sessionReadMarkers?.[sessionId] || '';
70
+ }
71
+
72
+ export function markSessionSeen(
70
73
  config: ADHDevConfig,
71
- recentKey: string,
74
+ sessionId: string,
72
75
  seenAt = Date.now(),
76
+ completionMarker?: string | null,
73
77
  ): ADHDevConfig {
74
- const prev = config.recentSessionReads || {};
75
- const nextSeenAt = Math.max(prev[recentKey] || 0, seenAt);
78
+ const prev = config.sessionReads || {};
79
+ const nextSeenAt = Math.max(prev[sessionId] || 0, seenAt);
80
+ const prevMarkers = config.sessionReadMarkers || {};
81
+ const nextMarker = typeof completionMarker === 'string' ? completionMarker : '';
76
82
  return {
77
83
  ...config,
78
- recentSessionReads: {
84
+ sessionReads: {
79
85
  ...prev,
80
- [recentKey]: nextSeenAt,
86
+ [sessionId]: nextSeenAt,
81
87
  },
88
+ sessionReadMarkers: nextMarker
89
+ ? {
90
+ ...prevMarkers,
91
+ [sessionId]: nextMarker,
92
+ }
93
+ : prevMarkers,
82
94
  };
83
95
  }
package/src/index.ts CHANGED
@@ -46,11 +46,11 @@ export type {
46
46
  // rollup-dts cannot resolve re-exports from shared-types.ts for them.
47
47
  import type { RuntimeWriteOwner as _RuntimeWriteOwner } from './shared-types-extra.js';
48
48
  import type { RuntimeAttachedClient as _RuntimeAttachedClient } from './shared-types-extra.js';
49
- import type { RecentSessionEntry as _RecentSessionEntry } from './shared-types.js';
49
+ import type { RecentLaunchEntry as _RecentLaunchEntry } from './shared-types.js';
50
50
  import type { TerminalBackendStatus as _TerminalBackendStatus } from './shared-types-extra.js';
51
51
  export type RuntimeWriteOwner = _RuntimeWriteOwner;
52
52
  export type RuntimeAttachedClient = _RuntimeAttachedClient;
53
- export type RecentSessionEntry = _RecentSessionEntry;
53
+ export type RecentLaunchEntry = _RecentLaunchEntry;
54
54
  export type TerminalBackendStatus = _TerminalBackendStatus;
55
55
 
56
56
  // Type aliases — rollup-dts cannot bundle re-exported type aliases at all.
@@ -7,7 +7,7 @@
7
7
 
8
8
  import * as path from 'path';
9
9
  import * as crypto from 'crypto';
10
- import type { ProviderModule } from './contracts.js';
10
+ import type { ProviderModule, ProviderLaunchMode, ProviderLaunchOption } from './contracts.js';
11
11
  import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext } from './provider-instance.js';
12
12
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
13
13
  import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
@@ -33,20 +33,46 @@ export class CliProviderInstance implements ProviderInstance {
33
33
  private historyWriter: ChatHistoryWriter;
34
34
  readonly instanceId: string;
35
35
 
36
+ private launchMode: ProviderLaunchMode | null;
37
+ private resolvedOutputFormat: 'terminal' | 'stream-json';
38
+
36
39
  constructor(
37
40
  private provider: ProviderModule,
38
41
  private workingDir: string,
39
42
  private cliArgs: string[] = [],
40
43
  instanceId?: string,
41
44
  transportFactory?: PtyTransportFactory,
45
+ launchModeId?: string,
42
46
  ) {
43
47
  this.type = provider.type;
44
48
  this.instanceId = instanceId || crypto.randomUUID();
49
+ this.launchMode = (launchModeId && provider.launchModes?.find(m => m.id === launchModeId)) || null;
50
+ this.resolvedOutputFormat = this.resolveOutputFormat();
45
51
  this.adapter = new ProviderCliAdapter(provider as any as CliProviderModule, workingDir, cliArgs, transportFactory);
46
52
  this.monitor = new StatusMonitor();
47
53
  this.historyWriter = new ChatHistoryWriter();
48
54
  }
49
55
 
56
+ /**
57
+ * Determine output rendering format from:
58
+ * 1. launchMode.outputFormat (explicit override)
59
+ * 2. launchOptions[].outputFormatMap — check actual args for matching values
60
+ * 3. Default: 'terminal'
61
+ */
62
+ private resolveOutputFormat(): 'terminal' | 'stream-json' {
63
+ if (this.launchMode?.outputFormat) return this.launchMode.outputFormat;
64
+ if (this.provider.launchOptions?.length) {
65
+ for (const opt of this.provider.launchOptions) {
66
+ if (!opt.outputFormatMap) continue;
67
+ // Check if any cliArg matches a value with an outputFormatMap entry
68
+ for (const [val, fmt] of Object.entries(opt.outputFormatMap)) {
69
+ if (this.cliArgs.includes(val)) return fmt;
70
+ }
71
+ }
72
+ }
73
+ return 'terminal';
74
+ }
75
+
50
76
  // ─── Lifecycle ─────────────────────────────────
51
77
 
52
78
  async init(context: InstanceContext): Promise<void> {
@@ -102,7 +128,8 @@ export class CliProviderInstance implements ProviderInstance {
102
128
  name: this.provider.name,
103
129
  category: 'cli',
104
130
  status: adapterStatus.status,
105
- mode: 'terminal',
131
+ mode: this.resolvedOutputFormat === 'stream-json' ? 'chat' : 'terminal',
132
+ launchMode: this.launchMode?.id,
106
133
  activeChat: {
107
134
  id: `${this.type}_${this.workingDir}`,
108
135
  title: `${this.provider.name} · ${dirName}`,