@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/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
|
package/dist/engine/index.cjs
CHANGED
|
@@ -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
|
-
|
|
1582
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
1684
|
-
|
|
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:
|
|
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(
|
|
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
|
|
1752
|
-
if (!
|
|
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:
|
|
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.
|
|
1803
|
-
|
|
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.
|
|
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]
|
|
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.
|
|
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,
|
|
1880
|
-
return (candidate.provider === "claude" ? describeClaude(candidate) : describeCodex(candidate,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|