@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.
@@ -317,6 +317,20 @@ export interface ProviderModule {
317
317
  shell?: boolean;
318
318
  env?: Record<string, string>;
319
319
  };
320
+ /**
321
+ * Configurable options shown at session launch time (schema declaration).
322
+ * The frontend renders these as a launch config UI.
323
+ * Values are passed to launchArgBuilder to produce the final args.
324
+ */
325
+ launchOptions?: ProviderLaunchOption[];
326
+ /**
327
+ * Builds extra spawn args from user-selected launch option values.
328
+ * Called with the merged defaults + mode preset + user overrides.
329
+ * When defined, takes precedence over launchMode.extraArgs.
330
+ */
331
+ launchArgBuilder?: (options: Record<string, string | boolean | number>) => string[];
332
+ /** Named presets — shortcuts that set launchOption values in bulk */
333
+ launchModes?: ProviderLaunchMode[];
320
334
  patterns?: {
321
335
  prompt?: RegExp[];
322
336
  generating?: RegExp[];
@@ -393,6 +407,72 @@ export interface ProviderModule {
393
407
  auth?: AcpAuthMethod[];
394
408
  }
395
409
 
410
+ // ─── CLI Launch Options (individual configurable flags) ────────────────
411
+
412
+ export type ProviderLaunchOptionType = 'select' | 'boolean' | 'string' | 'number';
413
+
414
+ /**
415
+ * A single configurable option exposed at session launch time.
416
+ * Providers declare these so the frontend can render a launch config UI.
417
+ * The final args are built by launchArgBuilder(selectedValues).
418
+ *
419
+ * Example — Claude Code:
420
+ * { id: 'outputFormat', type: 'select', options: [
421
+ * { value: 'terminal', label: 'Terminal' },
422
+ * { value: 'stream-json', label: 'Chat' },
423
+ * ], default: 'terminal' }
424
+ */
425
+ export interface ProviderLaunchOption {
426
+ /** Unique identifier — key used in launchArgBuilder options map */
427
+ id: string;
428
+ /** Display label */
429
+ name: string;
430
+ description?: string;
431
+ type: ProviderLaunchOptionType;
432
+ /** Options for 'select' type */
433
+ options?: { value: string; label: string; description?: string }[];
434
+ /** Default value — applied when not explicitly set */
435
+ default?: string | boolean | number;
436
+ /**
437
+ * Maps specific values of this option to a frontend rendering hint.
438
+ * e.g. { 'stream-json': 'stream-json' } tells the UI to switch to Chat mode.
439
+ */
440
+ outputFormatMap?: Record<string, 'terminal' | 'stream-json'>;
441
+ }
442
+
443
+ /**
444
+ * A named launch preset — shorthand for a specific set of launchOption values.
445
+ * When selected, its `options` are merged with user-configured values before
446
+ * calling launchArgBuilder. Falls back to `extraArgs` if no launchArgBuilder defined.
447
+ */
448
+ export interface ProviderLaunchMode {
449
+ /** Unique mode identifier (e.g. 'terminal', 'chat') */
450
+ id: string;
451
+ /** Display name */
452
+ name: string;
453
+ description?: string;
454
+ /**
455
+ * Preset option values — merged over defaults before calling launchArgBuilder.
456
+ * Keys correspond to ProviderLaunchOption.id.
457
+ */
458
+ options?: Record<string, string | boolean | number>;
459
+ /**
460
+ * Fallback: raw args appended when no launchArgBuilder is defined.
461
+ * Use launchArgBuilder + options for anything more than trivial cases.
462
+ */
463
+ extraArgs?: string[];
464
+ /** Env var overrides applied on top of spawn.env */
465
+ env?: Record<string, string>;
466
+ /**
467
+ * Output rendering hint (shorthand when not using launchOptions/outputFormatMap).
468
+ * - 'terminal' — raw PTY stream (default)
469
+ * - 'stream-json' — structured JSON events → chat messages
470
+ */
471
+ outputFormat?: 'terminal' | 'stream-json';
472
+ /** Whether this is the default mode when none is specified */
473
+ default?: boolean;
474
+ }
475
+
396
476
  export interface ProviderResumeCapability {
397
477
  supported: boolean;
398
478
  stopStrategy?: 'command' | 'ctrl_c';
@@ -103,6 +103,8 @@ export interface CliProviderState extends ProviderStateBase {
103
103
  category: 'cli';
104
104
  /** terminal = PTY stream, chat = parsed conversation */
105
105
  mode: 'terminal' | 'chat';
106
+ /** Active launch mode id (e.g. 'chat') — undefined means default terminal */
107
+ launchMode?: string;
106
108
  }
107
109
 
108
110
  /** ACP provider state */
@@ -20,7 +20,7 @@ export interface RuntimeAttachedClient {
20
20
  readOnly: boolean;
21
21
  }
22
22
 
23
- /** Session status union (used by SessionEntry.status, RecentSessionEntry.status, etc.) */
23
+ /** Session status union (used by SessionEntry.status, legacy recent-launch metadata, etc.) */
24
24
  export type SessionStatus = 'idle' | 'generating' | 'waiting_approval' | 'error' | 'stopped' | 'starting' | 'panel_hidden' | 'not_monitored' | 'disconnected';
25
25
 
26
26
  /** Inbox bucket categories for recent sessions */
@@ -147,17 +147,15 @@ export interface WorkspaceActivity {
147
147
  kind?: string;
148
148
  agentType?: string;
149
149
  }
150
- export interface RecentSessionEntry {
150
+ export interface RecentLaunchEntry {
151
151
  id: string;
152
- sessionId?: string | null;
153
152
  providerType: string;
154
153
  providerName: string;
155
154
  kind: 'ide' | 'cli' | 'acp';
156
- title: string;
155
+ title?: string;
157
156
  workspace?: string | null;
158
157
  currentModel?: string;
159
- status?: SessionEntry['status'];
160
- lastUsedAt: number;
158
+ lastLaunchedAt: number;
161
159
  }
162
160
  export interface StatusReportPayload {
163
161
  /** Daemon instance ID */
@@ -188,5 +186,5 @@ export interface StatusReportPayload {
188
186
  defaultWorkspaceId?: string | null;
189
187
  defaultWorkspacePath?: string | null;
190
188
  workspaceActivity?: WorkspaceActivity[];
191
- recentSessions?: RecentSessionEntry[];
189
+ recentLaunches?: RecentLaunchEntry[];
192
190
  }
@@ -102,6 +102,10 @@ export interface SessionEntry {
102
102
  runtimeKey?: string;
103
103
  runtimeDisplayName?: string;
104
104
  runtimeWorkspaceLabel?: string;
105
+ /** CLI only: active launch mode id (e.g. 'terminal', 'chat') */
106
+ launchMode?: string;
107
+ /** CLI only: output rendering mode derived from launchMode.outputFormat */
108
+ mode?: 'terminal' | 'chat';
105
109
  runtimeWriteOwner?: RuntimeWriteOwner | null;
106
110
  runtimeAttachedClients?: RuntimeAttachedClient[];
107
111
  resume?: ProviderResumeCapability;
@@ -120,7 +124,6 @@ export interface SessionEntry {
120
124
  errorMessage?: string;
121
125
  errorReason?: _ProviderErrorReason;
122
126
  lastUpdated?: number;
123
- recentKey?: string;
124
127
  unread?: boolean;
125
128
  lastSeenAt?: number;
126
129
  inboxBucket?: RecentSessionBucket;
@@ -215,22 +218,15 @@ export type { RecentSessionBucket, TerminalBackendStatus } from './shared-types-
215
218
  import type { RecentSessionBucket } from './shared-types-extra.js';
216
219
  import type { TerminalBackendStatus } from './shared-types-extra.js';
217
220
 
218
- export interface RecentSessionEntry {
221
+ export interface RecentLaunchEntry {
219
222
  id: string;
220
- recentKey: string;
221
- sessionId?: string | null;
222
223
  providerType: string;
223
224
  providerName: string;
224
225
  kind: 'ide' | 'cli' | 'acp';
225
- title: string;
226
+ title?: string;
226
227
  workspace?: string | null;
227
228
  currentModel?: string;
228
- status?: SessionEntry['status'];
229
- lastUsedAt: number;
230
- unread?: boolean;
231
- lastSeenAt?: number;
232
- inboxBucket?: RecentSessionBucket;
233
- surfaceHidden?: boolean;
229
+ lastLaunchedAt: number;
234
230
  }
235
231
 
236
232
  // ─── Status Report Payload (daemon → server) ────────────────────────
@@ -259,7 +255,7 @@ export interface StatusReportPayload {
259
255
  workspaces?: WorkspaceEntry[];
260
256
  defaultWorkspaceId?: string | null;
261
257
  defaultWorkspacePath?: string | null;
262
- recentSessions?: RecentSessionEntry[];
258
+ recentLaunches?: RecentLaunchEntry[];
263
259
  terminalBackend?: TerminalBackendStatus;
264
260
  /** Available providers (present in StatusSnapshot, optional in raw payload) */
265
261
  availableProviders?: AvailableProviderInfo[];
@@ -265,6 +265,8 @@ function buildCliSession(state: CliProviderState): SessionEntry {
265
265
  runtimeWorkspaceLabel: state.runtime?.workspaceLabel,
266
266
  runtimeWriteOwner: state.runtime?.writeOwner || null,
267
267
  runtimeAttachedClients: state.runtime?.attachedClients || [],
268
+ launchMode: state.launchMode,
269
+ mode: state.mode,
268
270
  resume: state.resume,
269
271
  activeChat,
270
272
  capabilities: PTY_SESSION_CAPABILITIES,
@@ -203,7 +203,7 @@ export class DaemonStatusReporter {
203
203
  currentModel: session.currentModel,
204
204
  currentPlan: session.currentPlan,
205
205
  currentAutoApprove: session.currentAutoApprove,
206
- recentKey: (session as any).recentKey,
206
+ lastUpdated: session.lastUpdated,
207
207
  unread: (session as any).unread,
208
208
  lastSeenAt: (session as any).lastSeenAt,
209
209
  inboxBucket: (session as any).inboxBucket,
@@ -8,17 +8,18 @@
8
8
 
9
9
  import * as os from 'os';
10
10
  import { loadConfig } from '../config/config.js';
11
- import { buildRecentActivityKey, getRecentActivity, getRecentSessionSeenAt } from '../config/recent-activity.js';
11
+ import { getRecentActivity, getSessionSeenAt, getSessionSeenMarker } from '../config/recent-activity.js';
12
12
  import { getWorkspaceState } from '../config/workspaces.js';
13
13
  import { getHostMemorySnapshot } from '../system/host-memory.js';
14
14
  import { getTerminalBackendRuntimeStatus } from '../cli-adapters/terminal-screen.js';
15
+ import { LOG } from '../logging/logger.js';
15
16
  import { buildSessionEntries, isCdpConnected } from './builders.js';
16
17
  import type { ProviderState } from '../providers/provider-instance.js';
17
18
  import type {
18
19
  AvailableProviderInfo,
19
20
  DetectedIdeInfo,
21
+ RecentLaunchEntry,
20
22
  RecentSessionBucket,
21
- RecentSessionEntry,
22
23
  SessionEntry,
23
24
  StatusReportPayload,
24
25
  } from '../shared-types.js';
@@ -53,6 +54,8 @@ export interface StatusSnapshot extends StatusReportPayload {
53
54
  availableProviders: AvailableProviderInfo[];
54
55
  }
55
56
 
57
+ const READ_DEBUG_ENABLED = process.argv.includes('--dev') || process.env.ADHDEV_READ_DEBUG === '1';
58
+
56
59
  function buildDetectedIdeInfos(
57
60
  detectedIdes: StatusSnapshotOptions['detectedIdes'],
58
61
  cdpManagers: StatusSnapshotOptions['cdpManagers'],
@@ -104,6 +107,30 @@ function getSessionMessageUpdatedAt(session: {
104
107
  );
105
108
  }
106
109
 
110
+ export function getSessionCompletionMarker(session: {
111
+ activeChat?: {
112
+ messages?: Array<{
113
+ role?: string;
114
+ id?: string;
115
+ index?: number;
116
+ timestamp?: number | string;
117
+ receivedAt?: number | string;
118
+ createdAt?: number | string;
119
+ _turnKey?: string;
120
+ }> | null
121
+ } | null
122
+ }) {
123
+ const lastMessage = session.activeChat?.messages?.at?.(-1) as any;
124
+ if (!lastMessage) return '';
125
+ const role = typeof lastMessage.role === 'string' ? lastMessage.role : '';
126
+ if (role === 'user' || role === 'human') return '';
127
+ if (typeof lastMessage._turnKey === 'string' && lastMessage._turnKey) return `turn:${lastMessage._turnKey}`;
128
+ if (typeof lastMessage.id === 'string' && lastMessage.id) return `id:${lastMessage.id}`;
129
+ if (typeof lastMessage.index === 'number' && Number.isFinite(lastMessage.index)) return `idx:${lastMessage.index}`;
130
+ const timestamp = parseMessageTime(lastMessage.timestamp) || parseMessageTime(lastMessage.receivedAt) || parseMessageTime(lastMessage.createdAt);
131
+ return timestamp > 0 ? `ts:${timestamp}` : '';
132
+ }
133
+
107
134
  function getSessionLastUsedAt(session: {
108
135
  activeChat?: {
109
136
  messages?: Array<{ timestamp?: number | string; receivedAt?: number | string; createdAt?: number | string }> | null
@@ -113,7 +140,7 @@ function getSessionLastUsedAt(session: {
113
140
  return getSessionMessageUpdatedAt(session) || session.lastUpdated || Date.now();
114
141
  }
115
142
 
116
- function getSessionKind(session: SessionEntry): RecentSessionEntry['kind'] {
143
+ function getSessionKind(session: SessionEntry): RecentLaunchEntry['kind'] {
117
144
  return session.transport === 'cdp-page' || session.transport === 'cdp-webview'
118
145
  ? 'ide'
119
146
  : session.transport === 'acp'
@@ -132,6 +159,8 @@ function getUnreadState(
132
159
  lastUsedAt: number,
133
160
  lastSeenAt: number,
134
161
  lastRole: string,
162
+ completionMarker: string,
163
+ seenCompletionMarker: string,
135
164
  ): { unread: boolean; inboxBucket: RecentSessionBucket } {
136
165
  if (status === 'waiting_approval') {
137
166
  return { unread: false, inboxBucket: 'needs_attention' };
@@ -139,88 +168,27 @@ function getUnreadState(
139
168
  if (status === 'generating' || status === 'starting') {
140
169
  return { unread: false, inboxBucket: 'working' };
141
170
  }
142
- const unread = hasContentChange && lastUsedAt > lastSeenAt && lastRole !== 'user' && lastRole !== 'human';
171
+ const unread = completionMarker
172
+ ? completionMarker !== seenCompletionMarker
173
+ : hasContentChange && lastUsedAt > lastSeenAt && lastRole !== 'user' && lastRole !== 'human';
143
174
  return { unread, inboxBucket: unread ? 'task_complete' : 'idle' };
144
175
  }
145
176
 
146
- function buildRecentSessions(
147
- sessions: ReturnType<typeof buildSessionEntries>,
177
+ function buildRecentLaunches(
148
178
  recentActivity: ReturnType<typeof getRecentActivity>,
149
- readState: Record<string, number>,
150
- ): RecentSessionEntry[] {
151
- const visibleKeys = new Set<string>();
152
- const hiddenKeys = new Set<string>();
153
- const live = sessions
154
- .filter((session) => !session.surfaceHidden && session.status !== 'stopped')
155
- .map((session) => {
156
- const kind = getSessionKind(session);
157
- const recentKey = buildRecentActivityKey({
158
- kind,
159
- providerType: session.providerType,
160
- workspace: session.workspace,
161
- });
162
- const lastSeenAt = readState[recentKey] || 0;
163
- const lastUsedAt = getSessionLastUsedAt(session);
164
- const { unread, inboxBucket } = getUnreadState(
165
- getSessionMessageUpdatedAt(session) > 0,
166
- session.status,
167
- lastUsedAt,
168
- lastSeenAt,
169
- getLastMessageRole(session),
170
- );
171
- return {
172
- id: session.id,
173
- recentKey,
174
- sessionId: session.id,
175
- providerType: session.providerType,
176
- providerName: session.providerName,
177
- kind,
178
- title: session.activeChat?.title || session.title || session.providerName,
179
- workspace: session.workspace,
180
- currentModel: session.currentModel,
181
- status: session.status,
182
- lastUsedAt,
183
- unread,
184
- lastSeenAt,
185
- inboxBucket,
186
- surfaceHidden: false,
187
- };
188
- });
189
- for (const item of live) {
190
- visibleKeys.add(`${item.kind}:${item.providerType}:${item.workspace || ''}`);
191
- }
192
- for (const session of sessions) {
193
- if (!session.surfaceHidden) continue;
194
- hiddenKeys.add(`${getSessionKind(session)}:${session.providerType}:${session.workspace || ''}`);
195
- }
196
- const persisted = recentActivity
197
- .filter((item) => {
198
- const key = `${item.kind}:${item.providerType}:${item.workspace || ''}`;
199
- return !visibleKeys.has(key) && !hiddenKeys.has(key);
200
- })
201
- .map((item) => {
202
- const lastSeenAt = readState[item.id] || 0;
203
- const unread = item.lastUsedAt > lastSeenAt;
204
- return {
205
- id: item.id,
206
- recentKey: item.id,
207
- sessionId: item.sessionId || null,
208
- providerType: item.providerType,
209
- providerName: item.providerName,
210
- kind: item.kind,
211
- title: item.title || item.providerName,
212
- workspace: item.workspace,
213
- currentModel: item.currentModel,
214
- lastUsedAt: item.lastUsedAt,
215
- unread,
216
- lastSeenAt,
217
- inboxBucket: unread ? 'task_complete' : 'idle' as RecentSessionBucket,
218
- surfaceHidden: false,
219
- };
220
- });
221
-
222
- return [...live, ...persisted]
223
- .sort((a, b) => b.lastUsedAt - a.lastUsedAt)
179
+ ): RecentLaunchEntry[] {
180
+ return recentActivity
181
+ .map((item) => ({
182
+ id: item.id,
183
+ providerType: item.providerType,
184
+ providerName: item.providerName,
185
+ kind: item.kind,
186
+ title: item.title || item.providerName,
187
+ workspace: item.workspace,
188
+ currentModel: item.currentModel,
189
+ lastLaunchedAt: item.lastUsedAt,
190
+ }))
191
+ .sort((a, b) => b.lastLaunchedAt - a.lastLaunchedAt)
224
192
  .slice(0, 12);
225
193
  }
226
194
 
@@ -233,16 +201,11 @@ export function buildStatusSnapshot(options: StatusSnapshotOptions): StatusSnaps
233
201
  options.allStates,
234
202
  options.cdpManagers as Map<string, any>,
235
203
  );
236
- const readState = cfg.recentSessionReads || {};
237
204
  for (const session of sessions) {
238
- const kind = getSessionKind(session);
239
- const recentKey = buildRecentActivityKey({
240
- kind,
241
- providerType: session.providerType,
242
- workspace: session.workspace,
243
- });
244
- const lastSeenAt = getRecentSessionSeenAt(cfg, recentKey);
205
+ const lastSeenAt = getSessionSeenAt(cfg, session.id);
206
+ const seenCompletionMarker = getSessionSeenMarker(cfg, session.id);
245
207
  const lastUsedAt = getSessionLastUsedAt(session);
208
+ const completionMarker = getSessionCompletionMarker(session);
246
209
  const { unread, inboxBucket } = session.surfaceHidden
247
210
  ? { unread: false, inboxBucket: 'idle' as RecentSessionBucket }
248
211
  : getUnreadState(
@@ -251,11 +214,18 @@ export function buildStatusSnapshot(options: StatusSnapshotOptions): StatusSnaps
251
214
  lastUsedAt,
252
215
  lastSeenAt,
253
216
  getLastMessageRole(session),
217
+ completionMarker,
218
+ seenCompletionMarker,
254
219
  );
255
- session.recentKey = recentKey;
256
220
  session.lastSeenAt = lastSeenAt;
257
221
  session.unread = unread;
258
222
  session.inboxBucket = inboxBucket;
223
+ if (READ_DEBUG_ENABLED && (session.unread || session.inboxBucket !== 'idle' || session.providerType.includes('codex'))) {
224
+ LOG.info(
225
+ 'RecentRead',
226
+ `snapshot session id=${session.id} provider=${session.providerType} status=${String(session.status || '')} bucket=${inboxBucket} unread=${String(unread)} lastSeenAt=${lastSeenAt} completionMarker=${completionMarker || '-'} seenMarker=${seenCompletionMarker || '-'} lastUpdated=${String(session.lastUpdated || 0)} lastUsedAt=${lastUsedAt} lastRole=${getLastMessageRole(session)} msgUpdatedAt=${getSessionMessageUpdatedAt(session)}`,
227
+ );
228
+ }
259
229
  }
260
230
  const terminalBackend = getTerminalBackendRuntimeStatus();
261
231
 
@@ -283,7 +253,7 @@ export function buildStatusSnapshot(options: StatusSnapshotOptions): StatusSnaps
283
253
  workspaces: wsState.workspaces,
284
254
  defaultWorkspaceId: wsState.defaultWorkspaceId,
285
255
  defaultWorkspacePath: wsState.defaultWorkspacePath,
286
- recentSessions: buildRecentSessions(sessions, recentActivity, readState),
256
+ recentLaunches: buildRecentLaunches(recentActivity),
287
257
  terminalBackend,
288
258
  availableProviders: buildAvailableProviders(options.providerLoader),
289
259
  };