linkshell-cli 0.10.74 → 0.10.76

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 (134) hide show
  1. package/dist/cli/src/commands/desktop-workspace.js +62 -9
  2. package/dist/cli/src/commands/desktop-workspace.js.map +1 -1
  3. package/dist/cli/src/commands/desktop.d.ts +90 -74
  4. package/dist/cli/src/commands/desktop.js +87 -1
  5. package/dist/cli/src/commands/desktop.js.map +1 -1
  6. package/dist/cli/src/commands/login-error.d.ts +9 -0
  7. package/dist/cli/src/commands/login-error.js +14 -0
  8. package/dist/cli/src/commands/login-error.js.map +1 -0
  9. package/dist/cli/src/commands/login.d.ts +2 -7
  10. package/dist/cli/src/commands/login.js +23 -52
  11. package/dist/cli/src/commands/login.js.map +1 -1
  12. package/dist/cli/src/commands/sign-in.d.ts +77 -0
  13. package/dist/cli/src/commands/sign-in.js +390 -0
  14. package/dist/cli/src/commands/sign-in.js.map +1 -0
  15. package/dist/cli/src/index.js +2 -1
  16. package/dist/cli/src/index.js.map +1 -1
  17. package/dist/cli/tsconfig.tsbuildinfo +1 -1
  18. package/dist/client-core/src/account-errors.d.ts +17 -0
  19. package/dist/client-core/src/account-errors.js +82 -0
  20. package/dist/client-core/src/account-errors.js.map +1 -0
  21. package/dist/host/src/acp-settings.js +1 -1
  22. package/dist/host/src/acp-settings.js.map +1 -1
  23. package/dist/host/src/computer-preview.d.ts +1 -9
  24. package/dist/host/src/drivers/acp/antigravity.d.ts +13 -0
  25. package/dist/host/src/drivers/acp/antigravity.js +13 -0
  26. package/dist/host/src/drivers/acp/antigravity.js.map +1 -1
  27. package/dist/host/src/drivers/acp/driver.d.ts +14 -1
  28. package/dist/host/src/drivers/acp/driver.js +45 -3
  29. package/dist/host/src/drivers/acp/driver.js.map +1 -1
  30. package/dist/host/src/drivers/acp/mapper.d.ts +2 -0
  31. package/dist/host/src/drivers/acp/mapper.js +6 -1
  32. package/dist/host/src/drivers/acp/mapper.js.map +1 -1
  33. package/dist/host/src/drivers/acp/questions.js +2 -2
  34. package/dist/host/src/drivers/acp/questions.js.map +1 -1
  35. package/dist/host/src/drivers/claude/app-sessions.d.ts +58 -0
  36. package/dist/host/src/drivers/claude/app-sessions.js +183 -0
  37. package/dist/host/src/drivers/claude/app-sessions.js.map +1 -0
  38. package/dist/host/src/drivers/claude/driver.d.ts +22 -1
  39. package/dist/host/src/drivers/claude/driver.js +99 -11
  40. package/dist/host/src/drivers/claude/driver.js.map +1 -1
  41. package/dist/host/src/drivers/claude/holders.d.ts +3 -1
  42. package/dist/host/src/drivers/claude/holders.js +4 -0
  43. package/dist/host/src/drivers/claude/holders.js.map +1 -1
  44. package/dist/host/src/drivers/claude/routing.d.ts +19 -0
  45. package/dist/host/src/drivers/claude/routing.js +145 -0
  46. package/dist/host/src/drivers/claude/routing.js.map +1 -0
  47. package/dist/host/src/drivers/claude/tasks.d.ts +2 -2
  48. package/dist/host/src/drivers/claude/transcript.d.ts +389 -344
  49. package/dist/host/src/drivers/claude/transcript.js +97 -22
  50. package/dist/host/src/drivers/claude/transcript.js.map +1 -1
  51. package/dist/host/src/drivers/codex/driver.d.ts +15 -0
  52. package/dist/host/src/drivers/codex/driver.js +47 -9
  53. package/dist/host/src/drivers/codex/driver.js.map +1 -1
  54. package/dist/host/src/drivers/codex/mapper.d.ts +41 -7
  55. package/dist/host/src/drivers/codex/mapper.js +167 -26
  56. package/dist/host/src/drivers/codex/mapper.js.map +1 -1
  57. package/dist/host/src/drivers/codex/mcp-apps.d.ts +69 -0
  58. package/dist/host/src/drivers/codex/mcp-apps.js +340 -0
  59. package/dist/host/src/drivers/codex/mcp-apps.js.map +1 -0
  60. package/dist/host/src/drivers/codex/settings.d.ts +13 -0
  61. package/dist/host/src/drivers/codex/settings.js +22 -6
  62. package/dist/host/src/drivers/codex/settings.js.map +1 -1
  63. package/dist/host/src/drivers/types.d.ts +50 -1
  64. package/dist/host/src/fs.d.ts +9 -9
  65. package/dist/host/src/hub.d.ts +120 -83
  66. package/dist/host/src/hub.js +157 -13
  67. package/dist/host/src/hub.js.map +1 -1
  68. package/dist/host/src/input.d.ts +43 -89
  69. package/dist/host/src/input.js +1 -1
  70. package/dist/host/src/input.js.map +1 -1
  71. package/dist/host/src/rpc/mcp.d.ts +8 -0
  72. package/dist/host/src/rpc/mcp.js +33 -0
  73. package/dist/host/src/rpc/mcp.js.map +1 -0
  74. package/dist/host/src/rpc/server.js +16 -2
  75. package/dist/host/src/rpc/server.js.map +1 -1
  76. package/dist/host/src/screen-session.d.ts +3 -17
  77. package/dist/host/src/screen-session.js +2 -2
  78. package/dist/host/src/screen-session.js.map +1 -1
  79. package/dist/host/src/store.d.ts +7 -0
  80. package/dist/host/src/store.js +27 -3
  81. package/dist/host/src/store.js.map +1 -1
  82. package/dist/host/src/terminals.d.ts +12 -6
  83. package/dist/host/src/terminals.js +29 -2
  84. package/dist/host/src/terminals.js.map +1 -1
  85. package/dist/host/src/usage.d.ts +5 -0
  86. package/dist/host/src/usage.js +10 -2
  87. package/dist/host/src/usage.js.map +1 -1
  88. package/dist/wire/src/acp.d.ts +68 -430
  89. package/dist/wire/src/acp.js +5 -5
  90. package/dist/wire/src/acp.js.map +1 -1
  91. package/dist/wire/src/async-questions.d.ts +2 -10
  92. package/dist/wire/src/computer-preview.d.ts +1 -15
  93. package/dist/wire/src/computer-preview.js +1 -1
  94. package/dist/wire/src/computer-preview.js.map +1 -1
  95. package/dist/wire/src/content.d.ts +205 -370
  96. package/dist/wire/src/content.js +4 -4
  97. package/dist/wire/src/content.js.map +1 -1
  98. package/dist/wire/src/editor.d.ts +48 -592
  99. package/dist/wire/src/envelopes.d.ts +53 -0
  100. package/dist/wire/src/envelopes.js +263 -0
  101. package/dist/wire/src/envelopes.js.map +1 -0
  102. package/dist/wire/src/goal.d.ts +15 -48
  103. package/dist/wire/src/index.d.ts +2 -0
  104. package/dist/wire/src/index.js +2 -0
  105. package/dist/wire/src/index.js.map +1 -1
  106. package/dist/wire/src/live-activity.d.ts +83 -212
  107. package/dist/wire/src/live-activity.js +1 -1
  108. package/dist/wire/src/live-activity.js.map +1 -1
  109. package/dist/wire/src/model.d.ts +454 -2277
  110. package/dist/wire/src/model.js +24 -0
  111. package/dist/wire/src/model.js.map +1 -1
  112. package/dist/wire/src/relay.d.ts +41 -267
  113. package/dist/wire/src/relay.js +1 -1
  114. package/dist/wire/src/relay.js.map +1 -1
  115. package/dist/wire/src/rpc-mcp.d.ts +84 -0
  116. package/dist/wire/src/rpc-mcp.js +78 -0
  117. package/dist/wire/src/rpc-mcp.js.map +1 -0
  118. package/dist/wire/src/rpc.d.ts +6747 -30929
  119. package/dist/wire/src/rpc.js +35 -7
  120. package/dist/wire/src/rpc.js.map +1 -1
  121. package/dist/wire/src/side.d.ts +13 -61
  122. package/dist/wire/src/updates.d.ts +1743 -6652
  123. package/dist/wire/src/updates.js +14 -1
  124. package/dist/wire/src/updates.js.map +1 -1
  125. package/dist/wire/src/usage.d.ts +17 -195
  126. package/dist/wire/src/watch.d.ts +223 -1141
  127. package/dist/wire/src/workflow.d.ts +46 -139
  128. package/package.json +4 -4
  129. package/src/commands/desktop-workspace.ts +69 -11
  130. package/src/commands/desktop.ts +71 -1
  131. package/src/commands/login-error.ts +12 -0
  132. package/src/commands/login.ts +13 -62
  133. package/src/commands/sign-in.ts +408 -0
  134. package/src/index.ts +2 -1
