jaitus 0.3.12 → 0.3.14
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/bin/jaitus.js +86 -73
- package/package.json +2 -2
- package/src/agent/loop.js +998 -980
- package/src/agent/tools.js +11 -5
- package/src/api.js +55 -21
- package/src/commands/update.js +75 -49
- package/src/errors.js +120 -106
- package/src/index.js +3152 -3142
- package/src/rewind.js +149 -148
- package/src/serviceNotice.js +151 -0
- package/src/sessions.js +8 -0
- package/src/tui/clipboard.js +172 -161
- package/src/tui-ink.js +2841 -2605
- package/src/tui.js +2560 -2559
package/src/rewind.js
CHANGED
|
@@ -1,148 +1,149 @@
|
|
|
1
|
-
// ─── Rewind: go back to any earlier turn ────────────────────────────────────────
|
|
2
|
-
//
|
|
3
|
-
// Esc+Esc rewinds the conversation to a previous point — and restores the files as
|
|
4
|
-
// they were. The mechanism is simple and honest: before each user prompt
|
|
5
|
-
// is processed, snapshot the message history; before any write/edit command the agent
|
|
6
|
-
// makes, snapshot the file it is about to touch. Restoring a checkpoint then means
|
|
7
|
-
// truncating the message array and copying the file bytes back.
|
|
8
|
-
//
|
|
9
|
-
// Snapshots live in memory for the session's life: this is undo, not backup. Files are
|
|
10
|
-
// capped (count and size) so one `run_command` that rewrites a giant file cannot eat
|
|
11
|
-
// the heap.
|
|
12
|
-
|
|
13
|
-
import fs from 'node:fs';
|
|
14
|
-
import path from 'node:path';
|
|
15
|
-
import os from 'node:os';
|
|
16
|
-
|
|
17
|
-
const MAX_FILE_SNAPSHOTS = 60;
|
|
18
|
-
const MAX_FILE_BYTES = 2_000_000;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* The rewind journal for one session. Kept per-session so two windows on the same
|
|
22
|
-
* project never share checkpoints.
|
|
23
|
-
*/
|
|
24
|
-
export function createJournal() {
|
|
25
|
-
const checkpoints = []; // { seq, at, prompt, messages, files }
|
|
26
|
-
const fileSnapshots = []; // newest last, global cap
|
|
27
|
-
let seqCounter = 0;
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
/** Snapshot the conversation before a user prompt is processed. */
|
|
31
|
-
checkpoint(messages, files = []) {
|
|
32
|
-
// Checkpoints are
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
const MAX_CHECKPOINT_BYTES = 4_000_000;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
*
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
fs.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
messages.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
}
|
|
1
|
+
// ─── Rewind: go back to any earlier turn ────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Esc+Esc rewinds the conversation to a previous point — and restores the files as
|
|
4
|
+
// they were. The mechanism is simple and honest: before each user prompt
|
|
5
|
+
// is processed, snapshot the message history; before any write/edit command the agent
|
|
6
|
+
// makes, snapshot the file it is about to touch. Restoring a checkpoint then means
|
|
7
|
+
// truncating the message array and copying the file bytes back.
|
|
8
|
+
//
|
|
9
|
+
// Snapshots live in memory for the session's life: this is undo, not backup. Files are
|
|
10
|
+
// capped (count and size) so one `run_command` that rewrites a giant file cannot eat
|
|
11
|
+
// the heap.
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
|
|
17
|
+
const MAX_FILE_SNAPSHOTS = 60;
|
|
18
|
+
const MAX_FILE_BYTES = 2_000_000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The rewind journal for one session. Kept per-session so two windows on the same
|
|
22
|
+
* project never share checkpoints.
|
|
23
|
+
*/
|
|
24
|
+
export function createJournal() {
|
|
25
|
+
const checkpoints = []; // { seq, at, prompt, messages, files }
|
|
26
|
+
const fileSnapshots = []; // newest last, global cap
|
|
27
|
+
let seqCounter = 0;
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
/** Snapshot the conversation before a user prompt is processed. */
|
|
31
|
+
checkpoint(messages, files = []) {
|
|
32
|
+
// Checkpoints are deep clones, and with pasted images in history 60 of them is
|
|
33
|
+
// megabytes of base64 each. Past the cap the CHECKPOINT copy (never the live
|
|
34
|
+
// conversation) replaces each large image with a text note: rewinding across it
|
|
35
|
+
// restores the note, not the pixels. The whole block is replaced, not just its
|
|
36
|
+
// data — an image block whose data is a sentence is rejected by every provider,
|
|
37
|
+
// so a rewind used to make every following request fail.
|
|
38
|
+
const MAX_CHECKPOINT_BYTES = 4_000_000;
|
|
39
|
+
const NOTE = { type: 'text', text: '[An image was here; it is not kept in undo history.]' };
|
|
40
|
+
const lighter = (key, value) => (value && typeof value === 'object' && value.type === 'image'
|
|
41
|
+
&& String(value.source?.data || '').length > 100_000 ? NOTE : value);
|
|
42
|
+
let snap;
|
|
43
|
+
try {
|
|
44
|
+
const raw = JSON.stringify(messages);
|
|
45
|
+
snap = JSON.parse(raw.length > MAX_CHECKPOINT_BYTES ? JSON.stringify(messages, lighter) : raw);
|
|
46
|
+
} catch {
|
|
47
|
+
// Too large even to serialise: keep the structure without images rather than
|
|
48
|
+
// an empty checkpoint, which would rewind the conversation to nothing.
|
|
49
|
+
try { snap = JSON.parse(JSON.stringify(messages, (k, v) => (v && typeof v === 'object' && v.type === 'image' ? NOTE : v))); } catch { return; }
|
|
50
|
+
}
|
|
51
|
+
checkpoints.push({
|
|
52
|
+
seq: ++seqCounter,
|
|
53
|
+
at: new Date().toISOString(),
|
|
54
|
+
prompt: messages.filter((m) => m.role === 'user' && typeof m.content === 'string').at(-1)?.content?.slice(0, 120) || '',
|
|
55
|
+
messages: snap,
|
|
56
|
+
files,
|
|
57
|
+
});
|
|
58
|
+
if (checkpoints.length > 60) checkpoints.shift();
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/** Remember a file's current bytes before the agent overwrites it. */
|
|
62
|
+
snapshotFile(absPath) {
|
|
63
|
+
if (fileSnapshots.length >= MAX_FILE_SNAPSHOTS) fileSnapshots.shift();
|
|
64
|
+
const seq = ++seqCounter; // monotonic: immune to clock granularity
|
|
65
|
+
const takenAt = new Date().toISOString();
|
|
66
|
+
try {
|
|
67
|
+
const stat = fs.statSync(absPath);
|
|
68
|
+
if (!stat.isFile() || stat.size > MAX_FILE_BYTES) return;
|
|
69
|
+
fileSnapshots.push({ path: absPath, data: fs.readFileSync(absPath), takenAt, seq });
|
|
70
|
+
} catch {
|
|
71
|
+
// Unreadable or vanished — restoring means deleting it if it exists now.
|
|
72
|
+
fileSnapshots.push({ path: absPath, data: null, takenAt, seq });
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Snapshot every file this journal has already seen, plus the session save file.
|
|
78
|
+
* Used before run_command: the shell can touch anything, but re-snapshotting
|
|
79
|
+
* known files is bounded and covers the session's own blast radius.
|
|
80
|
+
*/
|
|
81
|
+
snapshotTracked(cwd) {
|
|
82
|
+
for (const snap of fileSnapshots) {
|
|
83
|
+
try { this.snapshotFile(snap.path); } catch { /* already counted */ }
|
|
84
|
+
}
|
|
85
|
+
try { this.snapshotFile(path.join(cwd, '.jaitus', 'session.json')); } catch { /* fine */ }
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The most recent snapshot of one path, as text.
|
|
90
|
+
*
|
|
91
|
+
* The journal already holds what a file looked like before the agent touched it,
|
|
92
|
+
* which is exactly the "before" side of a diff — so showing what an edit actually
|
|
93
|
+
* did costs a lookup rather than a second read of the disk.
|
|
94
|
+
*/
|
|
95
|
+
latestSnapshot(absPath) {
|
|
96
|
+
for (let i = fileSnapshots.length - 1; i >= 0; i--) {
|
|
97
|
+
if (fileSnapshots[i].path === absPath) {
|
|
98
|
+
return fileSnapshots[i].data ? fileSnapshots[i].data.toString('utf8') : '';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
/** Turn summaries for the rewind dialog, oldest first. */
|
|
105
|
+
list() {
|
|
106
|
+
return checkpoints.map((cp, i) => ({ index: i, at: cp.at, prompt: cp.prompt, files: cp.files.length }));
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Restore checkpoint `index`: truncate messages to that point and restore every
|
|
111
|
+
* file the agent changed after it. Returns what happened, for the transcript.
|
|
112
|
+
*/
|
|
113
|
+
restore(index, messages) {
|
|
114
|
+
const cp = checkpoints[index];
|
|
115
|
+
if (!cp) return { ok: false, error: 'No such checkpoint.' };
|
|
116
|
+
const restored = [];
|
|
117
|
+
const deleted = [];
|
|
118
|
+
for (const snap of [...fileSnapshots].reverse()) {
|
|
119
|
+
if (snap.seq <= cp.seq) continue;
|
|
120
|
+
try {
|
|
121
|
+
if (snap.data) {
|
|
122
|
+
fs.mkdirSync(path.dirname(snap.path), { recursive: true });
|
|
123
|
+
fs.writeFileSync(snap.path, snap.data);
|
|
124
|
+
restored.push(snap.path);
|
|
125
|
+
} else if (fs.existsSync(snap.path)) {
|
|
126
|
+
fs.rmSync(snap.path);
|
|
127
|
+
deleted.push(snap.path);
|
|
128
|
+
}
|
|
129
|
+
} catch { /* best effort per file */ }
|
|
130
|
+
}
|
|
131
|
+
messages.length = 0;
|
|
132
|
+
messages.push(...JSON.parse(JSON.stringify(cp.messages)));
|
|
133
|
+
return { ok: true, restored, deleted, messageCount: cp.messages.length };
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
/** Drop all checkpoints (new session, /clear). */
|
|
137
|
+
reset() {
|
|
138
|
+
checkpoints.length = 0;
|
|
139
|
+
fileSnapshots.length = 0;
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Human-friendly absolute-path display (short home, forward slashes). */
|
|
145
|
+
export function displayPath(p) {
|
|
146
|
+
const home = os.homedir();
|
|
147
|
+
const s = String(p || '');
|
|
148
|
+
return s.startsWith(home) ? `~${s.slice(home.length)}` : s;
|
|
149
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// ─── Service notices in the CLI ─────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// The status page (www.jaitus.com/status) is the record. The CLI only says something
|
|
4
|
+
// when it affects using Jaitus right now, the way flyctl checks its status page:
|
|
5
|
+
//
|
|
6
|
+
// - At start-up, in the background with a short timeout: one line for a major or
|
|
7
|
+
// critical incident on the Jaitus service, maintenance in progress, or maintenance
|
|
8
|
+
// starting within 24 hours. Each notice at most once a day on this machine.
|
|
9
|
+
// - When a request has finally failed (after the usual retries) as unavailable, timed
|
|
10
|
+
// out or down for maintenance: the error says which known problem it is, or when
|
|
11
|
+
// Jaitus will be back.
|
|
12
|
+
//
|
|
13
|
+
// The server decides which notices qualify (GET /api/status/notices), by its own clock.
|
|
14
|
+
// Nothing here ever blocks, and any failure is silent. JAITUS_NO_STATUS_CHECK=1 or CI
|
|
15
|
+
// turns the start-up line off.
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { HOME, endpoints } from './config.js';
|
|
20
|
+
|
|
21
|
+
const TIMEOUT_MS = 2000;
|
|
22
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
23
|
+
const SHOWN_FILE = path.join(HOME, 'notice-check.json');
|
|
24
|
+
|
|
25
|
+
/** Whether the start-up line may run at all. */
|
|
26
|
+
export function noticeCheckEnabled(env = process.env) {
|
|
27
|
+
return !(env.JAITUS_NO_STATUS_CHECK || env.CI);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The public notices payload, or null. Never throws. */
|
|
31
|
+
export async function fetchNotices({ baseUrl = endpoints().baseUrl, fetchImpl = fetch, timeoutMs = TIMEOUT_MS } = {}) {
|
|
32
|
+
const ctrl = new AbortController();
|
|
33
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetchImpl(`${baseUrl}/api/status/notices`, { signal: ctrl.signal, headers: { accept: 'application/json' } });
|
|
36
|
+
if (!res.ok) return null;
|
|
37
|
+
const body = await res.json();
|
|
38
|
+
return body && typeof body === 'object' && body.data && typeof body.data === 'object' ? body.data : null;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
} finally {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** "www.jaitus.com/status", from the configured site. */
|
|
47
|
+
export function statusAddress(siteUrl = endpoints().siteUrl) {
|
|
48
|
+
return `${String(siteUrl).replace(/^https?:\/\//, '').replace(/\/+$/, '')}/status`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Text from the server is printed into a terminal: no escape codes, no control
|
|
52
|
+
// characters, one short line.
|
|
53
|
+
const clean = (t, max = 100) => {
|
|
54
|
+
const s = String(t ?? '')
|
|
55
|
+
// eslint-disable-next-line no-control-regex
|
|
56
|
+
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '')
|
|
57
|
+
// eslint-disable-next-line no-control-regex
|
|
58
|
+
.replace(/[\x00-\x1f\x7f-\x9f]/g, ' ')
|
|
59
|
+
.replace(/\s+/g, ' ')
|
|
60
|
+
.trim();
|
|
61
|
+
return s.length > max ? `${s.slice(0, max - 1)}…` : s;
|
|
62
|
+
};
|
|
63
|
+
const dropStop = (t) => t.replace(/[.!?…]+$/, '');
|
|
64
|
+
|
|
65
|
+
/** The start-up line for one notice from the server's `cli` list. */
|
|
66
|
+
export function lineFor(item, where = statusAddress()) {
|
|
67
|
+
if (!item || typeof item !== 'object') return null;
|
|
68
|
+
const title = dropStop(clean(item.title));
|
|
69
|
+
if (item.kind === 'incident') {
|
|
70
|
+
return `Jaitus is having problems${title ? `: ${title}` : ''}. Updates: ${where}`;
|
|
71
|
+
}
|
|
72
|
+
if (item.kind === 'maintenance' && item.phase === 'in_progress') {
|
|
73
|
+
const until = clean(item.until, 40);
|
|
74
|
+
if (item.overrun && until) return `Jaitus is down for maintenance, running past its planned end (${until}). Updates: ${where}`;
|
|
75
|
+
if (until) return `Jaitus is down for maintenance until ${until}. Updates: ${where}`;
|
|
76
|
+
return `Jaitus is down for maintenance. Updates: ${where}`;
|
|
77
|
+
}
|
|
78
|
+
if (item.kind === 'maintenance' && item.phase === 'upcoming') {
|
|
79
|
+
const when = clean(item.window, 60);
|
|
80
|
+
if (!when) return null;
|
|
81
|
+
return `Planned maintenance ${when}: Jaitus will not answer during it. Details: ${where}`;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function readShown(file) {
|
|
87
|
+
try {
|
|
88
|
+
const v = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
89
|
+
return v && typeof v.shown === 'object' && v.shown ? v.shown : {};
|
|
90
|
+
} catch {
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The one line to show at start-up, or null. A notice is shown at most once a day per
|
|
97
|
+
* machine; a planned window and the same window in progress are separate notices.
|
|
98
|
+
*/
|
|
99
|
+
export async function startupNotice({ env = process.env, now = Date.now(), file = SHOWN_FILE, fetchNotices: get = fetchNotices, where } = {}) {
|
|
100
|
+
if (!noticeCheckEnabled(env)) return null;
|
|
101
|
+
const data = await get().catch(() => null);
|
|
102
|
+
const items = Array.isArray(data?.cli) ? data.cli : [];
|
|
103
|
+
if (!items.length) return null;
|
|
104
|
+
const shown = readShown(file);
|
|
105
|
+
for (const item of items) {
|
|
106
|
+
const key = `${item?.id}:${item?.phase}`;
|
|
107
|
+
if (shown[key] != null && now - Number(shown[key]) < DAY_MS) continue;
|
|
108
|
+
const line = lineFor(item, where);
|
|
109
|
+
if (!line) continue;
|
|
110
|
+
shown[key] = now;
|
|
111
|
+
// Forget anything a week old, so the file never grows.
|
|
112
|
+
for (const k of Object.keys(shown)) if (now - Number(shown[k]) > 7 * DAY_MS) delete shown[k];
|
|
113
|
+
try {
|
|
114
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
115
|
+
fs.writeFileSync(file, JSON.stringify({ shown }));
|
|
116
|
+
} catch { /* read-only home: it may show again next start */ }
|
|
117
|
+
return line;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const EXPLAINED = new Set(['maintenance', 'unavailable', 'timeout']);
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A request that finally failed: add what the status page knows to the error's hint —
|
|
126
|
+
* when Jaitus is back from maintenance, or which known problem this is. Mutates and
|
|
127
|
+
* returns `error`; leaves it alone when there is nothing to add or the check fails.
|
|
128
|
+
*/
|
|
129
|
+
export async function explainFailure(error, { baseUrl, fetchNotices: get = fetchNotices, where } = {}) {
|
|
130
|
+
if (!error || !EXPLAINED.has(error.code)) return error;
|
|
131
|
+
const data = await get(baseUrl ? { baseUrl } : undefined).catch(() => null);
|
|
132
|
+
if (!data) return error;
|
|
133
|
+
const at = where || statusAddress();
|
|
134
|
+
const m = data.maintenance;
|
|
135
|
+
if (error.code === 'maintenance' || m?.enabled) {
|
|
136
|
+
const until = clean(m?.until, 40);
|
|
137
|
+
error.title = 'Down for maintenance';
|
|
138
|
+
error.hint = m?.enabled && until
|
|
139
|
+
? (m.overrun
|
|
140
|
+
? `It was planned to end at ${until} and is running late. Updates: ${at}`
|
|
141
|
+
: `Jaitus should be back by ${until}. Updates: ${at}`)
|
|
142
|
+
: `Try again shortly. Updates: ${at}`;
|
|
143
|
+
return error;
|
|
144
|
+
}
|
|
145
|
+
const incident = Array.isArray(data.incidents) ? data.incidents[0] : null;
|
|
146
|
+
const title = incident ? dropStop(clean(incident.title)) : '';
|
|
147
|
+
error.hint = title
|
|
148
|
+
? `Known problem: ${title}. Updates: ${at}`
|
|
149
|
+
: `${error.hint ? `${dropStop(error.hint)}. ` : ''}Service status: ${at}`;
|
|
150
|
+
return error;
|
|
151
|
+
}
|
package/src/sessions.js
CHANGED
|
@@ -9,6 +9,7 @@ import crypto from 'node:crypto';
|
|
|
9
9
|
import { SESSIONS } from './config.js';
|
|
10
10
|
import { c } from './ui.js';
|
|
11
11
|
import { unifiedDiff } from './diff.js';
|
|
12
|
+
import { TOOLS } from './agent/tools.js';
|
|
12
13
|
|
|
13
14
|
function safeId(id) {
|
|
14
15
|
return String(id || '').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 80);
|
|
@@ -195,6 +196,13 @@ export function replaySession(tui, messages) {
|
|
|
195
196
|
/** The same one-line summary the live loop shows, without importing the tool table. */
|
|
196
197
|
function summariseCall(call) {
|
|
197
198
|
const input = call.input && typeof call.input === 'object' ? call.input : {};
|
|
199
|
+
// The same label the row had while the session ran. A second, guessed label here
|
|
200
|
+
// showed a resumed task list as `{"todos":[{"content":…` and a background read as
|
|
201
|
+
// `{"id":"bg1"}`.
|
|
202
|
+
const tool = TOOLS.find((t) => t.name === call.name);
|
|
203
|
+
if (tool?.summarise) {
|
|
204
|
+
try { const s = tool.summarise(input); if (s) return String(s); } catch { /* fall back below */ }
|
|
205
|
+
}
|
|
198
206
|
if (input.command) return String(input.command);
|
|
199
207
|
if (input.path) return String(input.path);
|
|
200
208
|
if (input.pattern) return String(input.pattern);
|