@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/dist/server/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as CodingSessionsMethod, a as CodingSessionsApi, b as CodingEvent, c as CodingSessionsEngine, d as CodingSessionsEngineOptions } from '../index-
|
|
2
|
-
export { e as CodingSessionsAgentsOptions } from '../index-
|
|
1
|
+
import { C as CodingSessionsMethod, a as CodingSessionsApi, b as CodingEvent, c as CodingSessionsEngine, d as CodingSessionsEngineOptions } from '../index-29t7n472.cjs';
|
|
2
|
+
export { e as CodingSessionsAgentsOptions } from '../index-29t7n472.cjs';
|
|
3
3
|
import '@ai-matrx/agents/portable';
|
|
4
4
|
import '@ai-matrx/data';
|
|
5
5
|
import '@ai-matrx/agents/portable/mcp';
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as CodingSessionsMethod, a as CodingSessionsApi, b as CodingEvent, c as CodingSessionsEngine, d as CodingSessionsEngineOptions } from '../index-
|
|
2
|
-
export { e as CodingSessionsAgentsOptions } from '../index-
|
|
1
|
+
import { C as CodingSessionsMethod, a as CodingSessionsApi, b as CodingEvent, c as CodingSessionsEngine, d as CodingSessionsEngineOptions } from '../index-29t7n472.js';
|
|
2
|
+
export { e as CodingSessionsAgentsOptions } from '../index-29t7n472.js';
|
|
3
3
|
import '@ai-matrx/agents/portable';
|
|
4
4
|
import '@ai-matrx/data';
|
|
5
5
|
import '@ai-matrx/agents/portable/mcp';
|
package/dist/server/index.js
CHANGED
|
@@ -1105,6 +1105,9 @@ var PROVIDER_LABEL = {
|
|
|
1105
1105
|
claude: "Claude Code",
|
|
1106
1106
|
codex: "Codex"
|
|
1107
1107
|
};
|
|
1108
|
+
function chatKey(provider, providerSessionId, sessionId) {
|
|
1109
|
+
return providerSessionId ? `${provider}:${providerSessionId}` : `session:${sessionId ?? ""}`;
|
|
1110
|
+
}
|
|
1108
1111
|
var TERMINAL_RUN_STATUSES = ["completed", "stopped", "failed"];
|
|
1109
1112
|
function isTerminalRunStatus(status) {
|
|
1110
1113
|
return TERMINAL_RUN_STATUSES.includes(status);
|
|
@@ -1419,6 +1422,78 @@ import fs4 from "fs/promises";
|
|
|
1419
1422
|
import os3 from "os";
|
|
1420
1423
|
import path5 from "path";
|
|
1421
1424
|
|
|
1425
|
+
// src/sessions/titles.ts
|
|
1426
|
+
var TITLE_MAX = 300;
|
|
1427
|
+
var BRIDGE_PLACEHOLDER = /^AI Matrx · [a-z-]+$/;
|
|
1428
|
+
var TITLE_PRECEDENCE = [
|
|
1429
|
+
/** The person's rename inside Matrx (Coding Sessions' preference). */
|
|
1430
|
+
"override",
|
|
1431
|
+
/** What the coding app's own sidebar shows: Claude's record `title` (renames included); Codex's thread name; Cursor's name. */
|
|
1432
|
+
"app",
|
|
1433
|
+
/** The title the app last showed, remembered by the claude-accounts engine after the record went away. */
|
|
1434
|
+
"app-remembered",
|
|
1435
|
+
/** Claude Code transcript entries (the last one of each kind that is not the bridge placeholder). */
|
|
1436
|
+
"custom-title",
|
|
1437
|
+
"ai-title",
|
|
1438
|
+
"agent-name",
|
|
1439
|
+
"summary",
|
|
1440
|
+
/** The person's first typed message (Codex: the thread's title / first user message). */
|
|
1441
|
+
"first-prompt",
|
|
1442
|
+
"last-prompt"
|
|
1443
|
+
];
|
|
1444
|
+
var STAMPABLE = /* @__PURE__ */ new Set(["custom-title", "ai-title", "agent-name"]);
|
|
1445
|
+
function isBridgePlaceholder(value) {
|
|
1446
|
+
return typeof value === "string" && BRIDGE_PLACEHOLDER.test(value.trim());
|
|
1447
|
+
}
|
|
1448
|
+
function cleanTitle(value) {
|
|
1449
|
+
if (typeof value !== "string") return null;
|
|
1450
|
+
const line = value.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0);
|
|
1451
|
+
if (!line) return null;
|
|
1452
|
+
return line.length > TITLE_MAX ? `${line.slice(0, TITLE_MAX - 1)}\u2026` : line;
|
|
1453
|
+
}
|
|
1454
|
+
function resolveSessionTitle(sources) {
|
|
1455
|
+
for (const source of TITLE_PRECEDENCE) {
|
|
1456
|
+
const raw = sources[source];
|
|
1457
|
+
if (STAMPABLE.has(source) && isBridgePlaceholder(raw)) continue;
|
|
1458
|
+
const title = cleanTitle(raw);
|
|
1459
|
+
if (title) return { title, source };
|
|
1460
|
+
}
|
|
1461
|
+
return null;
|
|
1462
|
+
}
|
|
1463
|
+
function claudeTranscriptTitleSources(head, tail = []) {
|
|
1464
|
+
const out = {};
|
|
1465
|
+
const take = (source, value) => {
|
|
1466
|
+
if (typeof value !== "string" || !value.trim()) return;
|
|
1467
|
+
if (STAMPABLE.has(source) && isBridgePlaceholder(value)) return;
|
|
1468
|
+
out[source] = value;
|
|
1469
|
+
};
|
|
1470
|
+
const normalizer = createClaudeNormalizer();
|
|
1471
|
+
let firstPrompt = null;
|
|
1472
|
+
const scan = (entries, findPrompt) => {
|
|
1473
|
+
for (const entry of entries) {
|
|
1474
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
1475
|
+
const e = entry;
|
|
1476
|
+
if (e.type === "custom-title") take("custom-title", e.customTitle);
|
|
1477
|
+
else if (e.type === "ai-title") take("ai-title", e.aiTitle);
|
|
1478
|
+
else if (e.type === "agent-name") take("agent-name", e.agentName);
|
|
1479
|
+
else if (e.type === "summary") take("summary", e.summary);
|
|
1480
|
+
else if (e.type === "last-prompt") take("last-prompt", e.lastPrompt);
|
|
1481
|
+
else if (findPrompt && firstPrompt === null) {
|
|
1482
|
+
for (const item of normalizer.push(entry)) {
|
|
1483
|
+
if (item.kind === "user" && item.text.trim()) {
|
|
1484
|
+
firstPrompt = item.text;
|
|
1485
|
+
break;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1491
|
+
scan(head, true);
|
|
1492
|
+
scan(tail, false);
|
|
1493
|
+
if (firstPrompt !== null) out["first-prompt"] = firstPrompt;
|
|
1494
|
+
return out;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1422
1497
|
// src/engine/activity.ts
|
|
1423
1498
|
import { execFile as execFile2 } from "child_process";
|
|
1424
1499
|
var RECENT_WRITE_MS = 2 * 60 * 1e3;
|
|
@@ -1484,11 +1559,17 @@ function describeActivity(input) {
|
|
|
1484
1559
|
}
|
|
1485
1560
|
|
|
1486
1561
|
// src/engine/files.ts
|
|
1562
|
+
function codexHome(dir) {
|
|
1563
|
+
return {
|
|
1564
|
+
sessions: path5.join(dir, "sessions"),
|
|
1565
|
+
archived: path5.join(dir, "archived_sessions"),
|
|
1566
|
+
index: path5.join(dir, "session_index.jsonl")
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1487
1569
|
function defaultProviderHomes(home = os3.homedir()) {
|
|
1488
1570
|
return {
|
|
1489
1571
|
claudeProjects: path5.join(home, ".claude", "projects"),
|
|
1490
|
-
|
|
1491
|
-
codexIndex: path5.join(home, ".codex", "session_index.jsonl")
|
|
1572
|
+
codex: [codexHome(path5.join(home, ".codex"))]
|
|
1492
1573
|
};
|
|
1493
1574
|
}
|
|
1494
1575
|
var UUIDISH = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
|
|
@@ -1522,7 +1603,7 @@ async function claudeCandidates(root) {
|
|
|
1522
1603
|
}
|
|
1523
1604
|
return out;
|
|
1524
1605
|
}
|
|
1525
|
-
async function
|
|
1606
|
+
async function codexTree(root) {
|
|
1526
1607
|
const out = [];
|
|
1527
1608
|
for (const year of await safeReaddir(root)) {
|
|
1528
1609
|
if (!year.isDirectory()) continue;
|
|
@@ -1530,17 +1611,23 @@ async function codexCandidates(root) {
|
|
|
1530
1611
|
if (!month.isDirectory()) continue;
|
|
1531
1612
|
for (const day of await safeReaddir(path5.join(root, year.name, month.name))) {
|
|
1532
1613
|
if (!day.isDirectory()) continue;
|
|
1533
|
-
|
|
1534
|
-
const entries = await safeReaddir(dir);
|
|
1535
|
-
const stats = await Promise.all(
|
|
1536
|
-
entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => statCandidate("codex", path5.join(dir, e.name)))
|
|
1537
|
-
);
|
|
1538
|
-
for (const s of stats) if (s) out.push(s);
|
|
1614
|
+
out.push(...await codexFlat(path5.join(root, year.name, month.name, day.name), false));
|
|
1539
1615
|
}
|
|
1540
1616
|
}
|
|
1541
1617
|
}
|
|
1542
1618
|
return out;
|
|
1543
1619
|
}
|
|
1620
|
+
async function codexFlat(dir, archived) {
|
|
1621
|
+
const entries = await safeReaddir(dir);
|
|
1622
|
+
const stats = await Promise.all(
|
|
1623
|
+
entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => statCandidate("codex", path5.join(dir, e.name)))
|
|
1624
|
+
);
|
|
1625
|
+
return stats.filter((s) => s !== null).map((s) => archived ? { ...s, archived: true } : s);
|
|
1626
|
+
}
|
|
1627
|
+
async function codexCandidates(homes) {
|
|
1628
|
+
const lists = await Promise.all(homes.flatMap((h) => [codexTree(h.sessions), codexFlat(h.archived, true)]));
|
|
1629
|
+
return lists.flat();
|
|
1630
|
+
}
|
|
1544
1631
|
async function readSlice(file, start, length) {
|
|
1545
1632
|
const handle = await fs4.open(file, "r");
|
|
1546
1633
|
try {
|
|
@@ -1562,56 +1649,38 @@ function completeLines(text, droppedFirst) {
|
|
|
1562
1649
|
}
|
|
1563
1650
|
var HEAD_BYTES = 256 * 1024;
|
|
1564
1651
|
var TAIL_BYTES = 128 * 1024;
|
|
1565
|
-
function oneLine(text, max = 140) {
|
|
1566
|
-
const flat = text.replace(/\s+/g, " ").trim();
|
|
1567
|
-
return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
|
|
1568
|
-
}
|
|
1569
1652
|
async function describeClaude(candidate) {
|
|
1570
1653
|
const head = parseJsonl(completeLines(await readSlice(candidate.file, 0, HEAD_BYTES), false));
|
|
1571
1654
|
const tailStart = Math.max(0, candidate.size - TAIL_BYTES);
|
|
1572
1655
|
const tail = tailStart > 0 ? parseJsonl(completeLines(await readSlice(candidate.file, tailStart, TAIL_BYTES), true)) : head;
|
|
1573
1656
|
let cwd = null;
|
|
1574
|
-
let firstPrompt = null;
|
|
1575
1657
|
let entrypoint = null;
|
|
1576
|
-
const normalizer = createClaudeNormalizer();
|
|
1577
1658
|
for (const entry of head) {
|
|
1578
1659
|
if (typeof entry !== "object" || entry === null) continue;
|
|
1579
1660
|
const e = entry;
|
|
1580
1661
|
if (!cwd && typeof e.cwd === "string") cwd = e.cwd;
|
|
1581
1662
|
if (!entrypoint && typeof e.entrypoint === "string") entrypoint = e.entrypoint;
|
|
1582
|
-
if (
|
|
1583
|
-
for (const item of normalizer.push(entry)) {
|
|
1584
|
-
if (item.kind === "user") {
|
|
1585
|
-
firstPrompt = item.text;
|
|
1586
|
-
break;
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
if (cwd && firstPrompt !== null) break;
|
|
1663
|
+
if (cwd && entrypoint) break;
|
|
1591
1664
|
}
|
|
1592
|
-
const
|
|
1593
|
-
|
|
1594
|
-
if (typeof entry !== "object" || entry === null) continue;
|
|
1595
|
-
const e = entry;
|
|
1596
|
-
if (e.type === "custom-title" && typeof e.customTitle === "string") titles.custom = e.customTitle;
|
|
1597
|
-
if (e.type === "ai-title" && typeof e.aiTitle === "string") titles.ai = e.aiTitle;
|
|
1598
|
-
if (e.type === "agent-name" && typeof e.agentName === "string") titles.agent = e.agentName;
|
|
1599
|
-
if (e.type === "summary" && typeof e.summary === "string") titles.summary = e.summary;
|
|
1600
|
-
if (e.type === "last-prompt" && typeof e.lastPrompt === "string") titles.last = e.lastPrompt;
|
|
1601
|
-
}
|
|
1602
|
-
const title = titles.custom ?? titles.ai ?? titles.agent ?? titles.summary ?? firstPrompt ?? titles.last;
|
|
1603
|
-
if (!title) return null;
|
|
1665
|
+
const resolved = resolveSessionTitle(claudeTranscriptTitleSources(head, tailStart > 0 ? tail : []));
|
|
1666
|
+
if (!resolved) return null;
|
|
1604
1667
|
return {
|
|
1605
1668
|
provider: "claude",
|
|
1606
1669
|
providerSessionId: candidate.id,
|
|
1607
1670
|
cwd,
|
|
1608
|
-
title:
|
|
1671
|
+
title: resolved.title,
|
|
1672
|
+
titleSource: resolved.source,
|
|
1609
1673
|
updatedAt: new Date(candidate.mtimeMs).toISOString(),
|
|
1610
1674
|
source: entrypoint,
|
|
1611
1675
|
activeElsewhere: null
|
|
1612
1676
|
};
|
|
1613
1677
|
}
|
|
1614
|
-
async function readCodexIndex(
|
|
1678
|
+
async function readCodexIndex(files) {
|
|
1679
|
+
const names = /* @__PURE__ */ new Map();
|
|
1680
|
+
for (const file of files) for (const [id, name] of await readCodexIndexFile(file)) if (!names.has(id)) names.set(id, name);
|
|
1681
|
+
return names;
|
|
1682
|
+
}
|
|
1683
|
+
async function readCodexIndexFile(file) {
|
|
1615
1684
|
const names = /* @__PURE__ */ new Map();
|
|
1616
1685
|
let text;
|
|
1617
1686
|
try {
|
|
@@ -1657,13 +1726,15 @@ async function describeCodex(candidate, names) {
|
|
|
1657
1726
|
}
|
|
1658
1727
|
}
|
|
1659
1728
|
if (isSubAgent) return null;
|
|
1660
|
-
const
|
|
1661
|
-
if (!
|
|
1729
|
+
const resolved = resolveSessionTitle({ app: names.get(candidate.id), "first-prompt": firstPrompt });
|
|
1730
|
+
if (!resolved) return null;
|
|
1662
1731
|
return {
|
|
1663
1732
|
provider: "codex",
|
|
1664
1733
|
providerSessionId: candidate.id,
|
|
1665
1734
|
cwd,
|
|
1666
|
-
title:
|
|
1735
|
+
title: resolved.title,
|
|
1736
|
+
titleSource: resolved.source,
|
|
1737
|
+
...candidate.archived ? { archived: true } : {},
|
|
1667
1738
|
updatedAt: new Date(candidate.mtimeMs).toISOString(),
|
|
1668
1739
|
source,
|
|
1669
1740
|
activeElsewhere: null
|
|
@@ -1708,8 +1779,8 @@ async function discoverLocalSessions(options = {}) {
|
|
|
1708
1779
|
const homes = options.homes ?? defaultProviderHomes();
|
|
1709
1780
|
const [claude, codex, names] = await Promise.all([
|
|
1710
1781
|
claudeCandidates(homes.claudeProjects),
|
|
1711
|
-
codexCandidates(homes.
|
|
1712
|
-
readCodexIndex(homes.
|
|
1782
|
+
codexCandidates(homes.codex),
|
|
1783
|
+
readCodexIndex(homes.codex.map((h) => h.index))
|
|
1713
1784
|
]);
|
|
1714
1785
|
const candidates = [...claude, ...codex].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1715
1786
|
const processes = await listProcesses();
|
|
@@ -1729,7 +1800,7 @@ async function discoverLocalSessions(options = {}) {
|
|
|
1729
1800
|
}
|
|
1730
1801
|
async function findSessionFile(provider, providerSessionId, homes = defaultProviderHomes()) {
|
|
1731
1802
|
if (!/^[0-9a-f-]{36}$/i.test(providerSessionId)) return null;
|
|
1732
|
-
const candidates = provider === "claude" ? await claudeCandidates(homes.claudeProjects) : await codexCandidates(homes.
|
|
1803
|
+
const candidates = provider === "claude" ? await claudeCandidates(homes.claudeProjects) : await codexCandidates(homes.codex);
|
|
1733
1804
|
const hits = candidates.filter((c) => c.id === providerSessionId).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1734
1805
|
return hits[0]?.file ?? null;
|
|
1735
1806
|
}
|
|
@@ -1781,7 +1852,7 @@ async function readSessionFile(provider, file, endOffset, maxBytes = HISTORY_BUD
|
|
|
1781
1852
|
async function listSessionFiles(homes = defaultProviderHomes()) {
|
|
1782
1853
|
const [claude, codex] = await Promise.all([
|
|
1783
1854
|
claudeCandidates(homes.claudeProjects),
|
|
1784
|
-
codexCandidates(homes.
|
|
1855
|
+
codexCandidates(homes.codex)
|
|
1785
1856
|
]);
|
|
1786
1857
|
return [...claude, ...codex].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1787
1858
|
}
|
|
@@ -1824,7 +1895,7 @@ var PreferenceStore = class {
|
|
|
1824
1895
|
if (!key || key.length > 200) throw new Error("That chat can't be found.");
|
|
1825
1896
|
const next = { ...this.data[key] };
|
|
1826
1897
|
if ("title" in patch) {
|
|
1827
|
-
const title = typeof patch.title === "string" ? patch.title.replace(/\s+/g, " ").trim().slice(0,
|
|
1898
|
+
const title = typeof patch.title === "string" ? patch.title.replace(/\s+/g, " ").trim().slice(0, TITLE_MAX) : "";
|
|
1828
1899
|
if (title) next.title = title;
|
|
1829
1900
|
else delete next.title;
|
|
1830
1901
|
}
|
|
@@ -1842,6 +1913,18 @@ var PreferenceStore = class {
|
|
|
1842
1913
|
this.persist();
|
|
1843
1914
|
return this.all();
|
|
1844
1915
|
}
|
|
1916
|
+
/**
|
|
1917
|
+
* Move a preference to the chat's lasting key (`session:<id>` → `<provider>:<id>` once the
|
|
1918
|
+
* provider's id is known), so a rename made early is the one rename everywhere. A preference
|
|
1919
|
+
* already under the lasting key wins field by field.
|
|
1920
|
+
*/
|
|
1921
|
+
rekey(from, to) {
|
|
1922
|
+
if (from === to || !this.data[from]) return false;
|
|
1923
|
+
this.data[to] = { ...this.data[from], ...this.data[to] };
|
|
1924
|
+
delete this.data[from];
|
|
1925
|
+
this.persist();
|
|
1926
|
+
return true;
|
|
1927
|
+
}
|
|
1845
1928
|
persist() {
|
|
1846
1929
|
const snapshot = JSON.stringify(this.data, null, 1);
|
|
1847
1930
|
this.writing = this.writing.then(async () => {
|
|
@@ -1969,7 +2052,7 @@ async function searchLocalChats(query, options = {}) {
|
|
|
1969
2052
|
const raw = JSON.stringify(needle).slice(1, -1);
|
|
1970
2053
|
const limit = Math.max(1, Math.min(options.limit ?? 30, 100));
|
|
1971
2054
|
const deadline = Date.now() + (options.budgetMs ?? 3500);
|
|
1972
|
-
const names = await readCodexIndex(homes.
|
|
2055
|
+
const names = await readCodexIndex(homes.codex.map((h) => h.index));
|
|
1973
2056
|
const hits = [];
|
|
1974
2057
|
let i = cursor;
|
|
1975
2058
|
for (; i < files.length && hits.length < limit && Date.now() < deadline; i += 1) {
|
|
@@ -2004,6 +2087,50 @@ async function searchLocalChats(query, options = {}) {
|
|
|
2004
2087
|
};
|
|
2005
2088
|
}
|
|
2006
2089
|
|
|
2090
|
+
// src/sessions/hints.ts
|
|
2091
|
+
function titleHintKey(provider, sessionId) {
|
|
2092
|
+
return `${provider}:${sessionId}`;
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
// src/engine/title-hints.ts
|
|
2096
|
+
function overlayHint(chat, hints, expected) {
|
|
2097
|
+
if (!expected) return chat;
|
|
2098
|
+
if (hints === null) return { ...chat, titlePending: true };
|
|
2099
|
+
const hint = hints.get(titleHintKey(chat.provider, chat.providerSessionId));
|
|
2100
|
+
if (!hint) return chat;
|
|
2101
|
+
return {
|
|
2102
|
+
...chat,
|
|
2103
|
+
title: hint.title,
|
|
2104
|
+
titleSource: hint.source,
|
|
2105
|
+
...hint.archived || chat.archived ? { archived: true } : {},
|
|
2106
|
+
...hint.scheduled ? { scheduled: true } : {}
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
function unavailableFromHints(hints, covered) {
|
|
2110
|
+
if (!hints) return [];
|
|
2111
|
+
const out = [];
|
|
2112
|
+
for (const [key, hint] of hints) {
|
|
2113
|
+
if (!hint.unavailable || covered.has(key)) continue;
|
|
2114
|
+
const at = key.indexOf(":");
|
|
2115
|
+
const provider = key.slice(0, at);
|
|
2116
|
+
if (provider !== "claude" && provider !== "codex") continue;
|
|
2117
|
+
out.push({
|
|
2118
|
+
provider,
|
|
2119
|
+
providerSessionId: key.slice(at + 1),
|
|
2120
|
+
cwd: null,
|
|
2121
|
+
title: hint.title,
|
|
2122
|
+
titleSource: hint.source,
|
|
2123
|
+
updatedAt: new Date(hint.lastActiveMs ?? 0).toISOString(),
|
|
2124
|
+
source: null,
|
|
2125
|
+
activeElsewhere: null,
|
|
2126
|
+
unavailable: hint.unavailable,
|
|
2127
|
+
...hint.archived ? { archived: true } : {},
|
|
2128
|
+
...hint.scheduled ? { scheduled: true } : {}
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
return out.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2007
2134
|
// src/engine/store.ts
|
|
2008
2135
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
2009
2136
|
import fs6 from "fs/promises";
|
|
@@ -2354,6 +2481,13 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2354
2481
|
}
|
|
2355
2482
|
}
|
|
2356
2483
|
}
|
|
2484
|
+
const titleHints = options.titleHints;
|
|
2485
|
+
const hintsNow = () => titleHints?.current() ?? null;
|
|
2486
|
+
const unsubscribeHints = titleHints?.subscribe(() => emit({ type: "titles" })) ?? null;
|
|
2487
|
+
const withHintTitle = (session) => {
|
|
2488
|
+
const hint = session.providerSessionId ? hintsNow()?.get(titleHintKey(session.provider, session.providerSessionId)) : void 0;
|
|
2489
|
+
return hint ? { ...session, title: hint.title } : session;
|
|
2490
|
+
};
|
|
2357
2491
|
function saveSession(session) {
|
|
2358
2492
|
index.putSession(session);
|
|
2359
2493
|
emit({ type: "session", session });
|
|
@@ -2510,7 +2644,12 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2510
2644
|
if (sid && sid !== knownProviderSession) {
|
|
2511
2645
|
knownProviderSession = sid;
|
|
2512
2646
|
const current = index.session(session.id);
|
|
2513
|
-
if (current)
|
|
2647
|
+
if (current) {
|
|
2648
|
+
saveSession({ ...current, providerSessionId: sid });
|
|
2649
|
+
if (preferences.rekey(chatKey(current.provider, null, current.id), chatKey(current.provider, sid))) {
|
|
2650
|
+
emit({ type: "preferences", preferences: preferences.all() });
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2514
2653
|
}
|
|
2515
2654
|
for (const item of items) emitItem(liveRun, item);
|
|
2516
2655
|
}
|
|
@@ -2818,7 +2957,9 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2818
2957
|
);
|
|
2819
2958
|
},
|
|
2820
2959
|
async discoverSessions(opts) {
|
|
2821
|
-
const
|
|
2960
|
+
const read = await discoverLocalSessions({ limit: opts?.limit, homes, excludePids: ourPids() });
|
|
2961
|
+
const hints = hintsNow();
|
|
2962
|
+
const found = read.map((chat) => overlayHint(chat, hints, titleHints !== void 0));
|
|
2822
2963
|
if (backups) {
|
|
2823
2964
|
for (const chat of found) {
|
|
2824
2965
|
const key = `${chat.provider}:${chat.providerSessionId}`;
|
|
@@ -2829,7 +2970,12 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2829
2970
|
}
|
|
2830
2971
|
}
|
|
2831
2972
|
}
|
|
2832
|
-
|
|
2973
|
+
const limit = opts?.limit ?? 60;
|
|
2974
|
+
const oldest = found.length >= limit ? found[found.length - 1]?.updatedAt ?? "" : "";
|
|
2975
|
+
const gone = unavailableFromHints(hints, new Set(found.map((c) => titleHintKey(c.provider, c.providerSessionId)))).filter(
|
|
2976
|
+
(c) => c.updatedAt >= oldest
|
|
2977
|
+
);
|
|
2978
|
+
return gone.length ? [...found, ...gone].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, limit) : found;
|
|
2833
2979
|
},
|
|
2834
2980
|
async recentFolders() {
|
|
2835
2981
|
const seen = /* @__PURE__ */ new Map();
|
|
@@ -2850,7 +2996,7 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2850
2996
|
);
|
|
2851
2997
|
},
|
|
2852
2998
|
async listSessions() {
|
|
2853
|
-
return index.sessions();
|
|
2999
|
+
return index.sessions().map(withHintTitle);
|
|
2854
3000
|
},
|
|
2855
3001
|
async listRuns(sessionId) {
|
|
2856
3002
|
const session = index.session(sessionId);
|
|
@@ -3025,7 +3171,9 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
3025
3171
|
return all;
|
|
3026
3172
|
},
|
|
3027
3173
|
async searchChats(query, opts) {
|
|
3028
|
-
|
|
3174
|
+
const result = await searchLocalChats(String(query ?? ""), { homes, cursor: opts?.cursor, limit: opts?.limit });
|
|
3175
|
+
const hints = hintsNow();
|
|
3176
|
+
return { ...result, hits: result.hits.map((hit) => ({ ...hit, title: hints?.get(titleHintKey(hit.provider, hit.providerSessionId))?.title ?? hit.title })) };
|
|
3029
3177
|
},
|
|
3030
3178
|
async openFolder(raw, target) {
|
|
3031
3179
|
const folder = await assertFolder(String(raw ?? ""));
|
|
@@ -3055,6 +3203,7 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
3055
3203
|
return agents.handleMcp(secret, body);
|
|
3056
3204
|
},
|
|
3057
3205
|
async dispose() {
|
|
3206
|
+
unsubscribeHints?.();
|
|
3058
3207
|
for (const liveRun of live.values()) {
|
|
3059
3208
|
liveRun.stopRequested = true;
|
|
3060
3209
|
if (liveRun.pgid !== null) killGroup(liveRun);
|