@@ -1,4 +1,4 @@
1
- import { RpcError, questionReplies, questionAnswerError, updateAsyncQuestions, sessionIdFor, } from "@linkshell/wire";
1
+ import { RpcError, questionReplies, readUserText, questionAnswerError, updateAsyncQuestions, sessionIdFor, } from "@linkshell/wire";
2
2
  import { inOrder } from "./drivers/acp/driver.js";
3
3
  import { canForkNatively } from "./drivers/behavior.js";
4
4
  import { homedir } from "node:os";
@@ -57,6 +57,24 @@ function worktreeLossMessage(state) {
57
57
  const RENAMED = "renamed";
58
58
  /** Driver-state key: the conversation a forked session's agent is still to be told, with its first message. */
59
59
  const CARRY = "carry";
60
+ /** Driver-state key: the session a session was forked from (made here, or as its agent recorded it). */
61
+ const FORKED_FROM = "forkedFrom";
62
+ /**
63
+ * Driver-state keys, for agents with an app LinkShell only reads (`appArchive`):
64
+ * LinkShell's own archive choice and when it was made, and the app's state as
65
+ * last seen and since when.
66
+ */
67
+ const LOCAL_ARCHIVE = "archive-local";
68
+ const APP_ARCHIVE = "archive-app";
69
+ /**
70
+ * Archived here or in the app, unless it was unarchived here after the app
71
+ * archived it (`local.at` 0: chosen before times were kept, so it never outranks the app).
72
+ */
73
+ export function mergedArchive(local, app) {
74
+ if (local.archived)
75
+ return true;
76
+ return !!app?.archived && !(local.at > app.since);
77
+ }
60
78
  /** A returning client catches up from where it was, unless it missed more than this. */
61
79
  const CATCH_UP = { events: 800, bytes: 4 * 1024 * 1024 };
62
80
  /** State a client needs from before its window: the latest of each applies. */
@@ -172,6 +190,17 @@ export class SessionHub {
172
190
  this.emitSummary(stored);
173
191
  },
174
192
  removed: (agent, nativeId) => this.forget(sessionIdFor(agent, nativeId)),
193
+ nativeRecordsChanged: (agent, nativeIds) => {
194
+ for (const nativeId of nativeIds) {
195
+ const stored = this.store.getSession(sessionIdFor(agent, nativeId));
196
+ if (!stored)
197
+ continue;
198
+ const merged = this.reconcileAppArchive(stored);
199
+ this.emitSummary(merged);
200
+ if (merged.archived !== stored.archived)
201
+ this.announceRelated(merged.id);
202
+ }
203
+ },
175
204
  desktop: (agent, nativeId) => this.desktops.get(sessionIdFor(agent, nativeId)),
176
205
  state: (agent, nativeId) => {
177
206
  const sessionId = sessionIdFor(agent, nativeId);
@@ -180,6 +209,10 @@ export class SessionHub {
180
209
  set: (key, value) => this.store.setDriverState(sessionId, key, value),
181
210
  };
182
211
  },
212
+ agentState: (agent) => ({
213
+ get: (key) => this.store.getAgentState(agent, key),
214
+ set: (key, value) => this.store.setAgentState(agent, key, value),
215
+ }),
183
216
  log: (message) => this.log(message),
184
217
  };
