claude-simple 0.2.27 → 0.2.28
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/client.js +21 -2
- package/dist/app.0.2.28.min.js +1220 -0
- package/dist/index.html +2 -2
- package/dist/{style.0.2.27.min.css → style.0.2.28.min.css} +157 -1
- package/package.json +1 -1
- package/public/agent-panel.js +7 -2
- package/public/api.js +7 -4
- package/public/app.js +2 -0
- package/public/chat.js +34 -0
- package/public/notes.js +316 -0
- package/public/router.js +4 -0
- package/public/shell/session-list.js +30 -5
- package/public/shell/session-nav.js +6 -4
- package/public/shell/templates.js +3 -0
- package/public/shell.js +15 -2
- package/public/style.css +156 -0
- package/public/sw.js +1 -1
- package/src/agents/grok.js +16 -0
- package/src/agents/sessions.js +137 -12
- package/src/app.js +31 -15
- package/src/session-history.js +31 -0
- package/dist/app.0.2.27.min.js +0 -1182
package/public/style.css
CHANGED
|
@@ -1830,6 +1830,122 @@ html.settings-open #app { visibility: hidden; }
|
|
|
1830
1830
|
.set-nav-item { white-space: nowrap; }
|
|
1831
1831
|
}
|
|
1832
1832
|
|
|
1833
|
+
/* ── Personal notes (main pane, sidebar stays) ────────────────────────────── */
|
|
1834
|
+
|
|
1835
|
+
.notes-page {
|
|
1836
|
+
flex: 1;
|
|
1837
|
+
min-height: 0;
|
|
1838
|
+
display: flex;
|
|
1839
|
+
flex-direction: column;
|
|
1840
|
+
padding: 16px clamp(12px, 3vw, 32px) 24px;
|
|
1841
|
+
gap: 12px;
|
|
1842
|
+
overflow-y: auto;
|
|
1843
|
+
background: hsl(var(--b1));
|
|
1844
|
+
color: hsl(var(--bc));
|
|
1845
|
+
}
|
|
1846
|
+
.notes-page[hidden] { display: none !important; }
|
|
1847
|
+
.notes-count {
|
|
1848
|
+
margin: 4px 0 0;
|
|
1849
|
+
font-size: 12px;
|
|
1850
|
+
color: hsl(var(--bc) / .55);
|
|
1851
|
+
}
|
|
1852
|
+
.notes-header {
|
|
1853
|
+
display: flex;
|
|
1854
|
+
align-items: center;
|
|
1855
|
+
justify-content: space-between;
|
|
1856
|
+
gap: 16px;
|
|
1857
|
+
border-bottom: 1px solid hsl(var(--b3) / .35);
|
|
1858
|
+
padding-bottom: 12px;
|
|
1859
|
+
}
|
|
1860
|
+
.notes-header h2 {
|
|
1861
|
+
margin: 0;
|
|
1862
|
+
font-size: 1.4rem;
|
|
1863
|
+
letter-spacing: .02em;
|
|
1864
|
+
}
|
|
1865
|
+
.notes-header-actions { display: flex; gap: 8px; }
|
|
1866
|
+
.notes-add {
|
|
1867
|
+
display: flex;
|
|
1868
|
+
flex-direction: column;
|
|
1869
|
+
gap: 8px;
|
|
1870
|
+
padding: 12px;
|
|
1871
|
+
border-radius: 12px;
|
|
1872
|
+
background: hsl(var(--b2));
|
|
1873
|
+
border: 1px solid hsl(var(--b3) / .4);
|
|
1874
|
+
}
|
|
1875
|
+
.notes-add textarea,
|
|
1876
|
+
.notes-edit-text {
|
|
1877
|
+
width: 100%;
|
|
1878
|
+
font: inherit;
|
|
1879
|
+
background: hsl(var(--b1));
|
|
1880
|
+
color: inherit;
|
|
1881
|
+
border: 1px solid hsl(var(--b3) / .4);
|
|
1882
|
+
border-radius: 8px;
|
|
1883
|
+
padding: 10px 12px;
|
|
1884
|
+
resize: vertical;
|
|
1885
|
+
min-height: 80px;
|
|
1886
|
+
}
|
|
1887
|
+
.notes-form-actions {
|
|
1888
|
+
display: flex;
|
|
1889
|
+
gap: 8px;
|
|
1890
|
+
justify-content: flex-end;
|
|
1891
|
+
}
|
|
1892
|
+
.notes-list {
|
|
1893
|
+
list-style: none;
|
|
1894
|
+
margin: 0;
|
|
1895
|
+
padding: 0;
|
|
1896
|
+
display: flex;
|
|
1897
|
+
flex-direction: column;
|
|
1898
|
+
gap: 10px;
|
|
1899
|
+
}
|
|
1900
|
+
.notes-empty {
|
|
1901
|
+
padding: 28px;
|
|
1902
|
+
text-align: center;
|
|
1903
|
+
color: hsl(var(--bc) / .55);
|
|
1904
|
+
border: 1px dashed hsl(var(--b3) / .45);
|
|
1905
|
+
border-radius: 12px;
|
|
1906
|
+
}
|
|
1907
|
+
.notes-error { color: hsl(var(--er) / .85); border-color: hsl(var(--er) / .35); }
|
|
1908
|
+
.notes-item {
|
|
1909
|
+
background: hsl(var(--b2));
|
|
1910
|
+
border: 1px solid hsl(var(--b3) / .35);
|
|
1911
|
+
border-radius: 12px;
|
|
1912
|
+
padding: 14px 16px;
|
|
1913
|
+
display: flex;
|
|
1914
|
+
flex-direction: column;
|
|
1915
|
+
gap: 8px;
|
|
1916
|
+
}
|
|
1917
|
+
.notes-text {
|
|
1918
|
+
white-space: pre-wrap;
|
|
1919
|
+
word-break: break-word;
|
|
1920
|
+
font-size: .98rem;
|
|
1921
|
+
line-height: 1.55;
|
|
1922
|
+
}
|
|
1923
|
+
.notes-meta {
|
|
1924
|
+
font-size: .75rem;
|
|
1925
|
+
color: hsl(var(--bc) / .55);
|
|
1926
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
1927
|
+
}
|
|
1928
|
+
.notes-item-foot {
|
|
1929
|
+
display: flex;
|
|
1930
|
+
align-items: center;
|
|
1931
|
+
gap: 8px;
|
|
1932
|
+
}
|
|
1933
|
+
.notes-item-actions {
|
|
1934
|
+
display: flex;
|
|
1935
|
+
gap: 4px;
|
|
1936
|
+
margin-left: auto;
|
|
1937
|
+
}
|
|
1938
|
+
.btn.danger {
|
|
1939
|
+
background: hsl(var(--er) / .15);
|
|
1940
|
+
color: hsl(var(--er));
|
|
1941
|
+
border: 1px solid hsl(var(--er) / .35);
|
|
1942
|
+
}
|
|
1943
|
+
.btn.primary {
|
|
1944
|
+
background: hsl(var(--p));
|
|
1945
|
+
color: hsl(var(--pc));
|
|
1946
|
+
border: 1px solid hsl(var(--p) / .85);
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1833
1949
|
/* Overlays must not eat keystrokes when hidden */
|
|
1834
1950
|
#sidebar-overlay.hidden,
|
|
1835
1951
|
.mermaid-zoom-overlay.hidden,
|
|
@@ -1847,3 +1963,43 @@ html.settings-open #app { visibility: hidden; }
|
|
|
1847
1963
|
cursor: not-allowed;
|
|
1848
1964
|
opacity: .78;
|
|
1849
1965
|
}
|
|
1966
|
+
|
|
1967
|
+
.notes-add[hidden] { display: none; }
|
|
1968
|
+
.notes-error { color: #ef4444; }
|
|
1969
|
+
|
|
1970
|
+
.notes-item {
|
|
1971
|
+
background: rgba(255255255, .04);
|
|
1972
|
+
border: 1px solid rgba(255255255, .08);
|
|
1973
|
+
border-radius: 8px;
|
|
1974
|
+
padding: 12px;
|
|
1975
|
+
}
|
|
1976
|
+
.notes-text {
|
|
1977
|
+
white-space: pre-wrap;
|
|
1978
|
+
word-break: break-word;
|
|
1979
|
+
line-height: 1.55;
|
|
1980
|
+
}
|
|
1981
|
+
.notes-meta {
|
|
1982
|
+
margin-top: 8px;
|
|
1983
|
+
color: rgba(255255255, .4);
|
|
1984
|
+
font-size: 11px;
|
|
1985
|
+
display: flex;
|
|
1986
|
+
gap: 6px;
|
|
1987
|
+
}
|
|
1988
|
+
.notes-item-actions {
|
|
1989
|
+
margin-top: 8px;
|
|
1990
|
+
display: flex;
|
|
1991
|
+
gap: 6px;
|
|
1992
|
+
justify-content: flex-end;
|
|
1993
|
+
}
|
|
1994
|
+
.notes-edit-text {
|
|
1995
|
+
width: 100%;
|
|
1996
|
+
resize: vertical;
|
|
1997
|
+
min-height: 60px;
|
|
1998
|
+
background: transparent;
|
|
1999
|
+
color: inherit;
|
|
2000
|
+
border: 1px solid rgba(255255255, .08);
|
|
2001
|
+
border-radius: 6px;
|
|
2002
|
+
padding: 6px 8px;
|
|
2003
|
+
font-family: inherit;
|
|
2004
|
+
font-size: 14px;
|
|
2005
|
+
}
|
package/public/sw.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* Agent UI service worker — cache shell assets only.
|
|
2
2
|
* Never intercepts WebSocket or mutates API semantics beyond offline fallback.
|
|
3
3
|
* Bump CACHE when shipping UI that must invalidate old shells. */
|
|
4
|
-
const CACHE = 'agent-ui-shell-
|
|
4
|
+
const CACHE = 'agent-ui-shell-v28';
|
|
5
5
|
const PRECACHE = [
|
|
6
6
|
'/',
|
|
7
7
|
'/index.html',
|
package/src/agents/grok.js
CHANGED
|
@@ -388,8 +388,24 @@ function clientFor(options, cliPath) {
|
|
|
388
388
|
return client;
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
+
export function grokActivityStatus(update) {
|
|
392
|
+
const kind = update?.sessionUpdate;
|
|
393
|
+
if (kind === 'tool_call' || kind === 'tool_call_update') {
|
|
394
|
+
const name = update.title || update.kind || 'tool';
|
|
395
|
+
const st = update.status ? ` (${update.status})` : '';
|
|
396
|
+
return `Grok 工具: ${name}${st}`;
|
|
397
|
+
}
|
|
398
|
+
if (kind === 'agent_thought_chunk') return 'Grok 正在思考…';
|
|
399
|
+
if (kind === 'agent_message_chunk') return '';
|
|
400
|
+
return 'Grok ACP running…';
|
|
401
|
+
}
|
|
402
|
+
|
|
391
403
|
function emitUpdate(update, send, state) {
|
|
392
404
|
const kind = update?.sessionUpdate;
|
|
405
|
+
const activity = grokActivityStatus(update);
|
|
406
|
+
if (activity) {
|
|
407
|
+
send({ type: 'status', message: activity, agent: 'grok' });
|
|
408
|
+
}
|
|
393
409
|
if (kind === 'agent_message_chunk' && update.content?.text) {
|
|
394
410
|
if (state.thoughtOpen) {
|
|
395
411
|
send({ type: 'thought_stream_end', agent: 'grok' });
|
package/src/agents/sessions.js
CHANGED
|
@@ -496,6 +496,105 @@ async function forkCodexNativeSession(sourceSessionId, targetSessionId, codexHom
|
|
|
496
496
|
return targetFile;
|
|
497
497
|
}
|
|
498
498
|
|
|
499
|
+
function grokAssistantText(record) {
|
|
500
|
+
if (record?.type !== 'assistant') return '';
|
|
501
|
+
return extractGrokText(record.content).trim();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function grokUpdateKind(record) {
|
|
505
|
+
const update = record?.params?.update || record?.update || {};
|
|
506
|
+
return update.sessionUpdate || update.type || '';
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function grokUpdateText(record) {
|
|
510
|
+
const update = record?.params?.update || record?.update || {};
|
|
511
|
+
return extractGrokText(update.content).trim();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Grok resumes from a session directory, not a synthetic chat transcript.
|
|
516
|
+
* Fork by cloning the native files and cutting both journals at the selected
|
|
517
|
+
* assistant reply so tool results and encrypted reasoning stay intact.
|
|
518
|
+
*/
|
|
519
|
+
async function forkGrokNativeSession(sourceSessionId, targetSessionId, grokHome, assistantText, occurrence) {
|
|
520
|
+
const sourceDir = await findGrokSessionDir(sourceSessionId, grokHome);
|
|
521
|
+
if (!sourceDir) throw new Error('Grok source session was not found');
|
|
522
|
+
|
|
523
|
+
const sourceHistory = await fs.readFile(path.join(sourceDir, 'chat_history.jsonl'), 'utf8');
|
|
524
|
+
const historyOut = [];
|
|
525
|
+
let foundForkPoint = false;
|
|
526
|
+
let seenMatches = 0;
|
|
527
|
+
for (const line of sourceHistory.split(/\r?\n/)) {
|
|
528
|
+
if (!line.trim()) continue;
|
|
529
|
+
let record;
|
|
530
|
+
try { record = JSON.parse(line); } catch { continue; }
|
|
531
|
+
historyOut.push(JSON.stringify(replaceSessionId(record, sourceSessionId, targetSessionId)));
|
|
532
|
+
const text = grokAssistantText(record);
|
|
533
|
+
if (text && text === assistantText && ++seenMatches === occurrence) {
|
|
534
|
+
foundForkPoint = true;
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (!foundForkPoint) {
|
|
539
|
+
throw new Error('Selected Grok assistant message was not found in the native transcript');
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const targetDir = path.join(path.dirname(sourceDir), targetSessionId);
|
|
543
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
544
|
+
const histTmp = path.join(targetDir, `chat_history.jsonl.tmp-${crypto.randomUUID()}`);
|
|
545
|
+
await fs.writeFile(histTmp, `${historyOut.join('\n')}\n`, 'utf8');
|
|
546
|
+
await fs.rename(histTmp, path.join(targetDir, 'chat_history.jsonl'));
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
const sourceUpdates = await fs.readFile(path.join(sourceDir, 'updates.jsonl'), 'utf8');
|
|
550
|
+
const updatesOut = [];
|
|
551
|
+
let acc = '';
|
|
552
|
+
let seenUpdateMatches = 0;
|
|
553
|
+
let matched = false;
|
|
554
|
+
for (const line of sourceUpdates.split(/\r?\n/)) {
|
|
555
|
+
if (!line.trim()) continue;
|
|
556
|
+
let record;
|
|
557
|
+
try { record = JSON.parse(line); } catch { continue; }
|
|
558
|
+
const kind = grokUpdateKind(record);
|
|
559
|
+
if (matched && (kind === 'user_message_chunk' || kind === 'agent_message_chunk')) break;
|
|
560
|
+
updatesOut.push(JSON.stringify(replaceSessionId(record, sourceSessionId, targetSessionId)));
|
|
561
|
+
if (kind === 'agent_message_chunk') {
|
|
562
|
+
acc = `${acc}${grokUpdateText(record)}`;
|
|
563
|
+
if (acc.trim() === assistantText && ++seenUpdateMatches === occurrence) matched = true;
|
|
564
|
+
} else if (kind === 'user_message_chunk' || kind === 'turn_completed') {
|
|
565
|
+
acc = '';
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const updTmp = path.join(targetDir, `updates.jsonl.tmp-${crypto.randomUUID()}`);
|
|
569
|
+
await fs.writeFile(updTmp, updatesOut.length ? `${updatesOut.join('\n')}\n` : '', 'utf8');
|
|
570
|
+
await fs.rename(updTmp, path.join(targetDir, 'updates.jsonl'));
|
|
571
|
+
} catch {}
|
|
572
|
+
|
|
573
|
+
const copyNames = ['system_prompt.txt', 'prompt_context.json', 'resources_state.json', 'announcement_state.json', 'signals.json'];
|
|
574
|
+
for (const name of copyNames) {
|
|
575
|
+
try {
|
|
576
|
+
const raw = await fs.readFile(path.join(sourceDir, name), 'utf8');
|
|
577
|
+
let out = raw;
|
|
578
|
+
if (name.endsWith('.json')) {
|
|
579
|
+
try { out = JSON.stringify(replaceSessionId(JSON.parse(raw), sourceSessionId, targetSessionId), null, 2); }
|
|
580
|
+
catch { out = raw.replaceAll(sourceSessionId, targetSessionId); }
|
|
581
|
+
}
|
|
582
|
+
await fs.writeFile(path.join(targetDir, name), out, 'utf8');
|
|
583
|
+
} catch {}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
try {
|
|
587
|
+
const summaryRaw = await fs.readFile(path.join(sourceDir, 'summary.json'), 'utf8');
|
|
588
|
+
const summary = replaceSessionId(JSON.parse(summaryRaw), sourceSessionId, targetSessionId);
|
|
589
|
+
if (summary.info && typeof summary.info === 'object') summary.info.id = targetSessionId;
|
|
590
|
+
else summary.info = { ...(summary.info || {}), id: targetSessionId };
|
|
591
|
+
summary.updated_at = new Date().toISOString();
|
|
592
|
+
await fs.writeFile(path.join(targetDir, 'summary.json'), JSON.stringify(summary, null, 2), 'utf8');
|
|
593
|
+
} catch {}
|
|
594
|
+
|
|
595
|
+
return targetDir;
|
|
596
|
+
}
|
|
597
|
+
|
|
499
598
|
// ─── Grok ─────────────────────────────────────────────────────────────────────
|
|
500
599
|
|
|
501
600
|
export async function scanGrokSessions(grokHome, filterCwd = null) {
|
|
@@ -1654,10 +1753,10 @@ export async function readSessionMemoryFile(sessionId, agent, fileId, opts) {
|
|
|
1654
1753
|
}
|
|
1655
1754
|
|
|
1656
1755
|
/** Return bounded raw JSONL for a particular agent session, for Meta Agent analysis. */
|
|
1657
|
-
export async function loadSessionJsonl(sessionId, agent, { claudeConfigDirs, codexHome, grokHome, geminiHome, cursorHome }, { offset = 0, maxLines = 500 } = {}) {
|
|
1756
|
+
export async function loadSessionJsonl(sessionId, agent, { claudeConfigDirs, codexHome, grokHome, geminiHome, cursorHome }, { offset = 0, maxLines = 500, tail = false, maxChars = 24000 } = {}) {
|
|
1658
1757
|
if (!agent) {
|
|
1659
1758
|
for (const candidate of ['claude', 'codex', 'grok', 'gemini', 'cursor']) {
|
|
1660
|
-
const found = await loadSessionJsonl(sessionId, candidate, { claudeConfigDirs, codexHome, grokHome, geminiHome, cursorHome }, { offset, maxLines });
|
|
1759
|
+
const found = await loadSessionJsonl(sessionId, candidate, { claudeConfigDirs, codexHome, grokHome, geminiHome, cursorHome }, { offset, maxLines, tail, maxChars });
|
|
1661
1760
|
if (found) return found;
|
|
1662
1761
|
}
|
|
1663
1762
|
return null;
|
|
@@ -1673,20 +1772,35 @@ export async function loadSessionJsonl(sessionId, agent, { claudeConfigDirs, cod
|
|
|
1673
1772
|
file = dir ? path.join(dir, 'chat_history.jsonl') : null;
|
|
1674
1773
|
} else file = await findClaudeSessionFile(sessionId, claudeConfigDirs);
|
|
1675
1774
|
if (!file) return null;
|
|
1676
|
-
const start = Math.max(0, Number(offset) || 0);
|
|
1677
|
-
const limit = Math.min(Math.max(1, Number(maxLines) ||
|
|
1775
|
+
const start = Math.max(0, Math.floor(Number(offset) || 0));
|
|
1776
|
+
const limit = Math.min(Math.max(1, Math.floor(Number(maxLines) || 100)), 500);
|
|
1777
|
+
const budget = Math.min(Math.max(1000, Number(maxChars) || 24000), 64000);
|
|
1678
1778
|
const lines = [];
|
|
1679
|
-
let n = 0;
|
|
1680
|
-
let hasMore = false;
|
|
1779
|
+
let n = 0, chars = 0, hasMore = false, clipped = false;
|
|
1681
1780
|
const stream = fsSync.createReadStream(file, { encoding: 'utf8' });
|
|
1682
1781
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1782
|
+
try {
|
|
1783
|
+
for await (const line of rl) {
|
|
1784
|
+
const index = n++;
|
|
1785
|
+
if (!tail && index < start) continue;
|
|
1786
|
+
if (!tail && (lines.length >= limit || chars >= budget)) { hasMore = true; break; }
|
|
1787
|
+
// Bound huge tool output rows as well as total response size. Clipping
|
|
1788
|
+
// is explicit: a clipped line must not be mistaken for complete JSON.
|
|
1789
|
+
const cap = tail ? budget : budget - chars;
|
|
1790
|
+
const text = line.slice(0, cap);
|
|
1791
|
+
if (text.length < line.length) clipped = true;
|
|
1792
|
+
lines.push(text); chars += text.length;
|
|
1793
|
+
if (tail) while (lines.length > limit || (chars > budget && lines.length > 1)) {
|
|
1794
|
+
chars -= lines.shift().length;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
} finally { rl.close(); stream.destroy(); }
|
|
1688
1798
|
const stat = await fs.stat(file).catch(() => null);
|
|
1689
|
-
|
|
1799
|
+
const actualOffset = tail ? Math.max(0, n - lines.length) : start;
|
|
1800
|
+
return { agent: a, fileName: path.basename(file), offset: actualOffset, lines,
|
|
1801
|
+
nextOffset: actualOffset + lines.length, truncated: hasMore, clipped,
|
|
1802
|
+
tail, size: stat?.size || 0, updatedAt: stat?.mtimeMs || null };
|
|
1803
|
+
|
|
1690
1804
|
}
|
|
1691
1805
|
|
|
1692
1806
|
export async function getSessionMeta(sessionId, agent, { claudeConfigDirs, codexHome, grokHome, geminiHome, cursorHome }) {
|
|
@@ -1796,6 +1910,17 @@ export async function convertSession(sourceSessionId, sourceAgent, targetAgent,
|
|
|
1796
1910
|
} catch {}
|
|
1797
1911
|
return { targetSessionId };
|
|
1798
1912
|
}
|
|
1913
|
+
|
|
1914
|
+
if (ta === 'grok' && sourceAgent.toLowerCase() === 'grok' && Number.isInteger(upTo)) {
|
|
1915
|
+
const selected = messages[messages.length - 1];
|
|
1916
|
+
if (selected?.role !== 'assistant' || !selected.content?.trim()) {
|
|
1917
|
+
throw new Error('A Grok fork must end at a persisted assistant message');
|
|
1918
|
+
}
|
|
1919
|
+
const occurrence = messages.filter(message => message.role === 'assistant'
|
|
1920
|
+
&& message.content?.trim() === selected.content.trim()).length;
|
|
1921
|
+
await forkGrokNativeSession(sourceSessionId, targetSessionId, grokHome, selected.content.trim(), occurrence);
|
|
1922
|
+
return { targetSessionId };
|
|
1923
|
+
}
|
|
1799
1924
|
|
|
1800
1925
|
if (ta === 'claude') {
|
|
1801
1926
|
const configDir = claudeConfigDirs?.[0] || path.join(os.homedir(), '.claude');
|
package/src/app.js
CHANGED
|
@@ -37,6 +37,7 @@ import { createExternalSessionMirror } from './runtime/external-session-sync.js'
|
|
|
37
37
|
import { resolveApproval, cancelApprovalsForSession } from './runtime/approvals.js';
|
|
38
38
|
import { createMcpHttpHandler, createMcpService } from './mcp.js';
|
|
39
39
|
import { createCollab, roleAtLeast, proxyToLocalPort, machineId as localMachineId } from './collab.js';
|
|
40
|
+
import { parseHistoryQuery, sliceHistoryMessages } from './session-history.js';
|
|
40
41
|
import {
|
|
41
42
|
CLAUDE_ENV_KEYS,
|
|
42
43
|
loadAgentEnvDoc,
|
|
@@ -1376,11 +1377,9 @@ export function createApp() {
|
|
|
1376
1377
|
const { sessionId } = req.params;
|
|
1377
1378
|
if (!/^[0-9a-f-]{36}$/i.test(sessionId)) return res.status(400).json({ error: 'Invalid session id' });
|
|
1378
1379
|
const preferred = typeof req.query.agent === 'string' ? req.query.agent : null;
|
|
1379
|
-
// tail=0 → full history (default). Positive = last N
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
if (tail < 0) tail = 0;
|
|
1383
|
-
if (tail > 50_000) tail = 50_000;
|
|
1380
|
+
// tail=0 → full history (default). Positive = last N. before = exclusive
|
|
1381
|
+
// end index for older pages (scroll-up).
|
|
1382
|
+
const { tail, before } = parseHistoryQuery(req.query);
|
|
1384
1383
|
const compact = req.query.compact !== '0' && req.query.compact !== 'false';
|
|
1385
1384
|
const opts = {
|
|
1386
1385
|
claudeConfigDirs: CLAUDE_CONFIG_DIRS,
|
|
@@ -1403,20 +1402,16 @@ export function createApp() {
|
|
|
1403
1402
|
let messages = Array.isArray(bundle)
|
|
1404
1403
|
? bundle
|
|
1405
1404
|
: (bundle.messages || []);
|
|
1406
|
-
const
|
|
1407
|
-
|
|
1408
|
-
if (tail > 0 && messages.length > tail) {
|
|
1409
|
-
messages = messages.slice(-tail);
|
|
1410
|
-
truncated = true;
|
|
1411
|
-
}
|
|
1412
|
-
if (compact) messages = compactHistoryMessages(messages);
|
|
1405
|
+
const page = sliceHistoryMessages(messages, { tail, before });
|
|
1406
|
+
messages = compact ? compactHistoryMessages(page.messages) : page.messages;
|
|
1413
1407
|
const payload = {
|
|
1414
1408
|
messages,
|
|
1415
1409
|
context: Array.isArray(bundle) ? null : (bundle.context || null),
|
|
1416
1410
|
agent: Array.isArray(bundle) ? (preferred || 'claude') : (bundle.agent || preferred || 'claude'),
|
|
1417
|
-
total,
|
|
1418
|
-
truncated,
|
|
1419
|
-
|
|
1411
|
+
total: page.total,
|
|
1412
|
+
truncated: page.truncated,
|
|
1413
|
+
oldestIndex: page.oldestIndex,
|
|
1414
|
+
tail: page.tail,
|
|
1420
1415
|
compact,
|
|
1421
1416
|
};
|
|
1422
1417
|
res.setHeader('X-Session-Agent', payload.agent);
|
|
@@ -1552,6 +1547,27 @@ export function createApp() {
|
|
|
1552
1547
|
}
|
|
1553
1548
|
});
|
|
1554
1549
|
|
|
1550
|
+
app.get('/api/sessions/:sessionId/jsonl', authMiddleware, async (req, res) => {
|
|
1551
|
+
try {
|
|
1552
|
+
const { sessionId } = req.params;
|
|
1553
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId)) {
|
|
1554
|
+
return res.status(400).json({ error: 'Invalid session id' });
|
|
1555
|
+
}
|
|
1556
|
+
const agent = typeof req.query.agent === 'string' ? req.query.agent : null;
|
|
1557
|
+
if (agent && !['claude', 'codex', 'grok', 'gemini', 'cursor'].includes(agent)) {
|
|
1558
|
+
return res.status(400).json({ error: 'Invalid agent' });
|
|
1559
|
+
}
|
|
1560
|
+
const result = await loadSessionJsonl(sessionId, agent, {
|
|
1561
|
+
claudeConfigDirs: CLAUDE_CONFIG_DIRS, codexHome: CODEX_HOME,
|
|
1562
|
+
grokHome: GROK_HOME, geminiHome: GEMINI_HOME, cursorHome: CURSOR_HOME,
|
|
1563
|
+
}, { offset: req.query.offset, maxLines: req.query.limit,
|
|
1564
|
+
tail: req.query.tail === '1', maxChars: req.query.maxChars });
|
|
1565
|
+
if (!result) return res.status(404).json({ error: 'Session JSONL not found' });
|
|
1566
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
1567
|
+
res.json({ ...result, sessionId, running: isSessionBusy(sessionId) });
|
|
1568
|
+
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1555
1571
|
app.get('/api/sessions/:sessionId/memory/file', authMiddleware, async (req, res) => {
|
|
1556
1572
|
try {
|
|
1557
1573
|
const { sessionId } = req.params;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Slice a loaded transcript for mobile/hub transfers. */
|
|
2
|
+
|
|
3
|
+
export function parseHistoryQuery(query = {}) {
|
|
4
|
+
let tail = parseInt(query.tail ?? '0', 10);
|
|
5
|
+
if (Number.isNaN(tail) || tail < 0) tail = 0;
|
|
6
|
+
if (tail > 50_000) tail = 50_000;
|
|
7
|
+
let before = parseInt(query.before ?? '', 10);
|
|
8
|
+
if (Number.isNaN(before) || before < 0) before = null;
|
|
9
|
+
return { tail, before };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {unknown[]} messages
|
|
14
|
+
* @param {{ tail?: number, before?: number|null }} opts
|
|
15
|
+
*/
|
|
16
|
+
export function sliceHistoryMessages(messages, { tail = 0, before = null } = {}) {
|
|
17
|
+
const all = Array.isArray(messages) ? messages : [];
|
|
18
|
+
const total = all.length;
|
|
19
|
+
let end = total;
|
|
20
|
+
if (before != null) end = Math.min(before, total);
|
|
21
|
+
if (end < 0) end = 0;
|
|
22
|
+
let start = 0;
|
|
23
|
+
if (tail > 0 && end - start > tail) start = end - tail;
|
|
24
|
+
return {
|
|
25
|
+
messages: all.slice(start, end),
|
|
26
|
+
total,
|
|
27
|
+
truncated: start > 0,
|
|
28
|
+
oldestIndex: start,
|
|
29
|
+
tail: end - start,
|
|
30
|
+
};
|
|
31
|
+
}
|