@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # @ai-matrx/coding-sessions
2
2
 
3
+ ## 0.3.0
4
+
5
+ - With an owner of the apps' index (`titleHints`), this package reads none of the apps' own files:
6
+ Codex thread names and title search come from the owner (`appTitles`); the standalone host still
7
+ reads `session_index.jsonl` itself.
8
+ - `titleStatus()` / `retryTitles()` (engine + bridge): a failing owner shows "app titles unavailable —
9
+ Retry" beside Your other chats, never a silent fallback. `TitleHints.problem()` / `retry()`.
10
+ - `displayTitle()` (`./sessions`): the list's trim — leading list markers and quotes dropped, cut to
11
+ `DISPLAY_MAX` (100); the stored title stays exact and is the hover text. Vectors in the one file.
12
+ - A held chat whose own file is gone reads its history from the holder's copy
13
+ (`homes.claudeVault`, default `~/.claude/claude-app-vault/<id>/transcript.jsonl`).
14
+ - `engine.setHomes()`: a host that learns the homes later (Matrx 2: Codex homes from its core).
15
+ - `TitleHint.source` / `DiscoveredSession.titleSource` may be `"unknown"` (an owner that did not say).
16
+ - **Consumer action:** none for hosts without an owner; Matrx 2 passes `titleHints` with `problem`/`retry`.
17
+
18
+ ## 0.2.0
19
+
20
+ - **`./sessions` (new, pure):** the one title rule for a coding chat — `resolveSessionTitle` (override >
21
+ app record > remembered app title > custom-title > ai-title > agent-name > summary > first prompt >
22
+ last prompt), `cleanTitle` (first non-empty line, 300 chars), `isBridgePlaceholder` (the bridge's old
23
+ `AI Matrx · <source>` stamp is never a title), `claudeTranscriptTitleSources`, and the `TitleHints`
24
+ port through which the owner of the apps' index (Matrx 2's core) supplies the apps' own titles,
25
+ archived / scheduled flags and chats whose file is gone. Vectors: `fixtures/title-precedence.json`.
26
+ - Discovery and search titles come from that rule (no more 140-char flattening); Codex archived
27
+ rollouts (`archived_sessions/`) are read and marked `archived`; every Codex home in `homes.codex`.
28
+ - Engine option `titleHints`: discovery overlays the apps' titles without waiting (`titlePending`
29
+ meanwhile, a `titles` event when they land), lists the app's chats whose file is gone as
30
+ `unavailable`, and the engine's own sessions show the app's title once known.
31
+ - Screen: "titles updating" beside Your other chats; archived chats and scheduled runs hidden by
32
+ default behind Show hidden chats (a pin shows them); "file missing" rows. A rename made before the
33
+ provider id is known moves to the chat's lasting key; renames are cut at 300 (was 200).
34
+ - **Consumer action:** `ProviderHomes` is now `{ claudeProjects, codex: CodexHome[] }` (was
35
+ `codexSessions` / `codexIndex`); build one with `codexHome(dir)`. Hosts that own an apps index pass
36
+ `titleHints`.
37
+
3
38
  ## 0.1.166
4
39
 