185
218
  constructor(store, drivers, log = () => { },
@@ -291,6 +324,7 @@ export class SessionHub {
291
324
  else if (availability === "available" && this.store.isNativeMissing(session.id)) {
292
325
  this.store.setNativeMissing(session.id, false);
293
326
  this.emitSummary(session);
327
+ this.announceRelated(session.id);
294
328
  }
295
329
  }
296
330
  hideMissingSession(session) {
@@ -305,6 +339,7 @@ export class SessionHub {
305
339
  driver?.watched?.(session.nativeId, false);
306
340
  this.live.delete(session.id);
307
341
  this.emitRemoved(session.id);
342
+ this.announceRelated(session.id);
308
343
  void driver?.detach(session.nativeId).catch((error) => this.log(`[hub] detach missing ${session.id}: ${String(error)}`));
309
344
  }
310
345
  async stop() {
@@ -425,6 +460,8 @@ export class SessionHub {
425
460
  await existing.configureAcp?.(services.settings.settings(id));
426
461
  await existing.stop();
427
462
  this.drivers.delete(id);
463
+ // What the old definition's agent reported (its models) needn't hold for the new one.
464
+ this.store.clearAgentState(id);
428
465
  }
429
466
  if (save && driver) {
430
467
  services.settings.saveAgent(save);
@@ -493,6 +530,11 @@ export class SessionHub {
493
530
  throw RpcError.app("not_found", "这个会话的原始记录已不存在,已从列表隐藏;本地缓存仍保留。");
494
531
  return this.decorate(summary);
495
532
  }
533
+ /** A session and its agent's driver, unchecked: for what one driver offers itself (`mcp.*`), which says what it needs of it. */
534
+ sessionDriver(sessionId) {
535
+ const session = this.getSession(sessionId);
536
+ return { session, driver: this.drivers.get(session.agent) };
537
+ }
496
538
  /** Adds the live-only fields (current activity, first pending permission) to a stored summary. */
497
539
  /** Marks a session that works in one of LinkShell's worktrees. */
498
540
  withWorktree(summary) {
@@ -512,8 +554,55 @@ export class SessionHub {
512
554
  }
513
555
  return questions;
514
556
  }
557
+ /** Per session, the session it was forked from (null: none), read from the store once. */
558
+ forkOrigins = new Map();
559
+ /** Per session, the sessions whose lineage names it: their rows change when it comes or goes. */
560
+ related = new Map();
561
+ forkOrigin(sessionId) {
562
+ let origin = this.forkOrigins.get(sessionId);
563
+ if (origin === undefined) {
564
+ origin = this.store.getDriverState(sessionId, FORKED_FROM) ?? null;
565
+ this.forkOrigins.set(sessionId, origin);
566
+ }
567
+ return origin ?? undefined;
568
+ }
569
+ /** Where the session came from, naming only sessions this host has: a list can't nest under one it doesn't show. */
570
+ withLineage(summary) {
571
+ const native = this.drivers.get(summary.agent)?.lineage?.(summary.nativeId);
572
+ const ids = (nativeIds) => nativeIds?.map((nativeId) => sessionIdFor(summary.agent, nativeId)) ?? [];
573
+ const origin = this.forkOrigin(summary.id);
574
+ const parentId = this.firstKnown(summary, ids(native?.parent));
575
+ const forkedFrom = this.firstKnown(summary, [...(origin ? [origin] : []), ...ids(native?.forkedFrom)]);
576
+ const appId = native?.appId;
577
+ if (!parentId && !forkedFrom && !appId)
578
+ return summary;
579
+ return { ...summary, ...(parentId ? { parentId } : {}), ...(forkedFrom ? { forkedFrom } : {}), ...(appId ? { appId } : {}) };
580
+ }
581
+ /** As the Claude app nests: a session still in use stands alone when the one it came from is archived. */
582
+ firstKnown(summary, candidates) {
583
+ let found;
584
+ for (const candidate of candidates) {
585
+ if (candidate === summary.id)
586
+ continue;
587
+ let children = this.related.get(candidate);
588
+ if (!children)
589
+ this.related.set(candidate, (children = new Set()));
590
+ children.add(summary.id);
591
+ if (found)
592
+ continue;
593
+ const other = this.store.getSession(candidate);
594
+ if (other && !(other.archived && !summary.archived) && !this.store.isNativeMissing(candidate))
595
+ found = candidate;
596
+ }
597
+ return found;
598
+ }
599
+ /** The session came, went or was (un)archived: the rows that name it as parent or origin say so now. */
600
+ announceRelated(sessionId) {
601
+ for (const child of this.related.get(sessionId) ?? [])
602
+ this.announce(child);
603
+ }
515
604
  decorate(summary) {
516
- summary = { ...summary, asyncQuestions: this.questionsOf(summary.id) };
605
+ summary = this.withLineage({ ...summary, asyncQuestions: this.questionsOf(summary.id) });
517
606
  const live = this.live.get(summary.id);
518
607
  if (!live)
519
608
  return this.withWorktree(summary);
@@ -561,6 +650,11 @@ export class SessionHub {
561
650
  }
562
651
  return decorated;
563
652
  }
653
+ /** What a new session of `agent` could run on (see `agents.models`): nothing from a driver that can't tell before a session exists. */
654
+ async agentModels(agent, cwd) {
655
+ const driver = this.requireDriver(agent);
656
+ return (await driver.listModels?.(cwd)) ?? { models: [] };
657
+ }
564
658
  async createSession(input) {
565
659
  const driver = this.requireDriver(input.agent);
566
660
  const worktree = input.worktree ? await this.newWorktree(input.cwd, promptLabel(input.prompt)) : undefined;
@@ -615,7 +709,7 @@ export class SessionHub {
615
709
  }
616
710
  if (!discovered)
617
711
  return await this.forkByReplay(summary, driver, cwd, cut);
618
- this.recordDiscovered(driver.id, { ...discovered, title: discovered.title ?? summary.title });
712
+ this.recordDiscovered(driver.id, { ...discovered, title: discovered.title ?? summary.title, forkedFrom: summary.nativeId });
619
713
  return this.getSession(sessionIdFor(driver.id, discovered.nativeId));
620
714
  }
621
715
  catch (error) {
@@ -633,7 +727,7 @@ export class SessionHub {
633
727
  const start = this.store.pageStart(summary.id, cut);
634
728
  const events = this.store.readEvents(summary.id, start, PAGE.maxEvents + 1, cut).filter((event) => isConversation(event.update));
635
729
  const discovered = await driver.createSession({ cwd });
636
- this.recordDiscovered(driver.id, { ...discovered, title: discovered.title ?? summary.title });
730
+ this.recordDiscovered(driver.id, { ...discovered, title: discovered.title ?? summary.title, forkedFrom: summary.nativeId });
637
731
  const forkId = sessionIdFor(driver.id, discovered.nativeId);
638
732
  await this.ensureAttached(forkId);
639
733
  const live = this.liveFor(forkId);
@@ -1185,10 +1279,40 @@ export class SessionHub {
1185
1279
  const driver = this.drivers.get(summary.agent);
1186
1280
  if (driver?.archive)
1187
1281
  await driver.archive(summary.nativeId, archived);
1188
- const updated = this.store.patchSession(sessionId, { archived });
1282
+ // Kept apart from the app's, with when: unarchiving here outranks an older archive there.
1283
+ if (driver?.appArchive)
1284
+ this.store.setDriverState(sessionId, LOCAL_ARCHIVE, JSON.stringify({ archived, at: Date.now() }));
1285
+ const updated = this.reconcileAppArchive(this.store.patchSession(sessionId, { archived }));
1189
1286
  this.emitSummary(updated);
1287
+ this.announceRelated(sessionId);
1190
1288
  return this.decorate(updated);
1191
1289
  }
1290
+ /** Folds in the archive state of the agent's app that LinkShell only reads (see `mergedArchive`). */
1291
+ reconcileAppArchive(summary) {
1292
+ const reported = this.drivers.get(summary.agent)?.appArchive?.(summary.nativeId);
1293
+ // Not in that app (or not read yet): the flag is LinkShell's alone, or stays as last merged.
1294
+ if (!reported)
1295
+ return summary;
1296
+ const saved = this.store.getDriverState(summary.id, APP_ARCHIVE);
1297
+ const seen = saved ? JSON.parse(saved) : undefined;
1298
+ if (!seen && !reported.archived)
1299
+ return summary;
1300
+ // Only a change counts as the app archiving it: the app rewrites its record for much else.
1301
+ const app = seen?.archived === reported.archived ? seen : { archived: reported.archived, since: reported.at };
1302
+ if (app !== seen)
1303
+ this.store.setDriverState(summary.id, APP_ARCHIVE, JSON.stringify(app));
1304
+ const archived = mergedArchive(this.localArchive(summary), app);
1305
+ return archived === summary.archived ? summary : this.store.patchSession(summary.id, { archived });
1306
+ }
1307
+ localArchive(summary) {
1308
+ const saved = this.store.getDriverState(summary.id, LOCAL_ARCHIVE);
1309
+ if (saved)
1310
+ return JSON.parse(saved);
1311
+ // Until the app's state is first merged in, the flag is LinkShell's own choice.
1312
+ const local = { archived: summary.archived, at: 0 };
1313
+ this.store.setDriverState(summary.id, LOCAL_ARCHIVE, JSON.stringify(local));
1314
+ return local;
1315
+ }
1192
1316
  async rename(sessionId, title) {
1193
1317
  const summary = this.getSession(sessionId);
1194
1318
  const name = compact(title, TITLE_LENGTH);
@@ -1249,7 +1373,9 @@ export class SessionHub {
1249
1373
  this.previews.forget(sessionId);
1250
1374
  this.sides.forget(sessionId);
1251
1375
  this.store.removeSession(sessionId);
1376
+ this.forkOrigins.delete(sessionId);
1252
1377
  this.emitRemoved(sessionId);
1378
+ this.announceRelated(sessionId);
1253
1379
  }
1254
1380
  emitRemoved(sessionId) {
1255
1381
  for (const listener of this.removedListeners) {
@@ -1431,10 +1557,19 @@ export class SessionHub {
1431
1557
  // Items without a recorded time get the time of the one before, never "now".
1432
1558
  // (An agent that only says when a session last changed gives that as its creation too.)
1433
1559
  let ts = Math.min(summary.createdAt, history.find((item) => item.ts !== undefined)?.ts ?? summary.createdAt);
1434
- for (const item of history) {
1560
+ // The log only grows at its end. A message before what it already has is one an earlier
1561
+ // reading skipped (a reader that now shows more), not something that happened since:
1562
+ // added now it would land at the bottom, out of place, so it is only marked as seen.
1563
+ const logged = history.map((item) => this.store.isItemLogged(sessionId, item.itemId));
1564
+ const lastLogged = logged.lastIndexOf(true);
1565
+ for (const [index, item] of history.entries()) {
1435
1566
  ts = Math.min(Math.max(item.ts ?? ts, ts), summary.updatedAt);
1436
- if (this.store.isItemLogged(sessionId, item.itemId))
1567
+ if (logged[index])
1437
1568
  continue;
1569
+ if (index < lastLogged && item.updates.every((update) => update.sessionUpdate === "user_message_chunk")) {
1570
+ this.store.markItemLogged(sessionId, item.itemId);
1571
+ continue;
1572
+ }
1438
1573
  for (const update of item.updates)
1439
1574
  this.commit(sessionId, update, undefined, ts);
1440
1575
  this.store.markItemLogged(sessionId, item.itemId);
@@ -1500,9 +1635,9 @@ export class SessionHub {
1500
1635
  updatedAt: session.updatedAt,
1501
1636
  });
1502
1637
  const { created } = stored;
1503
- const summary = session.archived !== undefined && session.archived !== stored.summary.archived
1638
+ const summary = this.reconcileAppArchive(session.archived !== undefined && session.archived !== stored.summary.archived
1504
1639
  ? this.store.patchSession(stored.summary.id, { archived: session.archived })
1505
- : stored.summary;
1640
+ : stored.summary);
1506
1641
  const changed = !!before &&
1507
1642
  (before.title !== summary.title ||
1508
1643
  before.state !== summary.state ||
@@ -1517,8 +1652,16 @@ export class SessionHub {
1517
1652
  if (questionsChanged)
1518
1653
  this.store.setDriverState(summary.id, "asyncQuestions", JSON.stringify(session.asyncQuestions));
1519
1654
  }
1520
- if (created || changed || questionsChanged || restored)
1655
+ const origin = session.forkedFrom === undefined ? undefined : sessionIdFor(agent, session.forkedFrom);
1656
+ const forked = origin !== undefined && origin !== summary.id && origin !== this.forkOrigin(summary.id);
1657
+ if (forked) {
1658
+ this.store.setDriverState(summary.id, FORKED_FROM, origin);
1659
+ this.forkOrigins.set(summary.id, origin);
1660
+ }
1661
+ if (created || changed || questionsChanged || restored || forked)
1521
1662
  this.emitSummary(summary);
1663
+ if (created || restored || before?.archived !== summary.archived)
1664
+ this.announceRelated(summary.id);
1522
1665
  }
1523
1666
  ingest(sessionId, update, itemId, ts) {
1524
1667
  if (this.store.isNativeMissing(sessionId))
@@ -1595,8 +1738,9 @@ export class SessionHub {
1595
1738
  if (update.parentToolCallId || !update.content)
1596
1739
  break;
1597
1740
  const text = update.content.filter((block) => block.type === "text").map((block) => block.text).join("");
1598
- if (update.role === "user" && !before.title && text.trim())
1599
- patch.title = compact(text, TITLE_LENGTH);
1741
+ // A title is the person's words, not the context their client or Claude wrapped around them.
1742
+ if (update.role === "user" && !before.title && readUserText(text).text.trim())
1743
+ patch.title = compact(readUserText(text).text, TITLE_LENGTH);
1600
1744
  if (update.role === "agent" && text.trim())
1601
1745
  patch.preview = compact(text, PREVIEW_LENGTH);
1602
1746
  if (live.turnActive && update.role === "agent")
@@ -1604,7 +1748,7 @@ export class SessionHub {
1604
1748
  break;
1605
1749
  }
1606
1750
  case "user_message_chunk": {
1607
- const text = update.content.type === "text" ? update.content.text : "";
1751
+ const text = update.content.type === "text" ? readUserText(update.content.text).text : "";
1608
1752
  if (text && !before.title)
1609
1753
  patch.title = compact(text, TITLE_LENGTH);
1610
1754
  if (text && !before.preview)