@ai-matrx/coding-sessions 0.1.166 → 0.3.0

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 (38) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/engine/index.cjs +273 -59
  3. package/dist/engine/index.cjs.map +1 -1
  4. package/dist/engine/index.d.cts +1 -1
  5. package/dist/engine/index.d.ts +1 -1
  6. package/dist/engine/index.js +269 -58
  7. package/dist/engine/index.js.map +1 -1
  8. package/dist/{index-CDeDKQHe.d.cts → index-Bbe11HoB.d.cts} +108 -3
  9. package/dist/{index-CDeDKQHe.d.ts → index-Bbe11HoB.d.ts} +108 -3
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +30 -1
  12. package/dist/index.d.ts +30 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/react/index.cjs +47 -6
  15. package/dist/react/index.cjs.map +1 -1
  16. package/dist/react/index.d.cts +37 -0
  17. package/dist/react/index.d.ts +37 -0
  18. package/dist/react/index.js +47 -6
  19. package/dist/react/index.js.map +1 -1
  20. package/dist/server/index.cjs +241 -57
  21. package/dist/server/index.cjs.map +1 -1
  22. package/dist/server/index.d.cts +2 -2
  23. package/dist/server/index.d.ts +2 -2
  24. package/dist/server/index.js +241 -57
  25. package/dist/server/index.js.map +1 -1
  26. package/dist/sessions/index.cjs +360 -0
  27. package/dist/sessions/index.cjs.map +1 -0
  28. package/dist/sessions/index.d.cts +71 -0
  29. package/dist/sessions/index.d.ts +71 -0
  30. package/dist/sessions/index.js +337 -0
  31. package/dist/sessions/index.js.map +1 -0
  32. package/dist/titles-DjxUb0ji.d.cts +48 -0
  33. package/dist/titles-DjxUb0ji.d.ts +48 -0
  34. package/dist/ui/assets/index-BqzTnRFe.css +2 -0
  35. package/dist/ui/assets/{index-7eK8XQtL.js → index-U98AY8r-.js} +17 -17
  36. package/dist/ui/index.html +2 -2
  37. package/package.json +13 -3
  38. package/dist/ui/assets/index-BqpVx_My.css +0 -2
@@ -1149,6 +1149,9 @@ var PROVIDER_LABEL = {
1149
1149
  claude: "Claude Code",
1150
1150
  codex: "Codex"
1151
1151
  };
1152
+ function chatKey(provider, providerSessionId, sessionId) {
1153
+ return providerSessionId ? `${provider}:${providerSessionId}` : `session:${sessionId ?? ""}`;
1154
+ }
1152
1155
  var TERMINAL_RUN_STATUSES = ["completed", "stopped", "failed"];
