@ai-matrx/coding-sessions 0.1.165 → 0.2.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 +26 -0
- package/dist/engine/index.cjs +224 -54
- 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 +220 -53
- package/dist/engine/index.js.map +1 -1
- package/dist/{index-CDeDKQHe.d.cts → index-29t7n472.d.cts} +81 -3
- package/dist/{index-CDeDKQHe.d.ts → index-29t7n472.d.ts} +81 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +15 -3
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +23 -0
- package/dist/react/index.d.ts +23 -0
- package/dist/react/index.js +15 -3
- package/dist/react/index.js.map +1 -1
- package/dist/server/index.cjs +201 -52
- 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 +201 -52
- package/dist/server/index.js.map +1 -1
- package/dist/sessions/index.cjs +335 -0
- package/dist/sessions/index.cjs.map +1 -0
- package/dist/sessions/index.d.cts +63 -0
- package/dist/sessions/index.d.ts +63 -0
- package/dist/sessions/index.js +312 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/titles-eCnA5sY7.d.cts +41 -0
- package/dist/titles-eCnA5sY7.d.ts +41 -0
- package/dist/ui/assets/{index-5Iix3spw.js → index-SuRr0-D2.js} +2 -2
- package/dist/ui/index.html +1 -1
- package/package.json +13 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @ai-matrx/coding-sessions
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
- **`./sessions` (new, pure):** the one title rule for a coding chat — `resolveSessionTitle` (override >
|
|
6
|
+
app record > remembered app title > custom-title > ai-title > agent-name > summary > first prompt >
|
|
7
|
+
last prompt), `cleanTitle` (first non-empty line, 300 chars), `isBridgePlaceholder` (the bridge's old
|
|
8
|
+
`AI Matrx · <source>` stamp is never a title), `claudeTranscriptTitleSources`, and the `TitleHints`
|
|
9
|
+
port through which the owner of the apps' index (Matrx 2's core) supplies the apps' own titles,
|
|
10
|
+
archived / scheduled flags and chats whose file is gone. Vectors: `fixtures/title-precedence.json`.
|
|
11
|
+
- Discovery and search titles come from that rule (no more 140-char flattening); Codex archived
|
|
12
|
+
rollouts (`archived_sessions/`) are read and marked `archived`; every Codex home in `homes.codex`.
|
|
13
|
+
- Engine option `titleHints`: discovery overlays the apps' titles without waiting (`titlePending`
|
|
14
|
+
meanwhile, a `titles` event when they land), lists the app's chats whose file is gone as
|
|
15
|
+
`unavailable`, and the engine's own sessions show the app's title once known.
|
|
16
|
+
- Screen: "titles updating" beside Your other chats; archived chats and scheduled runs hidden by
|
|
17
|
+
default behind Show hidden chats (a pin shows them); "file missing" rows. A rename made before the
|
|
18
|
+
provider id is known moves to the chat's lasting key; renames are cut at 300 (was 200).
|
|
19
|
+
- **Consumer action:** `ProviderHomes` is now `{ claudeProjects, codex: CodexHome[] }` (was
|
|
20
|
+
`codexSessions` / `codexIndex`); build one with `codexHome(dir)`. Hosts that own an apps index pass
|
|
21
|
+
`titleHints`.
|
|
22
|
+
|
|
23
|
+
## 0.1.166
|
|
24
|
+
|
|
25
|
+
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
26
|
+
`git diff npm/coding-sessions/v0.1.165..npm/coding-sessions/v0.1.166 -- apps/shared/coding-sessions`).
|
|
27
|
+
No source changes intended and no consumer action required.
|
|
28
|
+
|
|
3
29
|
## 0.1.165
|
|
4
30
|
|
|
5
31
|
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,17 @@ 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
|
-
codexIndex: import_node_path5.default.join(home, ".codex", "session_index.jsonl")
|
|
1666
|
+
codex: [codexHome(import_node_path5.default.join(home, ".codex"))]
|
|
1583
1667
|
};
|
|
1584
1668
|
}
|
|
1585
1669
|
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 +1697,7 @@ async function claudeCandidates(root) {
|
|
|
1613
1697
|
}
|
|
1614
1698
|
return out;
|
|
1615
1699
|
}
|
|
1616
|
-
async function
|
|
1700
|
+
async function codexTree(root) {
|
|
1617
1701
|
const out = [];
|
|
1618
1702
|
for (const year of await safeReaddir(root)) {
|
|
1619
1703
|
if (!year.isDirectory()) continue;
|
|
@@ -1621,17 +1705,23 @@ async function codexCandidates(root) {
|
|
|
1621
1705
|
if (!month.isDirectory()) continue;
|
|
1622
1706
|
for (const day of await safeReaddir(import_node_path5.default.join(root, year.name, month.name))) {
|
|
1623
1707
|
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);
|
|
1708
|
+
out.push(...await codexFlat(import_node_path5.default.join(root, year.name, month.name, day.name), false));
|
|
1630
1709
|
}
|
|
1631
1710
|
}
|
|
1632
1711
|
}
|
|
1633
1712
|
return out;
|
|
1634
1713
|
}
|
|
1714
|
+
async function codexFlat(dir, archived) {
|
|
1715
|
+
const entries = await safeReaddir(dir);
|
|
1716
|
+
const stats = await Promise.all(
|
|
1717
|
+
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)))
|
|
1718
|
+
);
|
|
1719
|
+
return stats.filter((s) => s !== null).map((s) => archived ? { ...s, archived: true } : s);
|
|
1720
|
+
}
|
|
1721
|
+
async function codexCandidates(homes) {
|
|
1722
|
+
const lists = await Promise.all(homes.flatMap((h) => [codexTree(h.sessions), codexFlat(h.archived, true)]));
|
|
1723
|
+
return lists.flat();
|
|
1724
|
+
}
|
|
1635
1725
|
async function readSlice(file, start, length) {
|
|
1636
1726
|
const handle = await import_promises4.default.open(file, "r");
|
|
1637
1727
|
try {
|
|
@@ -1653,56 +1743,38 @@ function completeLines(text, droppedFirst) {
|
|
|
1653
1743
|
}
|
|
1654
1744
|
var HEAD_BYTES = 256 * 1024;
|
|
1655
1745
|
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
1746
|
async function describeClaude(candidate) {
|
|
1661
1747
|
const head = parseJsonl(completeLines(await readSlice(candidate.file, 0, HEAD_BYTES), false));
|
|
1662
1748
|
const tailStart = Math.max(0, candidate.size - TAIL_BYTES);
|
|
1663
1749
|
const tail = tailStart > 0 ? parseJsonl(completeLines(await readSlice(candidate.file, tailStart, TAIL_BYTES), true)) : head;
|
|
1664
1750
|
let cwd = null;
|
|
1665
|
-
let firstPrompt = null;
|
|
1666
1751
|
let entrypoint = null;
|
|
1667
|
-
const normalizer = createClaudeNormalizer();
|
|
1668
1752
|
for (const entry of head) {
|
|
1669
1753
|
if (typeof entry !== "object" || entry === null) continue;
|
|
1670
1754
|
const e = entry;
|
|
1671
1755
|
if (!cwd && typeof e.cwd === "string") cwd = e.cwd;
|
|
1672
1756
|
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;
|
|
1757
|
+
if (cwd && entrypoint) break;
|
|
1682
1758
|
}
|
|
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;
|
|
1759
|
+
const resolved = resolveSessionTitle(claudeTranscriptTitleSources(head, tailStart > 0 ? tail : []));
|
|
1760
|
+
if (!resolved) return null;
|
|
1695
1761
|
return {
|
|
1696
1762
|
provider: "claude",
|
|
1697
1763
|
providerSessionId: candidate.id,
|
|
1698
1764
|
cwd,
|
|
1699
|
-
title:
|
|
1765
|
+
title: resolved.title,
|
|
1766
|
+
titleSource: resolved.source,
|
|
1700
1767
|
updatedAt: new Date(candidate.mtimeMs).toISOString(),
|
|
1701
1768
|
source: entrypoint,
|
|
1702
1769
|
activeElsewhere: null
|
|
1703
1770
|
};
|
|
1704
1771
|
}
|
|
1705
|
-
async function readCodexIndex(
|
|
1772
|
+
async function readCodexIndex(files) {
|
|
1773
|
+
const names = /* @__PURE__ */ new Map();
|
|
1774
|
+
for (const file of files) for (const [id, name] of await readCodexIndexFile(file)) if (!names.has(id)) names.set(id, name);
|
|
1775
|
+
return names;
|
|
1776
|
+
}
|
|
1777
|
+
async function readCodexIndexFile(file) {
|
|
1706
1778
|
const names = /* @__PURE__ */ new Map();
|
|
1707
1779
|
let text;
|
|
1708
1780
|
try {
|
|
@@ -1748,13 +1820,15 @@ async function describeCodex(candidate, names) {
|
|
|
1748
1820
|
}
|
|
1749
1821
|
}
|
|
1750
1822
|
if (isSubAgent) return null;
|
|
1751
|
-
const
|
|
1752
|
-
if (!
|
|
1823
|
+
const resolved = resolveSessionTitle({ app: names.get(candidate.id), "first-prompt": firstPrompt });
|
|
1824
|
+
if (!resolved) return null;
|
|
1753
1825
|
return {
|
|
1754
1826
|
provider: "codex",
|
|
1755
1827
|
providerSessionId: candidate.id,
|
|
1756
1828
|
cwd,
|
|
1757
|
-
title:
|
|
1829
|
+
title: resolved.title,
|
|
1830
|
+
titleSource: resolved.source,
|
|
1831
|
+
...candidate.archived ? { archived: true } : {},
|
|
1758
1832
|
updatedAt: new Date(candidate.mtimeMs).toISOString(),
|
|
1759
1833
|
source,
|
|
1760
1834
|
activeElsewhere: null
|
|
@@ -1799,8 +1873,8 @@ async function discoverLocalSessions(options = {}) {
|
|
|
1799
1873
|
const homes = options.homes ?? defaultProviderHomes();
|
|
1800
1874
|
const [claude, codex, names] = await Promise.all([
|
|
1801
1875
|
claudeCandidates(homes.claudeProjects),
|
|
1802
|
-
codexCandidates(homes.
|
|
1803
|
-
readCodexIndex(homes.
|
|
1876
|
+
codexCandidates(homes.codex),
|
|
1877
|
+
readCodexIndex(homes.codex.map((h) => h.index))
|
|
1804
1878
|
]);
|
|
1805
1879
|
const candidates = [...claude, ...codex].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1806
1880
|
const processes = await listProcesses();
|
|
@@ -1820,7 +1894,7 @@ async function discoverLocalSessions(options = {}) {
|
|
|
1820
1894
|
}
|
|
1821
1895
|
async function findSessionFile(provider, providerSessionId, homes = defaultProviderHomes()) {
|
|
1822
1896
|
if (!/^[0-9a-f-]{36}$/i.test(providerSessionId)) return null;
|
|
1823
|
-
const candidates = provider === "claude" ? await claudeCandidates(homes.claudeProjects) : await codexCandidates(homes.
|
|
1897
|
+
const candidates = provider === "claude" ? await claudeCandidates(homes.claudeProjects) : await codexCandidates(homes.codex);
|
|
1824
1898
|
const hits = candidates.filter((c) => c.id === providerSessionId).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1825
1899
|
return hits[0]?.file ?? null;
|
|
1826
1900
|
}
|
|
@@ -1872,7 +1946,7 @@ async function readSessionFile(provider, file, endOffset, maxBytes = HISTORY_BUD
|
|
|
1872
1946
|
async function listSessionFiles(homes = defaultProviderHomes()) {
|
|
1873
1947
|
const [claude, codex] = await Promise.all([
|
|
1874
1948
|
claudeCandidates(homes.claudeProjects),
|
|
1875
|
-
codexCandidates(homes.
|
|
1949
|
+
codexCandidates(homes.codex)
|
|
1876
1950
|
]);
|
|
1877
1951
|
return [...claude, ...codex].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1878
1952
|
}
|
|
@@ -1915,7 +1989,7 @@ var PreferenceStore = class {
|
|
|
1915
1989
|
if (!key || key.length > 200) throw new Error("That chat can't be found.");
|
|
1916
1990
|
const next = { ...this.data[key] };
|
|
1917
1991
|
if ("title" in patch) {
|
|
1918
|
-
const title = typeof patch.title === "string" ? patch.title.replace(/\s+/g, " ").trim().slice(0,
|
|
1992
|
+
const title = typeof patch.title === "string" ? patch.title.replace(/\s+/g, " ").trim().slice(0, TITLE_MAX) : "";
|
|
1919
1993
|
if (title) next.title = title;
|
|
1920
1994
|
else delete next.title;
|
|
1921
1995
|
}
|
|
@@ -1933,6 +2007,18 @@ var PreferenceStore = class {
|
|
|
1933
2007
|
this.persist();
|
|
1934
2008
|
return this.all();
|
|
1935
2009
|
}
|
|
2010
|
+
/**
|
|
2011
|
+
* Move a preference to the chat's lasting key (`session:<id>` → `<provider>:<id>` once the
|
|
2012
|
+
* provider's id is known), so a rename made early is the one rename everywhere. A preference
|
|
2013
|
+
* already under the lasting key wins field by field.
|
|
2014
|
+
*/
|
|
2015
|
+
rekey(from, to) {
|
|
2016
|
+
if (from === to || !this.data[from]) return false;
|
|
2017
|
+
this.data[to] = { ...this.data[from], ...this.data[to] };
|
|
2018
|
+
delete this.data[from];
|
|
2019
|
+
this.persist();
|
|
2020
|
+
return true;
|
|
2021
|
+
}
|
|
1936
2022
|
persist() {
|
|
1937
2023
|
const snapshot = JSON.stringify(this.data, null, 1);
|
|
1938
2024
|
this.writing = this.writing.then(async () => {
|
|
@@ -2060,7 +2146,7 @@ async function searchLocalChats(query, options = {}) {
|
|
|
2060
2146
|
const raw = JSON.stringify(needle).slice(1, -1);
|
|
2061
2147
|
const limit = Math.max(1, Math.min(options.limit ?? 30, 100));
|
|
2062
2148
|
const deadline = Date.now() + (options.budgetMs ?? 3500);
|
|
2063
|
-
const names = await readCodexIndex(homes.
|
|
2149
|
+
const names = await readCodexIndex(homes.codex.map((h) => h.index));
|
|
2064
2150
|
const hits = [];
|
|
2065
2151
|
let i = cursor;
|
|
2066
2152
|
for (; i < files.length && hits.length < limit && Date.now() < deadline; i += 1) {
|
|
@@ -2095,6 +2181,65 @@ async function searchLocalChats(query, options = {}) {
|
|
|
2095
2181
|
};
|
|
2096
2182
|
}
|
|
2097
2183
|
|
|
2184
|
+
// src/sessions/hints.ts
|
|
2185
|
+
function titleHintKey(provider, sessionId) {
|
|
2186
|
+
return `${provider}:${sessionId}`;
|
|
2187
|
+
}
|
|
2188
|
+
function createTitleHintsStore() {
|
|
2189
|
+
let hints = null;
|
|
2190
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2191
|
+
return {
|
|
2192
|
+
current: () => hints,
|
|
2193
|
+
subscribe(listener) {
|
|
2194
|
+
listeners.add(listener);
|
|
2195
|
+
return () => listeners.delete(listener);
|
|
2196
|
+
},
|
|
2197
|
+
set(next) {
|
|
2198
|
+
hints = next;
|
|
2199
|
+
for (const l of listeners) l();
|
|
2200
|
+
}
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
// src/engine/title-hints.ts
|
|
2205
|
+
function overlayHint(chat, hints, expected) {
|
|
2206
|
+
if (!expected) return chat;
|
|
2207
|
+
if (hints === null) return { ...chat, titlePending: true };
|
|
2208
|
+
const hint = hints.get(titleHintKey(chat.provider, chat.providerSessionId));
|
|
2209
|
+
if (!hint) return chat;
|
|
2210
|
+
return {
|
|
2211
|
+
...chat,
|
|
2212
|
+
title: hint.title,
|
|
2213
|
+
titleSource: hint.source,
|
|
2214
|
+
...hint.archived || chat.archived ? { archived: true } : {},
|
|
2215
|
+
...hint.scheduled ? { scheduled: true } : {}
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
function unavailableFromHints(hints, covered) {
|
|
2219
|
+
if (!hints) return [];
|
|
2220
|
+
const out = [];
|
|
2221
|
+
for (const [key, hint] of hints) {
|
|
2222
|
+
if (!hint.unavailable || covered.has(key)) continue;
|
|
2223
|
+
const at = key.indexOf(":");
|
|
2224
|
+
const provider = key.slice(0, at);
|
|
2225
|
+
if (provider !== "claude" && provider !== "codex") continue;
|
|
2226
|
+
out.push({
|
|
2227
|
+
provider,
|
|
2228
|
+
providerSessionId: key.slice(at + 1),
|
|
2229
|
+
cwd: null,
|
|
2230
|
+
title: hint.title,
|
|
2231
|
+
titleSource: hint.source,
|
|
2232
|
+
updatedAt: new Date(hint.lastActiveMs ?? 0).toISOString(),
|
|
2233
|
+
source: null,
|
|
2234
|
+
activeElsewhere: null,
|
|
2235
|
+
unavailable: hint.unavailable,
|
|
2236
|
+
...hint.archived ? { archived: true } : {},
|
|
2237
|
+
...hint.scheduled ? { scheduled: true } : {}
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
2240
|
+
return out.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2098
2243
|
// src/engine/store.ts
|
|
2099
2244
|
var import_node_crypto4 = require("crypto");
|
|
2100
2245
|
var import_promises6 = __toESM(require("fs/promises"), 1);
|
|
@@ -2445,6 +2590,13 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2445
2590
|
}
|
|
2446
2591
|
}
|
|
2447
2592
|
}
|
|
2593
|
+
const titleHints = options.titleHints;
|
|
2594
|
+
const hintsNow = () => titleHints?.current() ?? null;
|
|
2595
|
+
const unsubscribeHints = titleHints?.subscribe(() => emit({ type: "titles" })) ?? null;
|
|
2596
|
+
const withHintTitle = (session) => {
|
|
2597
|
+
const hint = session.providerSessionId ? hintsNow()?.get(titleHintKey(session.provider, session.providerSessionId)) : void 0;
|
|
2598
|
+
return hint ? { ...session, title: hint.title } : session;
|
|
2599
|
+
};
|
|
2448
2600
|
function saveSession(session) {
|
|
2449
2601
|
index.putSession(session);
|
|
2450
2602
|
emit({ type: "session", session });
|
|
@@ -2601,7 +2753,12 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2601
2753
|
if (sid && sid !== knownProviderSession) {
|
|
2602
2754
|
knownProviderSession = sid;
|
|
2603
2755
|
const current = index.session(session.id);
|
|
2604
|
-
if (current)
|
|
2756
|
+
if (current) {
|
|
2757
|
+
saveSession({ ...current, providerSessionId: sid });
|
|
2758
|
+
if (preferences.rekey(chatKey(current.provider, null, current.id), chatKey(current.provider, sid))) {
|
|
2759
|
+
emit({ type: "preferences", preferences: preferences.all() });
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2605
2762
|
}
|
|
2606
2763
|
for (const item of items) emitItem(liveRun, item);
|
|
2607
2764
|
}
|
|
@@ -2909,7 +3066,9 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2909
3066
|
);
|
|
2910
3067
|
},
|
|
2911
3068
|
async discoverSessions(opts) {
|
|
2912
|
-
const
|
|
3069
|
+
const read = await discoverLocalSessions({ limit: opts?.limit, homes, excludePids: ourPids() });
|
|
3070
|
+
const hints = hintsNow();
|
|
3071
|
+
const found = read.map((chat) => overlayHint(chat, hints, titleHints !== void 0));
|
|
2913
3072
|
if (backups) {
|
|
2914
3073
|
for (const chat of found) {
|
|
2915
3074
|
const key = `${chat.provider}:${chat.providerSessionId}`;
|
|
@@ -2920,7 +3079,12 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2920
3079
|
}
|
|
2921
3080
|
}
|
|
2922
3081
|
}
|
|
2923
|
-
|
|
3082
|
+
const limit = opts?.limit ?? 60;
|
|
3083
|
+
const oldest = found.length >= limit ? found[found.length - 1]?.updatedAt ?? "" : "";
|
|
3084
|
+
const gone = unavailableFromHints(hints, new Set(found.map((c) => titleHintKey(c.provider, c.providerSessionId)))).filter(
|
|
3085
|
+
(c) => c.updatedAt >= oldest
|
|
3086
|
+
);
|
|
3087
|
+
return gone.length ? [...found, ...gone].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, limit) : found;
|
|
2924
3088
|
},
|
|
2925
3089
|
async recentFolders() {
|
|
2926
3090
|
const seen = /* @__PURE__ */ new Map();
|
|
@@ -2941,7 +3105,7 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2941
3105
|
);
|
|
2942
3106
|
},
|
|
2943
3107
|
async listSessions() {
|
|
2944
|
-
return index.sessions();
|
|
3108
|
+
return index.sessions().map(withHintTitle);
|
|
2945
3109
|
},
|
|
2946
3110
|
async listRuns(sessionId) {
|
|
2947
3111
|
const session = index.session(sessionId);
|
|
@@ -3116,7 +3280,9 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
3116
3280
|
return all;
|
|
3117
3281
|
},
|
|
3118
3282
|
async searchChats(query, opts) {
|
|
3119
|
-
|
|
3283
|
+
const result = await searchLocalChats(String(query ?? ""), { homes, cursor: opts?.cursor, limit: opts?.limit });
|
|
3284
|
+
const hints = hintsNow();
|
|
3285
|
+
return { ...result, hits: result.hits.map((hit) => ({ ...hit, title: hints?.get(titleHintKey(hit.provider, hit.providerSessionId))?.title ?? hit.title })) };
|
|
3120
3286
|
},
|
|
3121
3287
|
async openFolder(raw, target) {
|
|
3122
3288
|
const folder = await assertFolder(String(raw ?? ""));
|
|
@@ -3146,6 +3312,7 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
3146
3312
|
return agents.handleMcp(secret, body);
|
|
3147
3313
|
},
|
|
3148
3314
|
async dispose() {
|
|
3315
|
+
unsubscribeHints?.();
|
|
3149
3316
|
for (const liveRun of live.values()) {
|
|
3150
3317
|
liveRun.stopRequested = true;
|
|
3151
3318
|
if (liveRun.pgid !== null) killGroup(liveRun);
|
|
@@ -3202,9 +3369,11 @@ function openFolderIn(folder, target) {
|
|
|
3202
3369
|
buildCleanEnv,
|
|
3203
3370
|
claudeDefaults,
|
|
3204
3371
|
codexDefaults,
|
|
3372
|
+
codexHome,
|
|
3205
3373
|
createAiMatrxRawBackupSink,
|
|
3206
3374
|
createCodingSessionsEngine,
|
|
3207
3375
|
createRawBackupScheduler,
|
|
3376
|
+
createTitleHintsStore,
|
|
3208
3377
|
defaultProviderHomes,
|
|
3209
3378
|
describeActivity,
|
|
3210
3379
|
describeProviderStatus,
|
|
@@ -3217,6 +3386,7 @@ function openFolderIn(folder, target) {
|
|
|
3217
3386
|
readSessionFile,
|
|
3218
3387
|
searchLocalChats,
|
|
3219
3388
|
sessionActivity,
|
|
3220
|
-
snippetAround
|
|
3389
|
+
snippetAround,
|
|
3390
|
+
titleHintKey
|
|
3221
3391
|
});
|
|
3222
3392
|
//# sourceMappingURL=index.cjs.map
|