@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.
- package/CHANGELOG.md +35 -0
- package/dist/engine/index.cjs +273 -59
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.cts +1 -1
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/index.js +269 -58
- package/dist/engine/index.js.map +1 -1
- package/dist/{index-CDeDKQHe.d.cts → index-Bbe11HoB.d.cts} +108 -3
- package/dist/{index-CDeDKQHe.d.ts → index-Bbe11HoB.d.ts} +108 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -1
- package/dist/index.d.ts +30 -1
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +47 -6
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +37 -0
- package/dist/react/index.d.ts +37 -0
- package/dist/react/index.js +47 -6
- package/dist/react/index.js.map +1 -1
- package/dist/server/index.cjs +241 -57
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +2 -2
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +241 -57
- package/dist/server/index.js.map +1 -1
- package/dist/sessions/index.cjs +360 -0
- package/dist/sessions/index.cjs.map +1 -0
- package/dist/sessions/index.d.cts +71 -0
- package/dist/sessions/index.d.ts +71 -0
- package/dist/sessions/index.js +337 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/titles-DjxUb0ji.d.cts +48 -0
- package/dist/titles-DjxUb0ji.d.ts +48 -0
- package/dist/ui/assets/index-BqzTnRFe.css +2 -0
- package/dist/ui/assets/{index-7eK8XQtL.js → index-U98AY8r-.js} +17 -17
- package/dist/ui/index.html +2 -2
- package/package.json +13 -3
- package/dist/ui/assets/index-BqpVx_My.css +0 -2
package/dist/server/index.cjs
CHANGED
|
@@ -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
|
-
|
|
1535
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
1637
|
-
|
|
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:
|
|
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(
|
|
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
|
|
1705
|
-
if (!
|
|
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:
|
|
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.
|
|
1756
|
-
|
|
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.
|
|
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]
|
|
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.
|
|
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,
|
|
1833
|
-
return (candidate.provider === "claude" ? describeClaude(candidate) : describeCodex(candidate,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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",
|