1153
1156
  function isTerminalRunStatus(status) {
1154
1157
  return TERMINAL_RUN_STATUSES.includes(status);
@@ -1463,6 +1466,78 @@ var import_promises4 = __toESM(require("fs/promises"), 1);
1463
1466
  var import_node_os3 = __toESM(require("os"), 1);
1464
1467
  var import_node_path5 = __toESM(require("path"), 1);
1465
1468
 
1469
+ // src/sessions/titles.ts
1470
+ var TITLE_MAX = 300;
1471
+ var BRIDGE_PLACEHOLDER = /^AI Matrx · [a-z-]+$/;
1472
+ var TITLE_PRECEDENCE = [
1473
+ /** The person's rename inside Matrx (Coding Sessions' preference). */
1474
+ "override",
1475
+ /** What the coding app's own sidebar shows: Claude's record `title` (renames included); Codex's thread name; Cursor's name. */
1476
+ "app",
1477
+ /** The title the app last showed, remembered by the claude-accounts engine after the record went away. */
1478
+ "app-remembered",
1479
+ /** Claude Code transcript entries (the last one of each kind that is not the bridge placeholder). */
1480
+ "custom-title",
1481
+ "ai-title",
1482
+ "agent-name",
1483
+ "summary",
1484
+ /** The person's first typed message (Codex: the thread's title / first user message). */
1485
+ "first-prompt",
1486
+ "last-prompt"
1487
+ ];
1488
+ var STAMPABLE = /* @__PURE__ */ new Set(["custom-title", "ai-title", "agent-name"]);
1489
+ function isBridgePlaceholder(value) {
1490
+ return typeof value === "string" && BRIDGE_PLACEHOLDER.test(value.trim());
1491
+ }
1492
+ function cleanTitle(value) {
1493
+ if (typeof value !== "string") return null;
1494
+ const line = value.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0);
1495
+ if (!line) return null;
1496
+ return line.length > TITLE_MAX ? `${line.slice(0, TITLE_MAX - 1)}\u2026` : line;
1497
+ }
1498
+ function resolveSessionTitle(sources) {
1499
+ for (const source of TITLE_PRECEDENCE) {
1500
+ const raw = sources[source];
1501
+ if (STAMPABLE.has(source) && isBridgePlaceholder(raw)) continue;
1502
+ const title = cleanTitle(raw);
1503
+ if (title) return { title, source };
1504
+ }
1505
+ return null;
1506
+ }
1507
+ function claudeTranscriptTitleSources(head, tail = []) {
1508
+ const out = {};
1509
+ const take = (source, value) => {
1510
+ if (typeof value !== "string" || !value.trim()) return;
1511
+ if (STAMPABLE.has(source) && isBridgePlaceholder(value)) return;
1512
+ out[source] = value;
1513
+ };
1514
+ const normalizer = createClaudeNormalizer();
1515
+ let firstPrompt = null;
1516
+ const scan = (entries, findPrompt) => {
1517
+ for (const entry of entries) {
1518
+ if (typeof entry !== "object" || entry === null) continue;
1519
+ const e = entry;
1520
+ if (e.type === "custom-title") take("custom-title", e.customTitle);
1521
+ else if (e.type === "ai-title") take("ai-title", e.aiTitle);
1522
+ else if (e.type === "agent-name") take("agent-name", e.agentName);
1523
+ else if (e.type === "summary") take("summary", e.summary);
1524
+ else if (e.type === "last-prompt") take("last-prompt", e.lastPrompt);
1525
+ else if (findPrompt && firstPrompt === null) {
1526
+ for (const item of normalizer.push(entry)) {
1527
+ if (item.kind === "user" && item.text.trim()) {
1528
+ firstPrompt = item.text;
1529
+ break;
1530
+ }
1531
+ }
1532
+ }
1533
+ }
1534
+ };
1535
+ scan(head, true);
1536
+ scan(tail, false);
1537
+ if (firstPrompt !== null) out["first-prompt"] = firstPrompt;
1538
+ return out;
1539
+ }
1540
+
1466
1541
  // src/engine/activity.ts
1467
1542
  var import_node_child_process2 = require("child_process");
1468
1543
  var RECENT_WRITE_MS = 2 * 60 * 1e3;
@@ -1528,11 +1603,18 @@ function describeActivity(input) {
1528
1603
  }
1529
1604
 
1530
1605
  // src/engine/files.ts
1606
+ function codexHome(dir) {
1607
+ return {
1608
+ sessions: import_node_path5.default.join(dir, "sessions"),
1609
+ archived: import_node_path5.default.join(dir, "archived_sessions"),
1610
+ index: import_node_path5.default.join(dir, "session_index.jsonl")
1611
+ };
1612
+ }
1531
1613
  function defaultProviderHomes(home = import_node_os3.default.homedir()) {
1532
1614
  return {
1533
1615
  claudeProjects: import_node_path5.default.join(home, ".claude", "projects"),
1534
- codexSessions: import_node_path5.default.join(home, ".codex", "sessions"),
1535
- codexIndex: import_node_path5.default.join(home, ".codex", "session_index.jsonl")
1616
+ claudeVault: import_node_path5.default.join(home, ".claude", "claude-app-vault"),
1617
+ codex: [codexHome(import_node_path5.default.join(home, ".codex"))]
1536
1618
  };
1537
1619
  }
1538
1620
  var UUIDISH = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
@@ -1566,7 +1648,7 @@ async function claudeCandidates(root) {
1566
1648
  }
1567
1649
  return out;
1568
1650
  }
