@adhdev/daemon-core 0.8.37 → 0.8.39

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.
@@ -244,6 +244,10 @@ export interface SessionEntry {
244
244
  providerControls?: ProviderControlSchema[];
245
245
  errorMessage?: string;
246
246
  errorReason?: _ProviderErrorReason;
247
+ lastMessagePreview?: string;
248
+ lastMessageRole?: string;
249
+ lastMessageAt?: number;
250
+ lastMessageHash?: string;
247
251
  lastUpdated?: number;
248
252
  unread?: boolean;
249
253
  lastSeenAt?: number;
@@ -15,15 +15,12 @@ export interface SessionEntryBuildOptions {
15
15
  profile?: SessionEntryProfile;
16
16
  }
17
17
  /**
18
- * Find a CDP manager by key, with prefix matching for multi-window support.
18
+ * Find a CDP manager by key. Supports single-window (`cursor`) and full multi-window keys (`cursor_<targetId>`).
19
19
  *
20
20
  * Lookup order:
21
- * 1. Exact match: cdpManagers.get(key)
22
- * 2. Prefix match: key starts with `${ideType}_` (multi-window: "cursor_remote_vs")
23
- * 3. null
24
- *
25
- * This replaces raw `cdpManagers.get(ideType)` calls that broke when
26
- * multi-window keys like "cursor_remote_vs" were used.
21
+ * 1. Exact match when connected
22
+ * 2. If key has no multi-window suffix: at most **one** connected manager whose key starts with `key_`
23
+ * 3. If two or more windows share that prefix → **null** (ambiguous — pass full managerKey from `GET /api/cdp/targets`)
27
24
  */
28
25
  export declare function findCdpManager(cdpManagers: Map<string, DaemonCdpManager>, key: string): DaemonCdpManager | null;
29
26
  /**
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.8.37",
3
+ "version": "0.8.39",
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.8.37",
3
+ "version": "0.8.39",
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",
@@ -71,6 +71,16 @@ export function getDefaultAutoImplReference(ctx: DevServerContext, category: str
71
71
  if (category === 'cli') {
72
72
  return type === 'codex-cli' ? 'claude-cli' : 'codex-cli';
73
73
  }
74
+ if (category === 'extension') {
75
+ const preferred = ['claude-code-vscode', 'codex', 'cline', 'roo-code'];
76
+ for (const ref of preferred) {
77
+ if (ref === type) continue;
78
+ if (ctx.providerLoader.resolve(ref) || ctx.providerLoader.getMeta(ref)) return ref;
79
+ }
80
+ const all = ctx.providerLoader.getAll();
81
+ const fb = all.find((p: any) => p.category === 'extension' && p.type !== type);
82
+ if (fb?.type) return fb.type;
83
+ }
74
84
  return 'antigravity';
75
85
  }
76
86
 
@@ -725,6 +735,9 @@ export function buildAutoImplPrompt(ctx: DevServerContext,
725
735
 
726
736
  const lines: string[] = [];
727
737
 
738
+ /** CDP connection key: extension scripts use host IDE (default Cursor), not the extension id. */
739
+ const cdpIdeType = provider.category === 'extension' ? 'cursor' : type;
740
+
728
741
  // ── System instructions ──
729
742
  lines.push('You are implementing browser automation scripts for an IDE provider.');
730
743
  lines.push('Be concise. Do NOT explain your reasoning. Just edit files directly.');
@@ -734,6 +747,19 @@ export function buildAutoImplPrompt(ctx: DevServerContext,
734
747
  lines.push(`# Target: ${provider.name || type} (${type})`);
735
748
  lines.push(`Provider directory: \`${providerDir}\``);
736
749
  lines.push('');
750
+ if (provider.category === 'extension') {
751
+ lines.push('## CDP host (extension providers)');
752
+ lines.push(
753
+ `Extension **${type}** runs inside a host IDE. For \`/api/scripts/run\` and \`/api/cdp/evaluate\`, keep \`"type": "${type}"\` (which provider scripts run) but set \`"ideType"\` to the DevServer CDP **managerKey** for that window.`,
754
+ );
755
+ lines.push(
756
+ `Examples use \`"ideType": "${cdpIdeType}"\` (Cursor). If **multiple** IDE windows are connected, run \`GET /api/cdp/targets\` and use the correct \`managerKey\` / \`pageTitle\` — short \`cursor\` or \`vscode\` only works when it uniquely identifies one window.`,
757
+ );
758
+ lines.push(
759
+ 'For VS Code hosts, use `vscode` or full `vscode_<targetId>` managerKey in every curl below.',
760
+ );
761
+ lines.push('');
762
+ }
737
763
 
738
764
  // ── funcToFile mapping (needed early for file classification) ──
739
765
  const funcToFile: Record<string, string> = {
@@ -932,14 +958,14 @@ export function buildAutoImplPrompt(ctx: DevServerContext,
932
958
  lines.push('```bash');
933
959
  lines.push(`curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/cdp/evaluate \\`);
934
960
  lines.push(` -H "Content-Type: application/json" \\`);
935
- lines.push(` -d '{"expression": "document.body.innerHTML.substring(0, 1000)", "ideType": "${type}"}'`);
961
+ lines.push(` -d '{"expression": "document.body.innerHTML.substring(0, 1000)", "ideType": "${cdpIdeType}"}'`);
936
962
  lines.push('```');
937
963
  lines.push('');
938
964
  lines.push('### 2. Test your generated function');
939
965
  lines.push('Once you save the file, test it by running:');
940
966
  lines.push('```bash');
941
967
  lines.push(`curl -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/providers/reload`);
942
- lines.push(`curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/scripts/run -H "Content-Type: application/json" -d '{"script": "readChat", "type": "${type}", "ideType": "${type}"}'`);
968
+ lines.push(`curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/scripts/run -H "Content-Type: application/json" -d '{"script": "readChat", "type": "${type}", "ideType": "${cdpIdeType}"}'`);
943
969
  lines.push('```');
944
970
  lines.push('');
945
971
  lines.push('### Task Workflow');
@@ -949,10 +975,12 @@ export function buildAutoImplPrompt(ctx: DevServerContext,
949
975
  lines.push('4. Reload providers and TEST your script via the API.');
950
976
  lines.push('');
951
977
  lines.push('### 🔥 Advanced UI Parsing (CRUCIAL for `readChat`)');
952
- lines.push('Your `readChat` must flawlessly parse complex UI elements (tables, code blocks, tool calls, and AI thoughts). The quality must match the `antigravity` reference.');
978
+ lines.push(
979
+ `Your \`readChat\` must flawlessly parse complex UI elements (tables, code blocks, tool calls, and AI thoughts). Match the depth of the **${referenceType || 'reference'}** scripts above (patterns and structure, not necessarily the same DOM).`,
980
+ );
953
981
  lines.push('To achieve this, you MUST generate a live test scenario:');
954
982
  lines.push(`1. Early in your process, send a rich prompt to the IDE using the API:`);
955
- lines.push(` \`curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/scripts/run -H "Content-Type: application/json" -d '{"script": "sendMessage", "type": "${type}", "ideType": "${type}", "args": {"message": "Write a python script, draw a markdown table, use a tool, and show your reasoning/thought process"}}'\``);
983
+ lines.push(` \`curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/scripts/run -H "Content-Type: application/json" -d '{"script": "sendMessage", "type": "${type}", "ideType": "${cdpIdeType}", "args": {"message": "Write a python script, draw a markdown table, use a tool, and show your reasoning/thought process"}}'\``);
956
984
  lines.push('2. Wait a few seconds for the IDE AI to generate these elements in the UI.');
957
985
  lines.push('3. Use CDP evaluate to deeply inspect the DOM structure of the newly generated tables, code blocks, thought blocks, and tool calls.');
958
986
  lines.push('4. Ensure `readChat` extracts `content` with precise markdown formatting (especially for tables/code) and assigns correct `kind` tags (`thought`, `tool`, `terminal`).');
@@ -965,27 +993,27 @@ export function buildAutoImplPrompt(ctx: DevServerContext,
965
993
  lines.push('### Step 1: Baseline — confirm idle');
966
994
  lines.push('```bash');
967
995
  lines.push(`curl -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/providers/reload`);
968
- lines.push(`RESULT=$(curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/scripts/run -H "Content-Type: application/json" -d '{"script": "readChat", "type": "${type}", "ideType": "${type}"}')`);
996
+ lines.push(`RESULT=$(curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/scripts/run -H "Content-Type: application/json" -d '{"script": "readChat", "type": "${type}", "ideType": "${cdpIdeType}"}')`);
969
997
  lines.push(`echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); r=d.get('result',d); r=json.loads(r) if isinstance(r,str) else r; assert r.get('status')=='idle', f'Expected idle, got {r.get(chr(34)+chr(115)+chr(116)+chr(97)+chr(116)+chr(117)+chr(115)+chr(34))}'; print('Step 1 PASS: status=idle')"`);
970
998
  lines.push('```');
971
999
  lines.push('');
972
1000
  lines.push('### Step 2: Send a LONG message that triggers extended generation (10+ seconds)');
973
1001
  lines.push('```bash');
974
- lines.push(`curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/scripts/run -H "Content-Type: application/json" -d '{"script": "sendMessage", "type": "${type}", "ideType": "${type}", "args": {"message": "Write an extremely detailed 5000-word essay about the history of artificial intelligence from Alan Turing to 2025. Be very thorough and verbose."}}'`);
1002
+ lines.push(`curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/scripts/run -H "Content-Type: application/json" -d '{"script": "sendMessage", "type": "${type}", "ideType": "${cdpIdeType}", "args": {"message": "Write an extremely detailed 5000-word essay about the history of artificial intelligence from Alan Turing to 2025. Be very thorough and verbose."}}'`);
975
1003
  lines.push('sleep 3');
976
1004
  lines.push('```');
977
1005
  lines.push('');
978
1006
  lines.push('### Step 3: Check generating OR completed');
979
1007
  lines.push('The AI may still be generating OR may have finished already. Either generating or idle is acceptable:');
980
1008
  lines.push('```bash');
981
- lines.push(`RESULT=$(curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/scripts/run -H "Content-Type: application/json" -d '{"script": "readChat", "type": "${type}", "ideType": "${type}"}')`);
1009
+ lines.push(`RESULT=$(curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/scripts/run -H "Content-Type: application/json" -d '{"script": "readChat", "type": "${type}", "ideType": "${cdpIdeType}"}')`);
982
1010
  lines.push(`echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); r=d.get('result',d); r=json.loads(r) if isinstance(r,str) else r; s=r.get('status'); assert s in ('generating','idle','waiting_approval'), f'Unexpected: {s}'; print(f'Step 3 PASS: status={s}')"`);
983
1011
  lines.push('```');
984
1012
  lines.push('');
985
1013
  lines.push('### Step 4: Wait for completion and verify new message');
986
1014
  lines.push('```bash');
987
1015
  lines.push('sleep 10');
988
- lines.push(`RESULT=$(curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/scripts/run -H "Content-Type: application/json" -d '{"script": "readChat", "type": "${type}", "ideType": "${type}"}')`);
1016
+ lines.push(`RESULT=$(curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/scripts/run -H "Content-Type: application/json" -d '{"script": "readChat", "type": "${type}", "ideType": "${cdpIdeType}"}')`);
989
1017
  lines.push(`echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); r=d.get('result',d); r=json.loads(r) if isinstance(r,str) else r; s=r.get('status'); msgs=r.get('messages',[]); assert s=='idle', f'Expected idle, got {s}'; assert len(msgs)>0, 'No messages'; print(f'Step 4 PASS: status={s}, messages={len(msgs)}')"`);
990
1018
  lines.push('```');
991
1019
  lines.push('');
@@ -295,9 +295,28 @@ export async function handleScriptHints(ctx: DevServerContext, type: string, _re
295
295
  }
296
296
 
297
297
  export async function handleCdpTargets(ctx: DevServerContext, _req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
298
- const targets: { ide: string; connected: boolean; port: number }[] = [];
299
- for (const [ide, cdp] of ctx.cdpManagers.entries()) {
300
- targets.push({ ide, connected: cdp.isConnected, port: cdp.getPort() });
298
+ const targets: Array<{
299
+ managerKey: string;
300
+ /** @deprecated same as managerKey kept for older clients */
301
+ ide: string;
302
+ ideBase: string;
303
+ pageTitle: string;
304
+ targetId: string | null;
305
+ connected: boolean;
306
+ port: number;
307
+ }> = [];
308
+ for (const [managerKey, cdp] of ctx.cdpManagers.entries()) {
309
+ const underscore = managerKey.indexOf('_');
310
+ const ideBase = underscore === -1 ? managerKey : managerKey.slice(0, underscore);
311
+ targets.push({
312
+ managerKey,
313
+ ide: managerKey,
314
+ ideBase,
315
+ pageTitle: cdp.pageTitle,
316
+ targetId: cdp.targetId,
317
+ connected: cdp.isConnected,
318
+ port: cdp.getPort(),
319
+ });
301
320
  }
302
321
  ctx.json(res, 200, { targets });
303
322
  }
@@ -28,6 +28,7 @@ import type { DaemonCliManager } from '../commands/cli-manager.js';
28
28
  import { generateTemplate as genScaffoldTemplate, generateFiles as genScaffoldFiles } from './scaffold-template.js';
29
29
  import { VersionArchive, detectAllVersions } from '../providers/version-archive.js';
30
30
  import { LOG } from '../logging/logger.js';
31
+ import { findCdpManager } from '../status/builders.js';
31
32
  import { handleCdpEvaluate, handleCdpClick, handleCdpDomQuery, handleScreenshot, handleScriptsRun, handleTypeAndSend, handleTypeAndSendAt, handleScriptHints, handleCdpTargets, handleDomInspect, handleDomChildren, handleDomAnalyze, handleFindCommon, handleFindByText, handleDomContext } from './dev-cdp-handlers.js';
32
33
  import { handleCliStatus, handleCliLaunch, handleCliSend, handleCliStop, handleCliDebug, handleCliTrace, handleCliExercise, handleCliFixtureCapture, handleCliFixtureList, handleCliFixtureReplay, handleCliResolve, handleCliRaw, handleCliSSE } from './dev-cli-debug.js';
33
34
  import { handleAutoImplement, handleAutoImplCancel, handleAutoImplSSE } from './dev-auto-implement.js';
@@ -1590,24 +1591,29 @@ export class DevServer implements DevServerContext {
1590
1591
  }
1591
1592
  }
1592
1593
 
1593
- /** Get CDP manager — matching IDE when ideType specified, first connected one otherwise.
1594
- * DevServer is a debugging tool so first-connected fallback is acceptable,
1595
- * but callers should pass ideType when possible. */
1594
+ /**
1595
+ * Resolve a CDP manager for DevServer APIs.
1596
+ * - Pass full **managerKey** from `GET /api/cdp/targets` when multiple Cursor/VS Code windows are open
1597
+ * (e.g. `cursor_0006DE34…`); short `cursor` only works when it maps to exactly one connected manager.
1598
+ * - With `ideType` omitted: only succeeds when exactly one connected manager exists.
1599
+ */
1596
1600
  public getCdp(ideType?: string): DaemonCdpManager | null {
1597
1601
  if (ideType) {
1598
- const cdp = this.cdpManagers.get(ideType);
1599
- if (cdp?.isConnected) return cdp;
1600
- // Prefix match for multi-window keys
1601
- for (const [k, m] of this.cdpManagers.entries()) {
1602
- if (k.startsWith(ideType + '_') && m.isConnected) return m;
1603
- }
1604
- LOG.warn('DevServer', `getCdp: no manager found for ideType '${ideType}', available: [${[...this.cdpManagers.keys()].join(', ')}]`);
1602
+ const cdp = findCdpManager(this.cdpManagers, ideType);
1603
+ if (cdp) return cdp;
1604
+ LOG.warn(
1605
+ 'DevServer',
1606
+ `getCdp: no unique match for '${ideType}', available: [${[...this.cdpManagers.keys()].join(', ')}] — use managerKey from GET /api/cdp/targets`,
1607
+ );
1605
1608
  return null;
1606
1609
  }
1607
- // No ideType return first connected (dev convenience)
1608
- for (const cdp of this.cdpManagers.values()) {
1609
- if (cdp.isConnected) return cdp;
1610
- }
1610
+ const connected = [...this.cdpManagers.entries()].filter(([, m]) => m.isConnected);
1611
+ if (connected.length === 1) return connected[0][1];
1612
+ if (connected.length === 0) return null;
1613
+ LOG.warn(
1614
+ 'DevServer',
1615
+ `getCdp: ideType omitted but ${connected.length} CDP windows — pass managerKey from GET /api/cdp/targets`,
1616
+ );
1611
1617
  return null;
1612
1618
  }
1613
1619
 
package/src/installer.ts CHANGED
@@ -74,6 +74,19 @@ export const EXTENSION_CATALOG: ExtensionInfo[] = [
74
74
  requiresApiKey: true,
75
75
  apiKeyName: 'Anthropic/OpenAI API key',
76
76
  },
77
+ {
78
+ id: 'claude-code-vscode',
79
+ name: 'Claude Code',
80
+ displayName: 'Claude Code (Anthropic)',
81
+ marketplaceId: 'anthropic.claude-code',
82
+ description: 'Anthropic Claude Code agent in VS Code–compatible editors',
83
+ category: 'ai-agent',
84
+ icon: '🟠',
85
+ recommended: true,
86
+ requiresApiKey: true,
87
+ apiKeyName: 'Anthropic account',
88
+ website: 'https://www.anthropic.com/claude-code',
89
+ },
77
90
  {
78
91
  id: 'continue',
79
92
  name: 'Continue',
@@ -308,6 +308,10 @@ export interface SessionEntry {
308
308
  providerControls?: ProviderControlSchema[];
309
309
  errorMessage?: string;
310
310
  errorReason?: _ProviderErrorReason;
311
+ lastMessagePreview?: string;
312
+ lastMessageRole?: string;
313
+ lastMessageAt?: number;
314
+ lastMessageHash?: string;
311
315
  lastUpdated?: number;
312
316
  unread?: boolean;
313
317
  lastSeenAt?: number;
@@ -50,30 +50,26 @@ function shouldIncludeRuntimeMetadata(profile: SessionEntryProfile): boolean {
50
50
  // ─── CDP Manager lookup helpers ──────────────────────
51
51
 
52
52
  /**
53
- * Find a CDP manager by key, with prefix matching for multi-window support.
53
+ * Find a CDP manager by key. Supports single-window (`cursor`) and full multi-window keys (`cursor_<targetId>`).
54
54
  *
55
55
  * Lookup order:
56
- * 1. Exact match: cdpManagers.get(key)
57
- * 2. Prefix match: key starts with `${ideType}_` (multi-window: "cursor_remote_vs")
58
- * 3. null
59
- *
60
- * This replaces raw `cdpManagers.get(ideType)` calls that broke when
61
- * multi-window keys like "cursor_remote_vs" were used.
56
+ * 1. Exact match when connected
57
+ * 2. If key has no multi-window suffix: at most **one** connected manager whose key starts with `key_`
58
+ * 3. If two or more windows share that prefix → **null** (ambiguous — pass full managerKey from `GET /api/cdp/targets`)
62
59
  */
63
60
  export function findCdpManager(
64
61
  cdpManagers: Map<string, DaemonCdpManager>,
65
62
  key: string,
66
63
  ): DaemonCdpManager | null {
67
- // 1. Exact match (single-window: "cursor", or full managerKey: "cursor_remote_vs")
64
+ // 1. Exact match (single-window: "cursor", or full managerKey: "cursor_<targetId>")
68
65
  const exact = cdpManagers.get(key);
69
- if (exact) return exact;
66
+ if (exact) return exact.isConnected ? exact : null;
70
67
 
71
- // 2. Prefix match (key = ideType like "cursor", managerKey = "cursor_remote_vs")
68
+ // 2. Prefix match only when it resolves to exactly one connected manager
72
69
  const prefix = key + '_';
73
- for (const [k, m] of cdpManagers.entries()) {
74
- if (k.startsWith(prefix) && m.isConnected) return m;
75
- }
76
-
70
+ const matches = [...cdpManagers.entries()].filter(([k, m]) => m.isConnected && k.startsWith(prefix));
71
+ if (matches.length === 1) return matches[0][1];
72
+ // 0 matches → null; 2+ → ambiguous — caller must pass full managerKey (e.g. from /api/cdp/targets)
77
73
  return null;
78
74
  }
79
75
 
@@ -99,8 +95,13 @@ export function isCdpConnected(
99
95
  cdpManagers: Map<string, DaemonCdpManager>,
100
96
  key: string,
101
97
  ): boolean {
102
- const m = findCdpManager(cdpManagers, key);
103
- return m?.isConnected ?? false;
98
+ const exact = cdpManagers.get(key);
99
+ if (exact?.isConnected) return true;
100
+ const prefix = key + '_';
101
+ for (const [k, m] of cdpManagers.entries()) {
102
+ if (m.isConnected && k.startsWith(prefix)) return true;
103
+ }
104
+ return false;
104
105
  }
105
106
 
106
107
  /**
@@ -146,6 +146,71 @@ function parseMessageTime(value: unknown): number {
146
146
  return 0;
147
147
  }
148
148
 
149
+ function stringifyPreviewContent(content: unknown): string {
150
+ if (typeof content === 'string') return content;
151
+ if (Array.isArray(content)) {
152
+ return content.map((block) => {
153
+ if (typeof block === 'string') return block;
154
+ if (block && typeof block === 'object' && 'text' in block) {
155
+ return String((block as { text?: unknown }).text || '');
156
+ }
157
+ return '';
158
+ }).join(' ');
159
+ }
160
+ if (content && typeof content === 'object' && 'text' in content) {
161
+ return String((content as { text?: unknown }).text || '');
162
+ }
163
+ return String(content || '');
164
+ }
165
+
166
+ function normalizePreviewText(content: unknown): string {
167
+ return stringifyPreviewContent(content)
168
+ .replace(/\s+/g, ' ')
169
+ .trim();
170
+ }
171
+
172
+ function clampPreviewText(value: string, maxChars = 120): string {
173
+ if (value.length <= maxChars) return value;
174
+ if (maxChars <= 1) return value.slice(0, maxChars);
175
+ return `${value.slice(0, maxChars - 1)}…`;
176
+ }
177
+
178
+ function simplePreviewHash(value: string): string {
179
+ let h = 0x811c9dc5;
180
+ for (let i = 0; i < value.length; i += 1) {
181
+ h ^= value.charCodeAt(i);
182
+ h = (h * 0x01000193) >>> 0;
183
+ }
184
+ return h.toString(16);
185
+ }
186
+
187
+ function getLastDisplayMessage(session: {
188
+ activeChat?: {
189
+ messages?: Array<{
190
+ role?: string;
191
+ content?: unknown;
192
+ receivedAt?: number | string;
193
+ }> | null
194
+ } | null
195
+ }) {
196
+ const messages = session.activeChat?.messages;
197
+ if (!Array.isArray(messages) || messages.length === 0) return null;
198
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
199
+ const candidate = messages[i];
200
+ const role = typeof candidate?.role === 'string' ? candidate.role : '';
201
+ if (role === 'system') continue;
202
+ const preview = clampPreviewText(normalizePreviewText(candidate?.content));
203
+ if (!preview) continue;
204
+ return {
205
+ role,
206
+ preview,
207
+ receivedAt: parseMessageTime(candidate?.receivedAt),
208
+ hash: simplePreviewHash(`${role}:${preview}`),
209
+ };
210
+ }
211
+ return null;
212
+ }
213
+
149
214
  function getSessionMessageUpdatedAt(session: {
150
215
  activeChat?: {
151
216
  messages?: Array<{ receivedAt?: number | string }> | null
@@ -196,8 +261,7 @@ function getSessionKind(session: SessionEntry): RecentLaunchEntry['kind'] {
196
261
  }
197
262
 
198
263
  function getLastMessageRole(session: { activeChat?: { messages?: Array<{ role?: string }> | null } | null }): string {
199
- const role = session.activeChat?.messages?.at?.(-1)?.role;
200
- return typeof role === 'string' ? role : '';
264
+ return getLastDisplayMessage(session)?.role || '';
201
265
  }
202
266
 
203
267
  function getUnreadState(
@@ -286,6 +350,13 @@ export function buildStatusSnapshot(options: StatusSnapshotOptions): StatusSnaps
286
350
  `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(sourceSession)} msgUpdatedAt=${getSessionMessageUpdatedAt(sourceSession)}`,
287
351
  );
288
352
  }
353
+ const lastDisplayMessage = getLastDisplayMessage(sourceSession);
354
+ if (lastDisplayMessage) {
355
+ session.lastMessagePreview = lastDisplayMessage.preview;
356
+ session.lastMessageRole = lastDisplayMessage.role;
357
+ if (lastDisplayMessage.receivedAt > 0) session.lastMessageAt = lastDisplayMessage.receivedAt;
358
+ session.lastMessageHash = lastDisplayMessage.hash;
359
+ }
289
360
  }
290
361
  const includeMachineMetadata = profile !== 'live';
291
362
  const terminalBackend = includeMachineMetadata