5
40
  Automatic changed-only republish (docs/metadata drift since the last tag — see
@@ -38,9 +38,11 @@ __export(engine_exports, {
38
38
  buildCleanEnv: () => buildCleanEnv,
39
39
  claudeDefaults: () => claudeDefaults,
40
40
  codexDefaults: () => codexDefaults,
41
+ codexHome: () => codexHome,
41
42
  createAiMatrxRawBackupSink: () => createAiMatrxRawBackupSink,
42
43
  createCodingSessionsEngine: () => createCodingSessionsEngine,
43
44
  createRawBackupScheduler: () => createRawBackupScheduler,
45
+ createTitleHintsStore: () => createTitleHintsStore,
44
46
  defaultProviderHomes: () => defaultProviderHomes,
45
47
  describeActivity: () => describeActivity,
46
48
  describeProviderStatus: () => describeProviderStatus,
@@ -53,7 +55,8 @@ __export(engine_exports, {
53
55
  readSessionFile: () => readSessionFile,
54
56
  searchLocalChats: () => searchLocalChats,
55
57
  sessionActivity: () => sessionActivity,
56
- snippetAround: () => snippetAround
58
+ snippetAround: () => snippetAround,
59
+ titleHintKey: () => titleHintKey
57
60
  });
58
61
  module.exports = __toCommonJS(engine_exports);
59
62
 
@@ -1196,6 +1199,9 @@ var PROVIDER_LABEL = {
1196
1199
  claude: "Claude Code",
1197
1200
  codex: "Codex"
1198
1201
  };
1202
+ function chatKey(provider, providerSessionId, sessionId) {
1203
+ return providerSessionId ? `${provider}:${providerSessionId}` : `session:${sessionId ?? ""}`;
1204
+ }
1199
1205
  var TERMINAL_RUN_STATUSES = ["completed", "stopped", "failed"];
1200
1206
  function isTerminalRunStatus(status) {
1201
1207
  return TERMINAL_RUN_STATUSES.includes(status);
@@ -1510,6 +1516,78 @@ var import_promises4 = __toESM(require("fs/promises"), 1);
1510
1516
  var import_node_os3 = __toESM(require("os"), 1);
1511
1517
  var import_node_path5 = __toESM(require("path"), 1);
1512
1518
 
1519
+ // src/sessions/titles.ts
1520
+ var TITLE_MAX = 300;
1521
+ var BRIDGE_PLACEHOLDER = /^AI Matrx · [a-z-]+$/;
1522
+ var TITLE_PRECEDENCE = [
1523
+ /** The person's rename inside Matrx (Coding Sessions' preference). */
1524
+ "override",
1525
+ /** What the coding app's own sidebar shows: Claude's record `title` (renames included); Codex's thread name; Cursor's name. */
1526
+ "app",
1527
+ /** The title the app last showed, remembered by the claude-accounts engine after the record went away. */
1528
+ "app-remembered",
1529
+ /** Claude Code transcript entries (the last one of each kind that is not the bridge placeholder). */
1530
+ "custom-title",
1531
+ "ai-title",
1532
+ "agent-name",
1533
+ "summary",
1534
+ /** The person's first typed message (Codex: the thread's title / first user message). */
1535
+ "first-prompt",
1536
+ "last-prompt"
1537
+ ];
1538
+ var STAMPABLE = /* @__PURE__ */ new Set(["custom-title", "ai-title", "agent-name"]);
1539
+ function isBridgePlaceholder(value) {
1540
+ return typeof value === "string" && BRIDGE_PLACEHOLDER.test(value.trim());
1541
+ }
1542
+ function cleanTitle(value) {
1543
+ if (typeof value !== "string") return null;
1544
+ const line = value.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0);
1545
+ if (!line) return null;
1546
+ return line.length > TITLE_MAX ? `${line.slice(0, TITLE_MAX - 1)}\u2026` : line;
1547
+ }
1548
+ function resolveSessionTitle(sources) {
1549
+ for (const source of TITLE_PRECEDENCE) {
1550
+ const raw = sources[source];
1551
+ if (STAMPABLE.has(source) && isBridgePlaceholder(raw)) continue;
1552
+ const title = cleanTitle(raw);
1553
+ if (title) return { title, source };
1554
+ }
1555
+ return null;
1556
+ }
1557
+ function claudeTranscriptTitleSources(head, tail = []) {
1558
+ const out = {};
1559
+ const take = (source, value) => {
1560
+ if (typeof value !== "string" || !value.trim()) return;
1561
+ if (STAMPABLE.has(source) && isBridgePlaceholder(value)) return;
1562
+ out[source] = value;
1563
+ };
1564
+ const normalizer = createClaudeNormalizer();
1565
+ let firstPrompt = null;
1566
+ const scan = (entries, findPrompt) => {
1567
+ for (const entry of entries) {
1568
+ if (typeof entry !== "object" || entry === null) continue;
1569
+ const e = entry;
1570
+ if (e.type === "custom-title") take("custom-title", e.customTitle);
1571
+ else if (e.type === "ai-title") take("ai-title", e.aiTitle);
1572
+ else if (e.type === "agent-name") take("agent-name", e.agentName);
1573
+ else if (e.type === "summary") take("summary", e.summary);
1574
+ else if (e.type === "last-prompt") take("last-prompt", e.lastPrompt);
1575
+ else if (findPrompt && firstPrompt === null) {
1576
+ for (const item of normalizer.push(entry)) {
1577
+ if (item.kind === "user" && item.text.trim()) {
1578
+ firstPrompt = item.text;
1579
+ break;
1580
+ }
1581
+ }
1582
+ }
1583
+ }
1584
+ };
1585
+ scan(head, true);
1586
+ scan(tail, false);
1587
+ if (firstPrompt !== null) out["first-prompt"] = firstPrompt;
1588
+ return out;
1589
+ }
1590
+
1513
1591
  // src/engine/activity.ts
1514
1592
  var import_node_child_process2 = require("child_process");
1515
1593
  var RECENT_WRITE_MS = 2 * 60 * 1e3;
@@ -1575,11 +1653,18 @@ function describeActivity(input) {
1575
1653
  }
1576
1654
 
1577
1655
  // src/engine/files.ts
1656
+ function codexHome(dir) {
1657
+ return {
1658
+ sessions: import_node_path5.default.join(dir, "sessions"),
1659
+ archived: import_node_path5.default.join(dir, "archived_sessions"),
1660
+ index: import_node_path5.default.join(dir, "session_index.jsonl")
1661
+ };
1662
+ }
1578
1663
  function defaultProviderHomes(home = import_node_os3.default.homedir()) {
1579
1664
  return {
1580
1665
  claudeProjects: import_node_path5.default.join(home, ".claude", "projects"),
1581
- codexSessions: import_node_path5.default.join(home, ".codex", "sessions"),
1582
- codexIndex: import_node_path5.default.join(home, ".codex", "session_index.jsonl")
1666
+ claudeVault: import_node_path5.default.join(home, ".claude", "claude-app-vault"),
1667
+ codex: [codexHome(import_node_path5.default.join(home, ".codex"))]
1583
1668
  };
1584
1669
  }
1585
1670
  var UUIDISH = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
@@ -1613,7 +1698,7 @@ async function claudeCandidates(root) {
1613
1698
  }
1614
1699
  return out;
1615
1700
  }
1616
- async function codexCandidates(root) {
1701
+ async function codexTree(root) {
1617
1702
  const out = [];
1618
1703
  for (const year of await safeReaddir(root)) {
1619
1704
  if (!year.isDirectory()) continue;
@@ -1621,17 +1706,23 @@ async function codexCandidates(root) {
1621
1706
  if (!month.isDirectory()) continue;
1622
1707
  for (const day of await safeReaddir(import_node_path5.default.join(root, year.name, month.name))) {
1623
1708
  if (!day.isDirectory()) continue;
1624
- const dir = import_node_path5.default.join(root, year.name, month.name, day.name);
1625
- const entries = await safeReaddir(dir);
1626
- const stats = await Promise.all(
1627
- 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)))
1628
- );
1629
- for (const s of stats) if (s) out.push(s);
1709
+ out.push(...await codexFlat(import_node_path5.default.join(root, year.name, month.name, day.name), false));
1630
1710
  }
1631
1711
  }
1632
1712
  }
1633
1713
  return out;
1634
1714
  }
