@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.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-Bbe11HoB.cjs';
|
|
2
|
+
export { e as CodingSessionsAgentsOptions } from '../index-Bbe11HoB.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-Bbe11HoB.js';
|
|
2
|
+
export { e as CodingSessionsAgentsOptions } from '../index-Bbe11HoB.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,18 @@ 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
|
-
|
|
1572
|
+
claudeVault: path5.join(home, ".claude", "claude-app-vault"),
|
|
1573
|
+
codex: [codexHome(path5.join(home, ".codex"))]
|
|
1492
1574
|
};
|
|
1493
1575
|
}
|
|
1494
1576
|
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 +1604,7 @@ async function claudeCandidates(root) {
|
|
|
1522
1604
|
}
|
|
1523
1605
|
return out;
|
|
1524
1606
|
}
|
|
1525
|
-
async function
|
|
1607
|
+
async function codexTree(root) {
|
|
1526
1608
|
const out = [];
|
|
1527
1609
|
for (const year of await safeReaddir(root)) {
|
|
1528
1610
|
if (!year.isDirectory()) continue;
|
|
@@ -1530,17 +1612,23 @@ async function codexCandidates(root) {
|
|
|
1530
1612
|
if (!month.isDirectory()) continue;
|
|
1531
1613
|
for (const day of await safeReaddir(path5.join(root, year.name, month.name))) {
|
|
1532
1614
|
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);
|
|
1615
|
+
out.push(...await codexFlat(path5.join(root, year.name, month.name, day.name), false));
|
|
1539
1616
|
}
|
|
1540
1617
|
}
|
|
1541
1618
|
}
|
|
1542
1619
|
return out;
|
|
1543
1620
|
}
|
|
1621
|
+
async function codexFlat(dir, archived) {
|
|
1622
|
+
const entries = await safeReaddir(dir);
|
|
1623
|
+
const stats = await Promise.all(
|
|
1624
|
+
entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => statCandidate("codex", path5.join(dir, e.name)))
|
|
1625
|
+
);
|
|
1626
|
+
return stats.filter((s) => s !== null).map((s) => archived ? { ...s, archived: true } : s);
|
|
1627
|
+
}
|
|
1628
|
+
async function codexCandidates(homes) {
|
|
1629
|
+
const lists = await Promise.all(homes.flatMap((h) => [codexTree(h.sessions), codexFlat(h.archived, true)]));
|
|
1630
|
+
return lists.flat();
|
|
1631
|
+
}
|
|
1544
1632
|
async function readSlice(file, start, length) {
|
|
1545
1633
|
const handle = await fs4.open(file, "r");
|
|
1546
1634
|
try {
|
|
@@ -1562,56 +1650,38 @@ function completeLines(text, droppedFirst) {
|
|
|
1562
1650
|
}
|
|
1563
1651
|
var HEAD_BYTES = 256 * 1024;
|
|
1564
1652
|
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
1653
|
async function describeClaude(candidate) {
|
|
1570
1654
|
const head = parseJsonl(completeLines(await readSlice(candidate.file, 0, HEAD_BYTES), false));
|
|
1571
1655
|
const tailStart = Math.max(0, candidate.size - TAIL_BYTES);
|
|
1572
1656
|
const tail = tailStart > 0 ? parseJsonl(completeLines(await readSlice(candidate.file, tailStart, TAIL_BYTES), true)) : head;
|
|
1573
1657
|
let cwd = null;
|
|
1574
|
-
let firstPrompt = null;
|
|
1575
1658
|
let entrypoint = null;
|
|
1576
|
-
const normalizer = createClaudeNormalizer();
|
|
1577
1659
|
for (const entry of head) {
|
|
1578
1660
|
if (typeof entry !== "object" || entry === null) continue;
|
|
1579
1661
|
const e = entry;
|
|
1580
1662
|
if (!cwd && typeof e.cwd === "string") cwd = e.cwd;
|
|
1581
1663
|
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;
|
|
1664
|
+
if (cwd && entrypoint) break;
|
|
1591
1665
|
}
|
|
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;
|
|
1666
|
+
const resolved = resolveSessionTitle(claudeTranscriptTitleSources(head, tailStart > 0 ? tail : []));
|
|
1667
|
+
if (!resolved) return null;
|
|
1604
1668
|
return {
|
|
1605
1669
|
provider: "claude",
|
|
1606
1670
|
providerSessionId: candidate.id,
|
|
1607
1671
|
cwd,
|
|
1608
|
-
title:
|
|
1672
|
+
title: resolved.title,
|
|
1673
|
+
titleSource: resolved.source,
|
|
1609
1674
|
updatedAt: new Date(candidate.mtimeMs).toISOString(),
|
|
1610
1675
|
source: entrypoint,
|
|
1611
1676
|
activeElsewhere: null
|
|
1612
1677
|
};
|
|
1613
1678
|
}
|
|
1614
|
-
async function readCodexIndex(
|
|
1679
|
+
async function readCodexIndex(files) {
|
|
1680
|
+
const names = /* @__PURE__ */ new Map();
|
|
1681
|
+
for (const file of files) for (const [id, name] of await readCodexIndexFile(file)) if (!names.has(id)) names.set(id, name);
|
|
1682
|
+
return names;
|
|
1683
|
+
}
|
|
1684
|
+
async function readCodexIndexFile(file) {
|
|
1615
1685
|
const names = /* @__PURE__ */ new Map();
|
|
1616
1686
|
let text;
|
|
1617
1687
|
try {
|
|
@@ -1657,18 +1727,26 @@ async function describeCodex(candidate, names) {
|
|
|
1657
1727
|
}
|
|
1658
1728
|
}
|
|
1659
1729
|
if (isSubAgent) return null;
|
|
1660
|
-
const
|
|
1661
|
-
if (!
|
|
1730
|
+
const resolved = resolveSessionTitle({ app: names.get(candidate.id), "first-prompt": firstPrompt });
|
|
1731
|
+
if (!resolved) return null;
|
|
1662
1732
|
return {
|
|
1663
1733
|
provider: "codex",
|
|
1664
1734
|
providerSessionId: candidate.id,
|
|
1665
1735
|
cwd,
|
|
1666
|
-
title:
|
|
1736
|
+
title: resolved.title,
|
|
1737
|
+
titleSource: resolved.source,
|
|
1738
|
+
...candidate.archived ? { archived: true } : {},
|
|
1667
1739
|
updatedAt: new Date(candidate.mtimeMs).toISOString(),
|
|
1668
1740
|
source,
|
|
1669
1741
|
activeElsewhere: null
|
|
1670
1742
|
};
|
|
1671
1743
|
}
|
|
1744
|
+
async function codexNames(homes, appTitles) {
|
|
1745
|
+
if (!appTitles) return readCodexIndex(homes.codex.map((h) => h.index));
|
|
1746
|
+
const out = /* @__PURE__ */ new Map();
|
|
1747
|
+
for (const [key, title] of appTitles) if (key.startsWith("codex:")) out.set(key.slice(6), title);
|
|
1748
|
+
return out;
|
|
1749
|
+
}
|
|
1672
1750
|
async function readTail(file, size) {
|
|
1673
1751
|
const start = Math.max(0, size - TAIL_BYTES);
|
|
1674
1752
|
return parseJsonl(completeLines(await readSlice(file, start, Math.min(size, TAIL_BYTES)), start > 0));
|
|
@@ -1708,8 +1786,8 @@ async function discoverLocalSessions(options = {}) {
|
|
|
1708
1786
|
const homes = options.homes ?? defaultProviderHomes();
|
|
1709
1787
|
const [claude, codex, names] = await Promise.all([
|
|
1710
1788
|
claudeCandidates(homes.claudeProjects),
|
|
1711
|
-
codexCandidates(homes.
|
|
1712
|
-
|
|
1789
|
+
codexCandidates(homes.codex),
|
|
1790
|
+
codexNames(homes, options.appTitles)
|
|
1713
1791
|
]);
|
|
1714
1792
|
const candidates = [...claude, ...codex].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1715
1793
|
const processes = await listProcesses();
|
|
@@ -1729,9 +1807,14 @@ async function discoverLocalSessions(options = {}) {
|
|
|
1729
1807
|
}
|
|
1730
1808
|
async function findSessionFile(provider, providerSessionId, homes = defaultProviderHomes()) {
|
|
1731
1809
|
if (!/^[0-9a-f-]{36}$/i.test(providerSessionId)) return null;
|
|
1732
|
-
const candidates = provider === "claude" ? await claudeCandidates(homes.claudeProjects) : await codexCandidates(homes.
|
|
1810
|
+
const candidates = provider === "claude" ? await claudeCandidates(homes.claudeProjects) : await codexCandidates(homes.codex);
|
|
1733
1811
|
const hits = candidates.filter((c) => c.id === providerSessionId).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1734
|
-
return hits[0]
|
|
1812
|
+
if (hits[0]) return hits[0].file;
|
|
1813
|
+
if (provider === "claude" && homes.claudeVault) {
|
|
1814
|
+
const copy = path5.join(homes.claudeVault, providerSessionId, "transcript.jsonl");
|
|
1815
|
+
if ((await fs4.stat(copy).catch(() => null))?.isFile()) return copy;
|
|
1816
|
+
}
|
|
1817
|
+
return null;
|
|
1735
1818
|
}
|
|
1736
1819
|
var HISTORY_BUDGET = 24 * 1024 * 1024;
|
|
1737
1820
|
var MAX_HISTORY_BUDGET = 256 * 1024 * 1024;
|
|
@@ -1781,12 +1864,12 @@ async function readSessionFile(provider, file, endOffset, maxBytes = HISTORY_BUD
|
|
|
1781
1864
|
async function listSessionFiles(homes = defaultProviderHomes()) {
|
|
1782
1865
|
const [claude, codex] = await Promise.all([
|
|
1783
1866
|
claudeCandidates(homes.claudeProjects),
|
|
1784
|
-
codexCandidates(homes.
|
|
1867
|
+
codexCandidates(homes.codex)
|
|
1785
1868
|
]);
|
|
1786
1869
|
return [...claude, ...codex].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1787
1870
|
}
|
|
1788
|
-
async function describeSessionFile(candidate,
|
|
1789
|
-
return (candidate.provider === "claude" ? describeClaude(candidate) : describeCodex(candidate,
|
|
1871
|
+
async function describeSessionFile(candidate, codexNames2) {
|
|
1872
|
+
return (candidate.provider === "claude" ? describeClaude(candidate) : describeCodex(candidate, codexNames2)).catch(
|
|
1790
1873
|
() => null
|
|
1791
1874
|
);
|
|
1792
1875
|
}
|
|
@@ -1824,7 +1907,7 @@ var PreferenceStore = class {
|
|
|
1824
1907
|
if (!key || key.length > 200) throw new Error("That chat can't be found.");
|
|
1825
1908
|
const next = { ...this.data[key] };
|
|
1826
1909
|
if ("title" in patch) {
|
|
1827
|
-
const title = typeof patch.title === "string" ? patch.title.replace(/\s+/g, " ").trim().slice(0,
|
|
1910
|
+
const title = typeof patch.title === "string" ? patch.title.replace(/\s+/g, " ").trim().slice(0, TITLE_MAX) : "";
|
|
1828
1911
|
if (title) next.title = title;
|
|
1829
1912
|
else delete next.title;
|
|
1830
1913
|
}
|
|
@@ -1842,6 +1925,18 @@ var PreferenceStore = class {
|
|
|
1842
1925
|
this.persist();
|
|
1843
1926
|
return this.all();
|
|
1844
1927
|
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Move a preference to the chat's lasting key (`session:<id>` → `<provider>:<id>` once the
|
|
1930
|
+
* provider's id is known), so a rename made early is the one rename everywhere. A preference
|
|
1931
|
+
* already under the lasting key wins field by field.
|
|
1932
|
+
*/
|
|
1933
|
+
rekey(from, to) {
|
|
1934
|
+
if (from === to || !this.data[from]) return false;
|
|
1935
|
+
this.data[to] = { ...this.data[from], ...this.data[to] };
|
|
1936
|
+
delete this.data[from];
|
|
1937
|
+
this.persist();
|
|
1938
|
+
return true;
|
|
1939
|
+
}
|
|
1845
1940
|
persist() {
|
|
1846
1941
|
const snapshot = JSON.stringify(this.data, null, 1);
|
|
1847
1942
|
this.writing = this.writing.then(async () => {
|
|
@@ -1969,12 +2064,12 @@ async function searchLocalChats(query, options = {}) {
|
|
|
1969
2064
|
const raw = JSON.stringify(needle).slice(1, -1);
|
|
1970
2065
|
const limit = Math.max(1, Math.min(options.limit ?? 30, 100));
|
|
1971
2066
|
const deadline = Date.now() + (options.budgetMs ?? 3500);
|
|
1972
|
-
const names = await
|
|
2067
|
+
const names = await codexNames(homes, options.appTitles);
|
|
1973
2068
|
const hits = [];
|
|
1974
2069
|
let i = cursor;
|
|
1975
2070
|
for (; i < files.length && hits.length < limit && Date.now() < deadline; i += 1) {
|
|
1976
2071
|
const candidate = files[i];
|
|
1977
|
-
const named = candidate.provider === "codex" ? names.get(candidate.id) : void 0;
|
|
2072
|
+
const named = options.appTitles?.get(`${candidate.provider}:${candidate.id}`) ?? (candidate.provider === "codex" ? names.get(candidate.id) : void 0);
|
|
1978
2073
|
const nameHit = named !== void 0 && named.toLowerCase().includes(needle);
|
|
1979
2074
|
const scanned = nameHit ? null : await scanFile(candidate.provider, candidate.file, needle, raw, deadline);
|
|
1980
2075
|
if (scanned === TIMED_OUT) {
|
|
@@ -2004,6 +2099,55 @@ async function searchLocalChats(query, options = {}) {
|
|
|
2004
2099
|
};
|
|
2005
2100
|
}
|
|
2006
2101
|
|
|
2102
|
+
// src/sessions/hints.ts
|
|
2103
|
+
function titleHintKey(provider, sessionId) {
|
|
2104
|
+
return `${provider}:${sessionId}`;
|
|
2105
|
+
}
|
|
2106
|
+
function appTitlesOf(hints) {
|
|
2107
|
+
const out = /* @__PURE__ */ new Map();
|
|
2108
|
+
for (const [key, hint] of hints) out.set(key, hint.title);
|
|
2109
|
+
return out;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// src/engine/title-hints.ts
|
|
2113
|
+
function overlayHint(chat, hints, expected) {
|
|
2114
|
+
if (!expected) return chat;
|
|
2115
|
+
if (hints === null) return { ...chat, titlePending: true };
|
|
2116
|
+
const hint = hints.get(titleHintKey(chat.provider, chat.providerSessionId));
|
|
2117
|
+
if (!hint) return chat;
|
|
2118
|
+
return {
|
|
2119
|
+
...chat,
|
|
2120
|
+
title: hint.title,
|
|
2121
|
+
titleSource: hint.source,
|
|
2122
|
+
...hint.archived || chat.archived ? { archived: true } : {},
|
|
2123
|
+
...hint.scheduled ? { scheduled: true } : {}
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
function unavailableFromHints(hints, covered) {
|
|
2127
|
+
if (!hints) return [];
|
|
2128
|
+
const out = [];
|
|
2129
|
+
for (const [key, hint] of hints) {
|
|
2130
|
+
if (!hint.unavailable || covered.has(key)) continue;
|
|
2131
|
+
const at = key.indexOf(":");
|
|
2132
|
+
const provider = key.slice(0, at);
|
|
2133
|
+
if (provider !== "claude" && provider !== "codex") continue;
|
|
2134
|
+
out.push({
|
|
2135
|
+
provider,
|
|
2136
|
+
providerSessionId: key.slice(at + 1),
|
|
2137
|
+
cwd: null,
|
|
2138
|
+
title: hint.title,
|
|
2139
|
+
titleSource: hint.source,
|
|
2140
|
+
updatedAt: new Date(hint.lastActiveMs ?? 0).toISOString(),
|
|
2141
|
+
source: null,
|
|
2142
|
+
activeElsewhere: null,
|
|
2143
|
+
unavailable: hint.unavailable,
|
|
2144
|
+
...hint.archived ? { archived: true } : {},
|
|
2145
|
+
...hint.scheduled ? { scheduled: true } : {}
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
return out.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2007
2151
|
// src/engine/store.ts
|
|
2008
2152
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
2009
2153
|
import fs6 from "fs/promises";
|
|
@@ -2274,7 +2418,7 @@ function describeAuthStatus(provider, probe) {
|
|
|
2274
2418
|
async function createCodingSessionsEngine(options = {}) {
|
|
2275
2419
|
const dataDir = options.dataDir ?? path9.join(os5.homedir(), ".matrx", "coding-sessions");
|
|
2276
2420
|
const stateDir = path9.join(dataDir, "paperclip-state");
|
|
2277
|
-
|
|
2421
|
+
let homes = options.homes ?? defaultProviderHomes();
|
|
2278
2422
|
const index = new RunIndex(dataDir);
|
|
2279
2423
|
await index.load();
|
|
2280
2424
|
const preferences = new PreferenceStore(dataDir);
|
|
@@ -2354,6 +2498,18 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2354
2498
|
}
|
|
2355
2499
|
}
|
|
2356
2500
|
}
|
|
2501
|
+
const titleHints = options.titleHints;
|
|
2502
|
+
const hintsNow = () => titleHints?.current() ?? null;
|
|
2503
|
+
const appTitles = () => {
|
|
2504
|
+
if (!titleHints) return void 0;
|
|
2505
|
+
const hints = hintsNow();
|
|
2506
|
+
return hints ? appTitlesOf(hints) : /* @__PURE__ */ new Map();
|
|
2507
|
+
};
|
|
2508
|
+
const unsubscribeHints = titleHints?.subscribe(() => emit({ type: "titles" })) ?? null;
|
|
2509
|
+
const withHintTitle = (session) => {
|
|
2510
|
+
const hint = session.providerSessionId ? hintsNow()?.get(titleHintKey(session.provider, session.providerSessionId)) : void 0;
|
|
2511
|
+
return hint ? { ...session, title: hint.title } : session;
|
|
2512
|
+
};
|
|
2357
2513
|
function saveSession(session) {
|
|
2358
2514
|
index.putSession(session);
|
|
2359
2515
|
emit({ type: "session", session });
|
|
@@ -2510,7 +2666,12 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2510
2666
|
if (sid && sid !== knownProviderSession) {
|
|
2511
2667
|
knownProviderSession = sid;
|
|
2512
2668
|
const current = index.session(session.id);
|
|
2513
|
-
if (current)
|
|
2669
|
+
if (current) {
|
|
2670
|
+
saveSession({ ...current, providerSessionId: sid });
|
|
2671
|
+
if (preferences.rekey(chatKey(current.provider, null, current.id), chatKey(current.provider, sid))) {
|
|
2672
|
+
emit({ type: "preferences", preferences: preferences.all() });
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2514
2675
|
}
|
|
2515
2676
|
for (const item of items) emitItem(liveRun, item);
|
|
2516
2677
|
}
|
|
@@ -2818,7 +2979,9 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2818
2979
|
);
|
|
2819
2980
|
},
|
|
2820
2981
|
async discoverSessions(opts) {
|
|
2821
|
-
const
|
|
2982
|
+
const read = await discoverLocalSessions({ limit: opts?.limit, homes, excludePids: ourPids(), appTitles: appTitles() });
|
|
2983
|
+
const hints = hintsNow();
|
|
2984
|
+
const found = read.map((chat) => overlayHint(chat, hints, titleHints !== void 0));
|
|
2822
2985
|
if (backups) {
|
|
2823
2986
|
for (const chat of found) {
|
|
2824
2987
|
const key = `${chat.provider}:${chat.providerSessionId}`;
|
|
@@ -2829,7 +2992,12 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2829
2992
|
}
|
|
2830
2993
|
}
|
|
2831
2994
|
}
|
|
2832
|
-
|
|
2995
|
+
const limit = opts?.limit ?? 60;
|
|
2996
|
+
const oldest = found.length >= limit ? found[found.length - 1]?.updatedAt ?? "" : "";
|
|
2997
|
+
const gone = unavailableFromHints(hints, new Set(found.map((c) => titleHintKey(c.provider, c.providerSessionId)))).filter(
|
|
2998
|
+
(c) => c.updatedAt >= oldest
|
|
2999
|
+
);
|
|
3000
|
+
return gone.length ? [...found, ...gone].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, limit) : found;
|
|
2833
3001
|
},
|
|
2834
3002
|
async recentFolders() {
|
|
2835
3003
|
const seen = /* @__PURE__ */ new Map();
|
|
@@ -2849,8 +3017,19 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2849
3017
|
}))
|
|
2850
3018
|
);
|
|
2851
3019
|
},
|
|
3020
|
+
async titleStatus() {
|
|
3021
|
+
if (!titleHints) return { owner: false, pending: false, problem: null };
|
|
3022
|
+
const problem = titleHints.problem?.() ?? null;
|
|
3023
|
+
return { owner: true, pending: hintsNow() === null && problem === null, problem };
|
|
3024
|
+
},
|
|
3025
|
+
async retryTitles() {
|
|
3026
|
+
titleHints?.retry?.();
|
|
3027
|
+
},
|
|
3028
|
+
setHomes(next) {
|
|
3029
|
+
homes = next;
|
|
3030
|
+
},
|
|
2852
3031
|
async listSessions() {
|
|
2853
|
-
return index.sessions();
|
|
3032
|
+
return index.sessions().map(withHintTitle);
|
|
2854
3033
|
},
|
|
2855
3034
|
async listRuns(sessionId) {
|
|
2856
3035
|
const session = index.session(sessionId);
|
|
@@ -3025,7 +3204,9 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
3025
3204
|
return all;
|
|
3026
3205
|
},
|
|
3027
3206
|
async searchChats(query, opts) {
|
|
3028
|
-
|
|
3207
|
+
const result = await searchLocalChats(String(query ?? ""), { homes, cursor: opts?.cursor, limit: opts?.limit, appTitles: appTitles() });
|
|
3208
|
+
const hints = hintsNow();
|
|
3209
|
+
return { ...result, hits: result.hits.map((hit) => ({ ...hit, title: hints?.get(titleHintKey(hit.provider, hit.providerSessionId))?.title ?? hit.title })) };
|
|
3029
3210
|
},
|
|
3030
3211
|
async openFolder(raw, target) {
|
|
3031
3212
|
const folder = await assertFolder(String(raw ?? ""));
|
|
@@ -3055,6 +3236,7 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
3055
3236
|
return agents.handleMcp(secret, body);
|
|
3056
3237
|
},
|
|
3057
3238
|
async dispose() {
|
|
3239
|
+
unsubscribeHints?.();
|
|
3058
3240
|
for (const liveRun of live.values()) {
|
|
3059
3241
|
liveRun.stopRequested = true;
|
|
3060
3242
|
if (liveRun.pgid !== null) killGroup(liveRun);
|
|
@@ -3106,6 +3288,8 @@ function openFolderIn(folder, target) {
|
|
|
3106
3288
|
var BRIDGE_METHODS = [
|
|
3107
3289
|
"providers",
|
|
3108
3290
|
"discoverSessions",
|
|
3291
|
+
"titleStatus",
|
|
3292
|
+
"retryTitles",
|
|
3109
3293
|
"recentFolders",
|
|
3110
3294
|
"listSessions",
|
|
3111
3295
|
"listRuns",
|