1569
- async function codexCandidates(root) {
1651
+ async function codexTree(root) {
1570
1652
  const out = [];
1571
1653
  for (const year of await safeReaddir(root)) {
1572
1654
  if (!year.isDirectory()) continue;
@@ -1574,17 +1656,23 @@ async function codexCandidates(root) {
1574
1656
  if (!month.isDirectory()) continue;
1575
1657
  for (const day of await safeReaddir(import_node_path5.default.join(root, year.name, month.name))) {
1576
1658
  if (!day.isDirectory()) continue;
1577
- const dir = import_node_path5.default.join(root, year.name, month.name, day.name);
1578
- const entries = await safeReaddir(dir);
1579
- const stats = await Promise.all(
1580
- entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => statCandidate("codex", import_node_path5.default.join(dir, e.name)))
1581
- );
1582
- for (const s of stats) if (s) out.push(s);
1659
+ out.push(...await codexFlat(import_node_path5.default.join(root, year.name, month.name, day.name), false));
1583
1660
  }
1584
1661
  }
1585
1662
  }
1586
1663
  return out;
1587
1664
  }
1665
+ async function codexFlat(dir, archived) {
1666
+ const entries = await safeReaddir(dir);
1667
+ const stats = await Promise.all(
1668
+ entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => statCandidate("codex", import_node_path5.default.join(dir, e.name)))
1669
+ );
1670
+ return stats.filter((s) => s !== null).map((s) => archived ? { ...s, archived: true } : s);
1671
+ }
1672
+ async function codexCandidates(homes) {
1673
+ const lists = await Promise.all(homes.flatMap((h) => [codexTree(h.sessions), codexFlat(h.archived, true)]));
1674
+ return lists.flat();
1675
+ }
1588
1676
  async function readSlice(file, start, length) {
1589
1677
  const handle = await import_promises4.default.open(file, "r");
1590
1678
  try {
@@ -1606,56 +1694,38 @@ function completeLines(text, droppedFirst) {
1606
1694
  }
1607
1695
  var HEAD_BYTES = 256 * 1024;
1608
1696
  var TAIL_BYTES = 128 * 1024;
1609
- function oneLine(text, max = 140) {
1610
- const flat = text.replace(/\s+/g, " ").trim();
1611
- return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
1612
- }
1613
1697
  async function describeClaude(candidate) {
1614
1698
  const head = parseJsonl(completeLines(await readSlice(candidate.file, 0, HEAD_BYTES), false));
1615
1699
  const tailStart = Math.max(0, candidate.size - TAIL_BYTES);
1616
1700
  const tail = tailStart > 0 ? parseJsonl(completeLines(await readSlice(candidate.file, tailStart, TAIL_BYTES), true)) : head;
1617
1701
  let cwd = null;
1618
- let firstPrompt = null;
1619
1702
  let entrypoint = null;
1620
- const normalizer = createClaudeNormalizer();
1621
1703
  for (const entry of head) {
1622
1704
  if (typeof entry !== "object" || entry === null) continue;
1623
1705
  const e = entry;
1624
1706
  if (!cwd && typeof e.cwd === "string") cwd = e.cwd;
1625
1707
  if (!entrypoint && typeof e.entrypoint === "string") entrypoint = e.entrypoint;
1626
- if (firstPrompt === null) {
1627
- for (const item of normalizer.push(entry)) {
1628
- if (item.kind === "user") {
1629
- firstPrompt = item.text;
1630
- break;
1631
- }
1632
- }
1633
- }
1634
- if (cwd && firstPrompt !== null) break;
1708
+ if (cwd && entrypoint) break;
1635
1709
  }
1636
- const titles = { custom: null, ai: null, agent: null, summary: null, last: null };
1637
- for (const entry of [...head, ...tail]) {
1638
- if (typeof entry !== "object" || entry === null) continue;
1639
- const e = entry;
1640
- if (e.type === "custom-title" && typeof e.customTitle === "string") titles.custom = e.customTitle;
1641
- if (e.type === "ai-title" && typeof e.aiTitle === "string") titles.ai = e.aiTitle;
1642
- if (e.type === "agent-name" && typeof e.agentName === "string") titles.agent = e.agentName;
1643
- if (e.type === "summary" && typeof e.summary === "string") titles.summary = e.summary;
1644
- if (e.type === "last-prompt" && typeof e.lastPrompt === "string") titles.last = e.lastPrompt;
1645
- }
1646
- const title = titles.custom ?? titles.ai ?? titles.agent ?? titles.summary ?? firstPrompt ?? titles.last;
1647
- if (!title) return null;
1710
+ const resolved = resolveSessionTitle(claudeTranscriptTitleSources(head, tailStart > 0 ? tail : []));
1711
+ if (!resolved) return null;
1648
1712
  return {
1649
1713
  provider: "claude",
1650
1714
  providerSessionId: candidate.id,
1651
1715
  cwd,
1652
- title: oneLine(title),
1716
+ title: resolved.title,
1717
+ titleSource: resolved.source,
1653
1718
  updatedAt: new Date(candidate.mtimeMs).toISOString(),
1654
1719
  source: entrypoint,
1655
1720
  activeElsewhere: null
1656
1721
  };
1657
1722
  }
1658
- async function readCodexIndex(file) {
1723
+ async function readCodexIndex(files) {
1724
+ const names = /* @__PURE__ */ new Map();
1725
+ for (const file of files) for (const [id, name] of await readCodexIndexFile(file)) if (!names.has(id)) names.set(id, name);
1726
+ return names;
1727
+ }
1728
+ async function readCodexIndexFile(file) {
1659
1729
  const names = /* @__PURE__ */ new Map();
1660
1730
  let text;
1661
1731
  try {
@@ -1701,18 +1771,26 @@ async function describeCodex(candidate, names) {
1701
1771
  }
1702
1772
  }
1703
1773
  if (isSubAgent) return null;
1704
- const title = names.get(candidate.id) ?? firstPrompt;
1705
- if (!title) return null;
1774
+ const resolved = resolveSessionTitle({ app: names.get(candidate.id), "first-prompt": firstPrompt });
1775
+ if (!resolved) return null;
1706
1776
  return {
1707
1777
  provider: "codex",
1708
1778
  providerSessionId: candidate.id,
1709
1779
  cwd,
1710
- title: oneLine(title),
1780
+ title: resolved.title,
1781
+ titleSource: resolved.source,
1782
+ ...candidate.archived ? { archived: true } : {},
1711
1783
  updatedAt: new Date(candidate.mtimeMs).toISOString(),
1712
1784
  source,
1713
1785
  activeElsewhere: null
1714
1786
  };
1715
1787
  }
1788
+ async function codexNames(homes, appTitles) {
1789
+ if (!appTitles) return readCodexIndex(homes.codex.map((h) => h.index));
1790
+ const out = /* @__PURE__ */ new Map();
1791
+ for (const [key, title] of appTitles) if (key.startsWith("codex:")) out.set(key.slice(6), title);
1792
+ return out;
1793
+ }
1716
1794
  async function readTail(file, size) {
1717
1795
  const start = Math.max(0, size - TAIL_BYTES);
1718
1796
  return parseJsonl(completeLines(await readSlice(file, start, Math.min(size, TAIL_BYTES)), start > 0));
@@ -1752,8 +1830,8 @@ async function discoverLocalSessions(options = {}) {
1752
1830
  const homes = options.homes ?? defaultProviderHomes();
1753
1831
  const [claude, codex, names] = await Promise.all([
1754
1832
  claudeCandidates(homes.claudeProjects),
1755
- codexCandidates(homes.codexSessions),
1756
- readCodexIndex(homes.codexIndex)
1833
+ codexCandidates(homes.codex),
1834
+ codexNames(homes, options.appTitles)
1757
1835
  ]);
1758
1836
  const candidates = [...claude, ...codex].sort((a, b) => b.mtimeMs - a.mtimeMs);
1759
1837
  const processes = await listProcesses();
@@ -1773,9 +1851,14 @@ async function discoverLocalSessions(options = {}) {
1773
1851
  }
1774
1852
  async function findSessionFile(provider, providerSessionId, homes = defaultProviderHomes()) {
1775
1853
  if (!/^[0-9a-f-]{36}$/i.test(providerSessionId)) return null;
1776
- const candidates = provider === "claude" ? await claudeCandidates(homes.claudeProjects) : await codexCandidates(homes.codexSessions);
1854
+ const candidates = provider === "claude" ? await claudeCandidates(homes.claudeProjects) : await codexCandidates(homes.codex);
1777
1855
  const hits = candidates.filter((c) => c.id === providerSessionId).sort((a, b) => b.mtimeMs - a.mtimeMs);
1778
- return hits[0]?.file ?? null;
1856
+ if (hits[0]) return hits[0].file;
1857
+ if (provider === "claude" && homes.claudeVault) {
1858
+ const copy = import_node_path5.default.join(homes.claudeVault, providerSessionId, "transcript.jsonl");
1859
+ if ((await import_promises4.default.stat(copy).catch(() => null))?.isFile()) return copy;
1860
+ }
1861
+ return null;
1779
1862
  }
1780
1863
  var HISTORY_BUDGET = 24 * 1024 * 1024;
1781
1864
  var MAX_HISTORY_BUDGET = 256 * 1024 * 1024;
@@ -1825,12 +1908,12 @@ async function readSessionFile(provider, file, endOffset, maxBytes = HISTORY_BUD
1825
1908
  async function listSessionFiles(homes = defaultProviderHomes()) {
1826
1909
  const [claude, codex] = await Promise.all([
1827
1910
  claudeCandidates(homes.claudeProjects),
1828
- codexCandidates(homes.codexSessions)
1911
+ codexCandidates(homes.codex)
1829
1912
  ]);
1830
1913
  return [...claude, ...codex].sort((a, b) => b.mtimeMs - a.mtimeMs);
1831
1914
  }
1832
- async function describeSessionFile(candidate, codexNames) {
1833
- return (candidate.provider === "claude" ? describeClaude(candidate) : describeCodex(candidate, codexNames)).catch(
1915
+ async function describeSessionFile(candidate, codexNames2) {
1916
+ return (candidate.provider === "claude" ? describeClaude(candidate) : describeCodex(candidate, codexNames2)).catch(
1834
1917
  () => null
1835
1918
  );
1836
1919
  }
@@ -1868,7 +1951,7 @@ var PreferenceStore = class {
1868
1951
  if (!key || key.length > 200) throw new Error("That chat can't be found.");
1869
1952
  const next = { ...this.data[key] };
1870
1953
  if ("title" in patch) {
1871
- const title = typeof patch.title === "string" ? patch.title.replace(/\s+/g, " ").trim().slice(0, 200) : "";
1954
+ const title = typeof patch.title === "string" ? patch.title.replace(/\s+/g, " ").trim().slice(0, TITLE_MAX) : "";
1872
1955
  if (title) next.title = title;
1873
1956
  else delete next.title;
1874
1957
  }
@@ -1886,6 +1969,18 @@ var PreferenceStore = class {
1886
1969
  this.persist();
1887
1970
  return this.all();
1888
1971
  }
1972
+ /**
1973
+ * Move a preference to the chat's lasting key (`session:<id>` → `<provider>:<id>` once the
1974
+ * provider's id is known), so a rename made early is the one rename everywhere. A preference
1975
+ * already under the lasting key wins field by field.
1976
+ */
1977
+ rekey(from, to) {
1978
+ if (from === to || !this.data[from]) return false;
1979
+ this.data[to] = { ...this.data[from], ...this.data[to] };
1980
+ delete this.data[from];
1981
+ this.persist();
1982
+ return true;
1983
+ }
1889
1984
  persist() {
1890
1985
  const snapshot = JSON.stringify(this.data, null, 1);
1891
1986
  this.writing = this.writing.then(async () => {
@@ -2013,12 +2108,12 @@ async function searchLocalChats(query, options = {}) {
2013
2108
  const raw = JSON.stringify(needle).slice(1, -1);
2014
2109
  const limit = Math.max(1, Math.min(options.limit ?? 30, 100));
2015
2110
  const deadline = Date.now() + (options.budgetMs ?? 3500);
2016
- const names = await readCodexIndex(homes.codexIndex);
2111
+ const names = await codexNames(homes, options.appTitles);
2017
2112
  const hits = [];
2018
2113
  let i = cursor;
2019
2114
  for (; i < files.length && hits.length < limit && Date.now() < deadline; i += 1) {
2020
2115
  const candidate = files[i];
2021
- const named = candidate.provider === "codex" ? names.get(candidate.id) : void 0;
2116
+ const named = options.appTitles?.get(`${candidate.provider}:${candidate.id}`) ?? (candidate.provider === "codex" ? names.get(candidate.id) : void 0);
2022
2117
  const nameHit = named !== void 0 && named.toLowerCase().includes(needle);
2023
2118
  const scanned = nameHit ? null : await scanFile(candidate.provider, candidate.file, needle, raw, deadline);
2024
2119
  if (scanned === TIMED_OUT) {
@@ -2048,6 +2143,55 @@ async function searchLocalChats(query, options = {}) {
2048
2143
  };
2049
2144
  }
2050
2145
 
2146
+ // src/sessions/hints.ts
2147
+ function titleHintKey(provider, sessionId) {
2148
+ return `${provider}:${sessionId}`;
2149
+ }
2150
+ function appTitlesOf(hints) {
2151
+ const out = /* @__PURE__ */ new Map();
2152
+ for (const [key, hint] of hints) out.set(key, hint.title);
2153
+ return out;
2154
+ }
2155
+
2156
+ // src/engine/title-hints.ts
2157
+ function overlayHint(chat, hints, expected) {
2158
+ if (!expected) return chat;
2159
+ if (hints === null) return { ...chat, titlePending: true };
2160
+ const hint = hints.get(titleHintKey(chat.provider, chat.providerSessionId));
2161
+ if (!hint) return chat;
2162
+ return {
2163
+ ...chat,
2164
+ title: hint.title,
2165
+ titleSource: hint.source,
2166
+ ...hint.archived || chat.archived ? { archived: true } : {},
2167
+ ...hint.scheduled ? { scheduled: true } : {}
2168
+ };
2169
+ }
2170
+ function unavailableFromHints(hints, covered) {
2171
+ if (!hints) return [];
2172
+ const out = [];
2173
+ for (const [key, hint] of hints) {
2174
+ if (!hint.unavailable || covered.has(key)) continue;
2175
+ const at = key.indexOf(":");
2176
+ const provider = key.slice(0, at);
2177
+ if (provider !== "claude" && provider !== "codex") continue;
2178
+ out.push({
2179
+ provider,
2180
+ providerSessionId: key.slice(at + 1),
2181
+ cwd: null,
2182
+ title: hint.title,
2183
+ titleSource: hint.source,
2184
+ updatedAt: new Date(hint.lastActiveMs ?? 0).toISOString(),
2185
+ source: null,
2186
+ activeElsewhere: null,
2187
+ unavailable: hint.unavailable,
2188
+ ...hint.archived ? { archived: true } : {},
2189
+ ...hint.scheduled ? { scheduled: true } : {}
2190
+ });
2191
+ }
2192
+ return out.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
2193
+ }
2194
+
2051
2195
  // src/engine/store.ts
2052
2196
  var import_node_crypto4 = require("crypto");
2053
2197
  var import_promises6 = __toESM(require("fs/promises"), 1);
@@ -2318,7 +2462,7 @@ function describeAuthStatus(provider, probe) {
2318
2462
  async function createCodingSessionsEngine(options = {}) {
2319
2463
  const dataDir = options.dataDir ?? import_node_path9.default.join(import_node_os5.default.homedir(), ".matrx", "coding-sessions");
2320
2464
  const stateDir = import_node_path9.default.join(dataDir, "paperclip-state");
2321
- const homes = options.homes ?? defaultProviderHomes();
2465
+ let homes = options.homes ?? defaultProviderHomes();
2322
2466
  const index = new RunIndex(dataDir);
2323
2467
  await index.load();
2324
2468
  const preferences = new PreferenceStore(dataDir);
@@ -2398,6 +2542,18 @@ async function createCodingSessionsEngine(options = {}) {
2398
2542
  }
2399
2543
  }
2400
2544
  }
2545
+ const titleHints = options.titleHints;
2546
+ const hintsNow = () => titleHints?.current() ?? null;
2547
+ const appTitles = () => {
2548
+ if (!titleHints) return void 0;
2549
+ const hints = hintsNow();
2550
+ return hints ? appTitlesOf(hints) : /* @__PURE__ */ new Map();
2551
+ };
2552
+ const unsubscribeHints = titleHints?.subscribe(() => emit({ type: "titles" })) ?? null;
2553
+ const withHintTitle = (session) => {
2554
+ const hint = session.providerSessionId ? hintsNow()?.get(titleHintKey(session.provider, session.providerSessionId)) : void 0;
2555
+ return hint ? { ...session, title: hint.title } : session;
2556
+ };
2401
2557
  function saveSession(session) {
2402
2558
  index.putSession(session);
2403
2559
  emit({ type: "session", session });
@@ -2554,7 +2710,12 @@ async function createCodingSessionsEngine(options = {}) {
2554
2710
  if (sid && sid !== knownProviderSession) {
2555
2711
  knownProviderSession = sid;
2556
2712
  const current = index.session(session.id);
2557
- if (current) saveSession({ ...current, providerSessionId: sid });
2713
+ if (current) {
2714
+ saveSession({ ...current, providerSessionId: sid });
2715
+ if (preferences.rekey(chatKey(current.provider, null, current.id), chatKey(current.provider, sid))) {
2716
+ emit({ type: "preferences", preferences: preferences.all() });
2717
+ }
2718
+ }
2558
2719
  }
2559
2720
  for (const item of items) emitItem(liveRun, item);
2560
2721
  }
@@ -2862,7 +3023,9 @@ async function createCodingSessionsEngine(options = {}) {
2862
3023
  );
2863
3024
  },
2864
3025
  async discoverSessions(opts) {
2865
- const found = await discoverLocalSessions({ limit: opts?.limit, homes, excludePids: ourPids() });
3026
+ const read = await discoverLocalSessions({ limit: opts?.limit, homes, excludePids: ourPids(), appTitles: appTitles() });
3027
+ const hints = hintsNow();
3028
+ const found = read.map((chat) => overlayHint(chat, hints, titleHints !== void 0));
2866
3029
  if (backups) {
2867
3030
  for (const chat of found) {
2868
3031
  const key = `${chat.provider}:${chat.providerSessionId}`;
@@ -2873,7 +3036,12 @@ async function createCodingSessionsEngine(options = {}) {
2873
3036
  }
2874
3037
  }
2875
3038
  }
2876
- return found;
3039
+ const limit = opts?.limit ?? 60;
3040
+ const oldest = found.length >= limit ? found[found.length - 1]?.updatedAt ?? "" : "";
3041
+ const gone = unavailableFromHints(hints, new Set(found.map((c) => titleHintKey(c.provider, c.providerSessionId)))).filter(
3042
+ (c) => c.updatedAt >= oldest
3043
+ );
3044
+ return gone.length ? [...found, ...gone].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, limit) : found;
2877
3045
  },
2878
3046
  async recentFolders() {
2879
3047
  const seen = /* @__PURE__ */ new Map();
@@ -2893,8 +3061,19 @@ async function createCodingSessionsEngine(options = {}) {
2893
3061
  }))
2894
3062
  );
2895
3063
  },
3064
+ async titleStatus() {
3065
+ if (!titleHints) return { owner: false, pending: false, problem: null };
3066
+ const problem = titleHints.problem?.() ?? null;
3067
+ return { owner: true, pending: hintsNow() === null && problem === null, problem };
3068
+ },
3069
+ async retryTitles() {
3070
+ titleHints?.retry?.();
3071
+ },
3072
+ setHomes(next) {
3073
+ homes = next;
3074
+ },
2896
3075
  async listSessions() {
2897
- return index.sessions();
3076
+ return index.sessions().map(withHintTitle);
2898
3077
  },
2899
3078
  async listRuns(sessionId) {
2900
3079
  const session = index.session(sessionId);
@@ -3069,7 +3248,9 @@ async function createCodingSessionsEngine(options = {}) {
3069
3248
  return all;
3070
3249
  },
3071
3250
  async searchChats(query, opts) {
3072
- return searchLocalChats(String(query ?? ""), { homes, cursor: opts?.cursor, limit: opts?.limit });
3251
+ const result = await searchLocalChats(String(query ?? ""), { homes, cursor: opts?.cursor, limit: opts?.limit, appTitles: appTitles() });
3252
+ const hints = hintsNow();
3253
+ return { ...result, hits: result.hits.map((hit) => ({ ...hit, title: hints?.get(titleHintKey(hit.provider, hit.providerSessionId))?.title ?? hit.title })) };
3073
3254
  },
3074
3255
  async openFolder(raw, target) {
3075
3256
  const folder = await assertFolder(String(raw ?? ""));
@@ -3099,6 +3280,7 @@ async function createCodingSessionsEngine(options = {}) {
3099
3280
  return agents.handleMcp(secret, body);
3100
3281
  },
3101
3282
  async dispose() {
3283
+ unsubscribeHints?.();
3102
3284
  for (const liveRun of live.values()) {
3103
3285
  liveRun.stopRequested = true;
3104
3286
  if (liveRun.pgid !== null) killGroup(liveRun);
@@ -3150,6 +3332,8 @@ function openFolderIn(folder, target) {
3150
3332
  var BRIDGE_METHODS = [
3151
3333
  "providers",
3152
3334
  "discoverSessions",
3335
+ "titleStatus",
3336
+ "retryTitles",
3153
3337
  "recentFolders",
3154
3338
  "listSessions",
3155
3339
  "listRuns",