1715
+ async function codexFlat(dir, archived) {
1716
+ const entries = await safeReaddir(dir);
1717
+ const stats = await Promise.all(
1718
+ 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)))
1719
+ );
1720
+ return stats.filter((s) => s !== null).map((s) => archived ? { ...s, archived: true } : s);
1721
+ }
1722
+ async function codexCandidates(homes) {
1723
+ const lists = await Promise.all(homes.flatMap((h) => [codexTree(h.sessions), codexFlat(h.archived, true)]));
1724
+ return lists.flat();
1725
+ }
1635
1726
  async function readSlice(file, start, length) {
1636
1727
  const handle = await import_promises4.default.open(file, "r");
1637
1728
  try {
@@ -1653,56 +1744,38 @@ function completeLines(text, droppedFirst) {
1653
1744
  }
1654
1745
  var HEAD_BYTES = 256 * 1024;
1655
1746
  var TAIL_BYTES = 128 * 1024;
1656
- function oneLine(text, max = 140) {
1657
- const flat = text.replace(/\s+/g, " ").trim();
1658
- return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
1659
- }
1660
1747
  async function describeClaude(candidate) {
1661
1748
  const head = parseJsonl(completeLines(await readSlice(candidate.file, 0, HEAD_BYTES), false));
1662
1749
  const tailStart = Math.max(0, candidate.size - TAIL_BYTES);
1663
1750
  const tail = tailStart > 0 ? parseJsonl(completeLines(await readSlice(candidate.file, tailStart, TAIL_BYTES), true)) : head;
1664
1751
  let cwd = null;
1665
- let firstPrompt = null;
1666
1752
  let entrypoint = null;
1667
- const normalizer = createClaudeNormalizer();
1668
1753
  for (const entry of head) {
1669
1754
  if (typeof entry !== "object" || entry === null) continue;
1670
1755
  const e = entry;
1671
1756
  if (!cwd && typeof e.cwd === "string") cwd = e.cwd;
1672
1757
  if (!entrypoint && typeof e.entrypoint === "string") entrypoint = e.entrypoint;
1673
- if (firstPrompt === null) {
1674
- for (const item of normalizer.push(entry)) {
1675
- if (item.kind === "user") {
1676
- firstPrompt = item.text;
1677
- break;
1678
- }
1679
- }
1680
- }
1681
- if (cwd && firstPrompt !== null) break;
1758
+ if (cwd && entrypoint) break;
1682
1759
  }
1683
- const titles = { custom: null, ai: null, agent: null, summary: null, last: null };
1684
- for (const entry of [...head, ...tail]) {
1685
- if (typeof entry !== "object" || entry === null) continue;
1686
- const e = entry;
1687
- if (e.type === "custom-title" && typeof e.customTitle === "string") titles.custom = e.customTitle;
1688
- if (e.type === "ai-title" && typeof e.aiTitle === "string") titles.ai = e.aiTitle;
1689
- if (e.type === "agent-name" && typeof e.agentName === "string") titles.agent = e.agentName;
1690
- if (e.type === "summary" && typeof e.summary === "string") titles.summary = e.summary;
1691
- if (e.type === "last-prompt" && typeof e.lastPrompt === "string") titles.last = e.lastPrompt;
1692
- }
1693
- const title = titles.custom ?? titles.ai ?? titles.agent ?? titles.summary ?? firstPrompt ?? titles.last;
1694
- if (!title) return null;
1760
+ const resolved = resolveSessionTitle(claudeTranscriptTitleSources(head, tailStart > 0 ? tail : []));
1761
+ if (!resolved) return null;
1695
1762
  return {
1696
1763
  provider: "claude",
1697
1764
  providerSessionId: candidate.id,
1698
1765
  cwd,
1699
- title: oneLine(title),
1766
+ title: resolved.title,
1767
+ titleSource: resolved.source,
1700
1768
  updatedAt: new Date(candidate.mtimeMs).toISOString(),
1701
1769
  source: entrypoint,
1702
1770
  activeElsewhere: null
1703
1771
  };
1704
1772
  }
1705
- async function readCodexIndex(file) {
1773
+ async function readCodexIndex(files) {
1774
+ const names = /* @__PURE__ */ new Map();
1775
+ for (const file of files) for (const [id, name] of await readCodexIndexFile(file)) if (!names.has(id)) names.set(id, name);
1776
+ return names;
1777
+ }
1778
+ async function readCodexIndexFile(file) {
1706
1779
  const names = /* @__PURE__ */ new Map();
1707
1780
  let text;
1708
1781
  try {
@@ -1748,18 +1821,26 @@ async function describeCodex(candidate, names) {
1748
1821
  }
1749
1822
  }
1750
1823
  if (isSubAgent) return null;
1751
- const title = names.get(candidate.id) ?? firstPrompt;
1752
- if (!title) return null;
1824
+ const resolved = resolveSessionTitle({ app: names.get(candidate.id), "first-prompt": firstPrompt });
1825
+ if (!resolved) return null;
1753
1826
  return {
1754
1827
  provider: "codex",
1755
1828
  providerSessionId: candidate.id,
1756
1829
  cwd,
1757
- title: oneLine(title),
1830
+ title: resolved.title,
1831
+ titleSource: resolved.source,
1832
+ ...candidate.archived ? { archived: true } : {},
1758
1833
  updatedAt: new Date(candidate.mtimeMs).toISOString(),
1759
1834
  source,
1760
1835
  activeElsewhere: null
1761
1836
  };
1762
1837
  }
1838
+ async function codexNames(homes, appTitles) {
1839
+ if (!appTitles) return readCodexIndex(homes.codex.map((h) => h.index));
1840
+ const out = /* @__PURE__ */ new Map();
1841
+ for (const [key, title] of appTitles) if (key.startsWith("codex:")) out.set(key.slice(6), title);
1842
+ return out;
1843
+ }
1763
1844
  async function readTail(file, size) {
1764
1845
  const start = Math.max(0, size - TAIL_BYTES);
1765
1846
  return parseJsonl(completeLines(await readSlice(file, start, Math.min(size, TAIL_BYTES)), start > 0));
@@ -1799,8 +1880,8 @@ async function discoverLocalSessions(options = {}) {
1799
1880
  const homes = options.homes ?? defaultProviderHomes();
1800
1881
  const [claude, codex, names] = await Promise.all([
1801
1882
  claudeCandidates(homes.claudeProjects),
1802
- codexCandidates(homes.codexSessions),
1803
- readCodexIndex(homes.codexIndex)
1883
+ codexCandidates(homes.codex),
1884
+ codexNames(homes, options.appTitles)
1804
1885
  ]);
1805
1886
  const candidates = [...claude, ...codex].sort((a, b) => b.mtimeMs - a.mtimeMs);
1806
1887
  const processes = await listProcesses();
@@ -1820,9 +1901,14 @@ async function discoverLocalSessions(options = {}) {
1820
1901
  }
1821
1902
  async function findSessionFile(provider, providerSessionId, homes = defaultProviderHomes()) {
1822
1903
  if (!/^[0-9a-f-]{36}$/i.test(providerSessionId)) return null;
1823
- const candidates = provider === "claude" ? await claudeCandidates(homes.claudeProjects) : await codexCandidates(homes.codexSessions);
1904
+ const candidates = provider === "claude" ? await claudeCandidates(homes.claudeProjects) : await codexCandidates(homes.codex);
1824
1905
  const hits = candidates.filter((c) => c.id === providerSessionId).sort((a, b) => b.mtimeMs - a.mtimeMs);
1825
- return hits[0]?.file ?? null;
1906
+ if (hits[0]) return hits[0].file;
1907
+ if (provider === "claude" && homes.claudeVault) {
1908
+ const copy = import_node_path5.default.join(homes.claudeVault, providerSessionId, "transcript.jsonl");
1909
+ if ((await import_promises4.default.stat(copy).catch(() => null))?.isFile()) return copy;
1910
+ }
1911
+ return null;
1826
1912
  }
1827
1913
  var HISTORY_BUDGET = 24 * 1024 * 1024;
1828
1914
  var MAX_HISTORY_BUDGET = 256 * 1024 * 1024;
@@ -1872,12 +1958,12 @@ async function readSessionFile(provider, file, endOffset, maxBytes = HISTORY_BUD
1872
1958
  async function listSessionFiles(homes = defaultProviderHomes()) {
1873
1959
  const [claude, codex] = await Promise.all([
1874
1960
  claudeCandidates(homes.claudeProjects),
1875
- codexCandidates(homes.codexSessions)
1961
+ codexCandidates(homes.codex)
1876
1962
  ]);
1877
1963
  return [...claude, ...codex].sort((a, b) => b.mtimeMs - a.mtimeMs);
1878
1964
  }
1879
- async function describeSessionFile(candidate, codexNames) {
1880
- return (candidate.provider === "claude" ? describeClaude(candidate) : describeCodex(candidate, codexNames)).catch(
1965
+ async function describeSessionFile(candidate, codexNames2) {
1966
+ return (candidate.provider === "claude" ? describeClaude(candidate) : describeCodex(candidate, codexNames2)).catch(
1881
1967
  () => null
1882
1968
  );
1883
1969
  }
@@ -1915,7 +2001,7 @@ var PreferenceStore = class {
1915
2001
  if (!key || key.length > 200) throw new Error("That chat can't be found.");
1916
2002
  const next = { ...this.data[key] };
1917
2003
  if ("title" in patch) {
1918
- const title = typeof patch.title === "string" ? patch.title.replace(/\s+/g, " ").trim().slice(0, 200) : "";
2004
+ const title = typeof patch.title === "string" ? patch.title.replace(/\s+/g, " ").trim().slice(0, TITLE_MAX) : "";
1919
2005
  if (title) next.title = title;
1920
2006
  else delete next.title;
1921
2007
  }
@@ -1933,6 +2019,18 @@ var PreferenceStore = class {
1933
2019
  this.persist();
1934
2020
  return this.all();
1935
2021
  }
2022
+ /**
2023
+ * Move a preference to the chat's lasting key (`session:<id>` → `<provider>:<id>` once the
2024
+ * provider's id is known), so a rename made early is the one rename everywhere. A preference
2025
+ * already under the lasting key wins field by field.
2026
+ */
2027
+ rekey(from, to) {
2028
+ if (from === to || !this.data[from]) return false;
2029
+ this.data[to] = { ...this.data[from], ...this.data[to] };
2030
+ delete this.data[from];
2031
+ this.persist();
2032
+ return true;
2033
+ }
1936
2034
  persist() {
1937
2035
  const snapshot = JSON.stringify(this.data, null, 1);
1938
2036
  this.writing = this.writing.then(async () => {
@@ -2060,12 +2158,12 @@ async function searchLocalChats(query, options = {}) {
2060
2158
  const raw = JSON.stringify(needle).slice(1, -1);
2061
2159
  const limit = Math.max(1, Math.min(options.limit ?? 30, 100));
2062
2160
  const deadline = Date.now() + (options.budgetMs ?? 3500);
2063
- const names = await readCodexIndex(homes.codexIndex);
2161
+ const names = await codexNames(homes, options.appTitles);
2064
2162
  const hits = [];
2065
2163
  let i = cursor;
2066
2164
  for (; i < files.length && hits.length < limit && Date.now() < deadline; i += 1) {
2067
2165
  const candidate = files[i];
2068
- const named = candidate.provider === "codex" ? names.get(candidate.id) : void 0;
2166
+ const named = options.appTitles?.get(`${candidate.provider}:${candidate.id}`) ?? (candidate.provider === "codex" ? names.get(candidate.id) : void 0);
2069
2167
  const nameHit = named !== void 0 && named.toLowerCase().includes(needle);
2070
2168
  const scanned = nameHit ? null : await scanFile(candidate.provider, candidate.file, needle, raw, deadline);
2071
2169
  if (scanned === TIMED_OUT) {
@@ -2095,6 +2193,81 @@ async function searchLocalChats(query, options = {}) {
2095
2193
  };
2096
2194
  }
2097
2195
 
2196
+ // src/sessions/hints.ts
2197
+ function titleHintKey(provider, sessionId) {
2198
+ return `${provider}:${sessionId}`;
2199
+ }
2200
+ function createTitleHintsStore() {
2201
+ let hints = null;
2202
+ let problem = null;
2203
+ const listeners = /* @__PURE__ */ new Set();
2204
+ const tell = () => {
2205
+ for (const l of listeners) l();
2206
+ };
2207
+ return {
2208
+ current: () => hints,
2209
+ problem: () => problem,
2210
+ subscribe(listener) {
2211
+ listeners.add(listener);
2212
+ return () => listeners.delete(listener);
2213
+ },
2214
+ set(next) {
2215
+ hints = next;
2216
+ problem = null;
2217
+ tell();
2218
+ },
2219
+ setProblem(next) {
2220
+ if (next === problem) return;
2221
+ problem = next;
2222
+ tell();
2223
+ }
2224
+ };
2225
+ }
2226
+ function appTitlesOf(hints) {
2227
+ const out = /* @__PURE__ */ new Map();
2228
+ for (const [key, hint] of hints) out.set(key, hint.title);
2229
+ return out;
2230
+ }
2231
+
2232
+ // src/engine/title-hints.ts
2233
+ function overlayHint(chat, hints, expected) {
2234
+ if (!expected) return chat;
2235
+ if (hints === null) return { ...chat, titlePending: true };
2236
+ const hint = hints.get(titleHintKey(chat.provider, chat.providerSessionId));
2237
+ if (!hint) return chat;
2238
+ return {
2239
+ ...chat,
2240
+ title: hint.title,
2241
+ titleSource: hint.source,
2242
+ ...hint.archived || chat.archived ? { archived: true } : {},
2243
+ ...hint.scheduled ? { scheduled: true } : {}
2244
+ };
2245
+ }
2246
+ function unavailableFromHints(hints, covered) {
2247
+ if (!hints) return [];
2248
+ const out = [];
2249
+ for (const [key, hint] of hints) {
2250
+ if (!hint.unavailable || covered.has(key)) continue;
2251
+ const at = key.indexOf(":");
2252
+ const provider = key.slice(0, at);
2253
+ if (provider !== "claude" && provider !== "codex") continue;
2254
+ out.push({
2255
+ provider,
2256
+ providerSessionId: key.slice(at + 1),
2257
+ cwd: null,
2258
+ title: hint.title,
2259
+ titleSource: hint.source,
2260
+ updatedAt: new Date(hint.lastActiveMs ?? 0).toISOString(),
2261
+ source: null,
2262
+ activeElsewhere: null,
2263
+ unavailable: hint.unavailable,
2264
+ ...hint.archived ? { archived: true } : {},
2265
+ ...hint.scheduled ? { scheduled: true } : {}
2266
+ });
2267
+ }
2268
+ return out.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
2269
+ }
2270
+
2098
2271
  // src/engine/store.ts
2099
2272
  var import_node_crypto4 = require("crypto");
2100
2273
  var import_promises6 = __toESM(require("fs/promises"), 1);
@@ -2365,7 +2538,7 @@ function describeAuthStatus(provider, probe) {
2365
2538
  async function createCodingSessionsEngine(options = {}) {
2366
2539
  const dataDir = options.dataDir ?? import_node_path9.default.join(import_node_os5.default.homedir(), ".matrx", "coding-sessions");
2367
2540
  const stateDir = import_node_path9.default.join(dataDir, "paperclip-state");
2368
- const homes = options.homes ?? defaultProviderHomes();
2541
+ let homes = options.homes ?? defaultProviderHomes();
2369
2542
  const index = new RunIndex(dataDir);
2370
2543
  await index.load();
2371
2544
  const preferences = new PreferenceStore(dataDir);
@@ -2445,6 +2618,18 @@ async function createCodingSessionsEngine(options = {}) {
2445
2618
  }
2446
2619
  }
2447
2620
  }
2621
+ const titleHints = options.titleHints;
2622
+ const hintsNow = () => titleHints?.current() ?? null;
2623
+ const appTitles = () => {
2624
+ if (!titleHints) return void 0;
2625
+ const hints = hintsNow();
2626
+ return hints ? appTitlesOf(hints) : /* @__PURE__ */ new Map();
2627
+ };
2628
+ const unsubscribeHints = titleHints?.subscribe(() => emit({ type: "titles" })) ?? null;
2629
+ const withHintTitle = (session) => {
2630
+ const hint = session.providerSessionId ? hintsNow()?.get(titleHintKey(session.provider, session.providerSessionId)) : void 0;
2631
+ return hint ? { ...session, title: hint.title } : session;
2632
+ };
2448
2633
  function saveSession(session) {
2449
2634
  index.putSession(session);
2450
2635
  emit({ type: "session", session });
@@ -2601,7 +2786,12 @@ async function createCodingSessionsEngine(options = {}) {
2601
2786
  if (sid && sid !== knownProviderSession) {
2602
2787
  knownProviderSession = sid;
2603
2788
  const current = index.session(session.id);
2604
- if (current) saveSession({ ...current, providerSessionId: sid });
2789
+ if (current) {
2790
+ saveSession({ ...current, providerSessionId: sid });
2791
+ if (preferences.rekey(chatKey(current.provider, null, current.id), chatKey(current.provider, sid))) {
2792
+ emit({ type: "preferences", preferences: preferences.all() });
2793
+ }
2794
+ }
2605
2795
  }
2606
2796
  for (const item of items) emitItem(liveRun, item);
2607
2797
  }
@@ -2909,7 +3099,9 @@ async function createCodingSessionsEngine(options = {}) {
2909
3099
  );
2910
3100
  },
2911
3101
  async discoverSessions(opts) {
2912
- const found = await discoverLocalSessions({ limit: opts?.limit, homes, excludePids: ourPids() });
3102
+ const read = await discoverLocalSessions({ limit: opts?.limit, homes, excludePids: ourPids(), appTitles: appTitles() });
3103
+ const hints = hintsNow();
3104
+ const found = read.map((chat) => overlayHint(chat, hints, titleHints !== void 0));
2913
3105
  if (backups) {
2914
3106
  for (const chat of found) {
2915
3107
  const key = `${chat.provider}:${chat.providerSessionId}`;
@@ -2920,7 +3112,12 @@ async function createCodingSessionsEngine(options = {}) {
2920
3112
  }
2921
3113
  }
2922
3114
  }
2923
- return found;
3115
+ const limit = opts?.limit ?? 60;
3116
+ const oldest = found.length >= limit ? found[found.length - 1]?.updatedAt ?? "" : "";
3117
+ const gone = unavailableFromHints(hints, new Set(found.map((c) => titleHintKey(c.provider, c.providerSessionId)))).filter(
3118
+ (c) => c.updatedAt >= oldest
3119
+ );
3120
+ return gone.length ? [...found, ...gone].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, limit) : found;
2924
3121
  },
2925
3122
  async recentFolders() {
2926
3123
  const seen = /* @__PURE__ */ new Map();
@@ -2940,8 +3137,19 @@ async function createCodingSessionsEngine(options = {}) {
2940
3137
  }))
2941
3138
  );
2942
3139
  },
3140
+ async titleStatus() {
3141
+ if (!titleHints) return { owner: false, pending: false, problem: null };
3142
+ const problem = titleHints.problem?.() ?? null;
3143
+ return { owner: true, pending: hintsNow() === null && problem === null, problem };
3144
+ },
3145
+ async retryTitles() {
3146
+ titleHints?.retry?.();
3147
+ },
3148
+ setHomes(next) {
3149
+ homes = next;
3150
+ },
2943
3151
  async listSessions() {
2944
- return index.sessions();
3152
+ return index.sessions().map(withHintTitle);
2945
3153
  },
2946
3154
  async listRuns(sessionId) {
2947
3155
  const session = index.session(sessionId);
@@ -3116,7 +3324,9 @@ async function createCodingSessionsEngine(options = {}) {
3116
3324
  return all;
3117
3325
  },
3118
3326
  async searchChats(query, opts) {
3119
- return searchLocalChats(String(query ?? ""), { homes, cursor: opts?.cursor, limit: opts?.limit });
3327
+ const result = await searchLocalChats(String(query ?? ""), { homes, cursor: opts?.cursor, limit: opts?.limit, appTitles: appTitles() });
3328
+ const hints = hintsNow();
3329
+ return { ...result, hits: result.hits.map((hit) => ({ ...hit, title: hints?.get(titleHintKey(hit.provider, hit.providerSessionId))?.title ?? hit.title })) };
3120
3330
  },
3121
3331
  async openFolder(raw, target) {
3122
3332
  const folder = await assertFolder(String(raw ?? ""));
@@ -3146,6 +3356,7 @@ async function createCodingSessionsEngine(options = {}) {
3146
3356
  return agents.handleMcp(secret, body);
3147
3357
  },
3148
3358
  async dispose() {
3359
+ unsubscribeHints?.();
3149
3360
  for (const liveRun of live.values()) {
3150
3361
  liveRun.stopRequested = true;
3151
3362
  if (liveRun.pgid !== null) killGroup(liveRun);
@@ -3202,9 +3413,11 @@ function openFolderIn(folder, target) {
3202
3413
  buildCleanEnv,
3203
3414
  claudeDefaults,
3204
3415
  codexDefaults,
3416
+ codexHome,
3205
3417
  createAiMatrxRawBackupSink,
3206
3418
  createCodingSessionsEngine,
3207
3419
  createRawBackupScheduler,
3420
+ createTitleHintsStore,
3208
3421
  defaultProviderHomes,
3209
3422
  describeActivity,
3210
3423
  describeProviderStatus,
@@ -3217,6 +3430,7 @@ function openFolderIn(folder, target) {
3217
3430
  readSessionFile,
3218
3431
  searchLocalChats,
3219
3432
  sessionActivity,
3220
- snippetAround
3433
+ snippetAround,
3434
+ titleHintKey
3221
3435
  });
3222
3436
  //# sourceMappingURL=index.cjs.map