talos-code 0.1.0 → 0.2.1
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 +112 -0
- package/README.md +9 -4
- package/dist/args.js +4 -1
- package/dist/commands/provider-cli.js +146 -87
- package/dist/config/types.js +1 -1
- package/dist/i18n/en/approval.js +3 -0
- package/dist/i18n/en/credentials.js +10 -0
- package/dist/i18n/en/errors.js +21 -0
- package/dist/i18n/en/firstrun.js +12 -0
- package/dist/i18n/en/screen.js +38 -0
- package/dist/i18n/error-view.js +1 -1
- package/dist/main.js +2 -1
- package/dist/provider/openrouter-login.js +172 -0
- package/dist/runtime/context-archive.js +69 -0
- package/dist/runtime/repo.js +11 -0
- package/dist/runtime/supervisor.js +5 -2
- package/dist/runtime/talos-composition.js +9 -3
- package/dist/tui/agent-view.js +51 -0
- package/dist/tui/app.js +364 -27
- package/dist/tui/catalog-service.js +2 -1
- package/dist/tui/components/status-indicator.js +10 -4
- package/dist/tui/components/terminal-shell.js +10 -1
- package/dist/tui/descendant-approvals.js +104 -0
- package/dist/tui/launch-state.js +9 -0
- package/dist/tui/live-activity.js +13 -2
- package/dist/tui/overlays/agent-tree.js +7 -3
- package/dist/tui/overlays/approval-dialog.js +3 -1
- package/dist/tui/overlays/model-picker.js +5 -0
- package/dist/tui/overlays/provider-picker.js +26 -0
- package/dist/tui/project-trust-prompt.js +5 -3
- package/dist/tui/session-controller.js +25 -12
- package/dist/tui/setup-wizard.js +46 -0
- package/dist/tui/slash-commands.js +3 -1
- package/dist/tui/theme-catalog.js +3 -1
- package/dist/version.js +1 -1
- package/dist/workspace/checkpoint-store.js +32 -1
- package/dist/workspace/checkpoint.js +40 -12
- package/package.json +3 -1
- package/vendor/harness-ui/src/kernel/talosHarness.mjs +149 -2
- package/vendor/manifest.json +2 -2
|
@@ -466,7 +466,7 @@ export function createTuiCatalogService(deps) {
|
|
|
466
466
|
const result = await localModels(provider, local, signal);
|
|
467
467
|
if (result.kind !== 'ok') {
|
|
468
468
|
const said = t(result.kind === 'down' ? 'firstrun.models.down' : 'firstrun.models.unreadable', { label, address: result.address });
|
|
469
|
-
return { provider, label, rows: known, verified: false, notice: known.length ? `${said} ${t('firstrun.models.knownOnly')}` : said };
|
|
469
|
+
return { provider, label, rows: known, verified: false, notice: known.length ? `${said} ${t('firstrun.models.knownOnly')}` : said, engine: result.kind };
|
|
470
470
|
}
|
|
471
471
|
const rows = mergeModels(result.rows, known);
|
|
472
472
|
const installed = result.rows.length;
|
|
@@ -498,6 +498,7 @@ export function createTuiCatalogService(deps) {
|
|
|
498
498
|
throw Object.assign(new Error('CONFIG_SCOPE_INVALID'), { code: 'CONFIG_SCOPE_INVALID' }); await deps.persistModel(id); },
|
|
499
499
|
testProviderKey,
|
|
500
500
|
keyTestNotice,
|
|
501
|
+
...(deps.openRouterLogin ? { startOpenRouterLogin: (input) => deps.openRouterLogin(input) } : {}),
|
|
501
502
|
async setProviderKey(id, secret, signal) {
|
|
502
503
|
const test = await testProviderKey(id, secret, signal);
|
|
503
504
|
if (!test.passed)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t } from "../../i18n/index.js";
|
|
1
|
+
import { t, tn } from "../../i18n/index.js";
|
|
2
2
|
import { frameIndex, stableElapsedLabel } from "../render-scheduler.js";
|
|
3
3
|
import { retryLine } from "../live-activity.js";
|
|
4
4
|
/* A static glyph for the states that do not spin: nothing moves while TALOS waits for the person or after the turn. */
|
|
@@ -7,16 +7,22 @@ const STATIC_GLYPH = {
|
|
|
7
7
|
done: { unicode: '✓', ascii: '+' }, interrupted: { unicode: '■', ascii: '-' }, failed: { unicode: '✗', ascii: 'x' },
|
|
8
8
|
};
|
|
9
9
|
/** The one word for an activity, shared by the status row and the main roster row (gate E7). */
|
|
10
|
-
export function activityWord(kind, tool
|
|
10
|
+
export function activityWord(kind, tool, agents) {
|
|
11
|
+
if (kind === 'agents' && agents && agents > 0)
|
|
12
|
+
return tn('screen.busy.agentsCount', agents);
|
|
13
|
+
return kind === 'tool' && tool ? t('screen.busy.toolNamed', { tool }) : t(`screen.busy.${kind}`);
|
|
14
|
+
}
|
|
11
15
|
const NO_ELAPSED = new Set(['idle', 'error']);
|
|
12
|
-
export function busyIndicatorText({ kind, elapsedMs, frame, unicode, frames, tool, retry, nowMs }) {
|
|
16
|
+
export function busyIndicatorText({ kind, elapsedMs, frame, unicode, frames, tool, agents, retry, nowMs }) {
|
|
13
17
|
const spinnerFrames = frames?.length ? frames : (unicode ? ['⠋', '⠙', '⠹', '⠸'] : ['-', '\\', '|', '/']);
|
|
14
18
|
const fixed = STATIC_GLYPH[kind];
|
|
15
19
|
const glyph = fixed ? (unicode ? fixed.unicode : fixed.ascii) : spinnerFrames[Math.abs(frame) % spinnerFrames.length];
|
|
16
20
|
/* R5a S4: the retry line replaces word and clock (the attempt, the status and the countdown say more than elapsed). */
|
|
17
21
|
if (kind === 'retrying' && retry)
|
|
18
22
|
return `${glyph} ${retryLine(retry, nowMs ?? Date.now())}`;
|
|
19
|
-
|
|
23
|
+
/* P13 (owner decision 4): while agents work, the line says how to watch them. */
|
|
24
|
+
const word = activityWord(kind, tool, agents);
|
|
25
|
+
const verb = (kind === 'agents' && agents && agents > 0 ? `${word} · ${t('screen.busy.agentsWatch')}` : word).padEnd(10, ' ');
|
|
20
26
|
const elapsed = NO_ELAPSED.has(kind) ? ' ' : stableElapsedLabel(elapsedMs);
|
|
21
27
|
return `${glyph} ${verb} ${elapsed}`;
|
|
22
28
|
}
|
|
@@ -191,6 +191,15 @@ export function frameRowsFor(rows, platform = process.platform, policy = WIN32_F
|
|
|
191
191
|
const measured = Math.max(1, Math.floor(Number(rows) || 1));
|
|
192
192
|
return platform === 'win32' && policy === 'rows-1' ? Math.max(1, measured - 1) : measured;
|
|
193
193
|
}
|
|
194
|
+
/*
|
|
195
|
+
* P11 (owner, 2026-09-25, "changing theme crashes the process"): Ink's Box wraps its children in a background Provider
|
|
196
|
+
* ONLY when it has a colour (ink/build/components/Box.js:27). `mono` has no background, so crossing it in /theme changed
|
|
197
|
+
* the tree's shape, React remounted the whole app, and its unmount closed the session controller (app.ts, the
|
|
198
|
+
* `controller.close()` cleanup): the conversation was lost. When the session paints the background the frame therefore
|
|
199
|
+
* ALWAYS has a background colour; for a theme without one it is a name chalk does not know, which Ink leaves unpainted
|
|
200
|
+
* (ink/build/colorize.js returns the text unchanged), so mono stays colourless and the shape never changes.
|
|
201
|
+
*/
|
|
202
|
+
export const UNPAINTED_BACKGROUND = 'transparent';
|
|
194
203
|
export function terminalFrameProps({ session, background, rows, columns, platform = process.platform, }) {
|
|
195
204
|
if (session.mode === 'plain')
|
|
196
205
|
return { flexDirection: 'column' };
|
|
@@ -200,6 +209,6 @@ export function terminalFrameProps({ session, background, rows, columns, platfor
|
|
|
200
209
|
flexDirection: 'column',
|
|
201
210
|
width,
|
|
202
211
|
height,
|
|
203
|
-
...(session.paintBackground
|
|
212
|
+
...(session.paintBackground ? { backgroundColor: background || UNPAINTED_BACKGROUND } : {}),
|
|
204
213
|
};
|
|
205
214
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { createTuiEventAdapter } from "./event-adapter.js";
|
|
2
|
+
/** The sessions below `rootId`, at every level, from a session list. */
|
|
3
|
+
export function descendantsOf(rows, rootId) {
|
|
4
|
+
const idOf = (row) => typeof row.sessionId === 'string' && row.sessionId ? row.sessionId : typeof row.id === 'string' && row.id ? row.id : null;
|
|
5
|
+
const below = new Set([rootId]), found = [];
|
|
6
|
+
for (let grew = true; grew;) {
|
|
7
|
+
grew = false;
|
|
8
|
+
for (const row of rows) {
|
|
9
|
+
const id = idOf(row);
|
|
10
|
+
if (!id || below.has(id) || typeof row.parentId !== 'string' || !below.has(row.parentId))
|
|
11
|
+
continue;
|
|
12
|
+
below.add(id);
|
|
13
|
+
grew = true;
|
|
14
|
+
found.push({ sessionId: id, task: typeof row.delegatedTask === 'string' && row.delegatedTask.trim() ? row.delegatedTask : null, depth: typeof row.depth === 'number' ? row.depth : 1 });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return found;
|
|
18
|
+
}
|
|
19
|
+
export function createDescendantApprovals({ runtime, secretValues = [], intervalMs = 500, onRequired, onResolved, onError = () => { } }) {
|
|
20
|
+
let rootId = null, timer = null, syncing = false, generation = 0;
|
|
21
|
+
const watched = new Map();
|
|
22
|
+
function watch(origin) {
|
|
23
|
+
const adapter = createTuiEventAdapter(secretValues);
|
|
24
|
+
adapter.bindSession(origin.sessionId);
|
|
25
|
+
/* The replay is synchronous inside `subscribe`: what it asks and answers is settled first, and only the requests still
|
|
26
|
+
waiting at its end are handed over. */
|
|
27
|
+
let replaying = true;
|
|
28
|
+
const waiting = new Map();
|
|
29
|
+
const handle = (event) => {
|
|
30
|
+
if (event.type === 'approval.required') {
|
|
31
|
+
if (replaying)
|
|
32
|
+
waiting.set(event.requestId, event);
|
|
33
|
+
else
|
|
34
|
+
onRequired(origin, event);
|
|
35
|
+
}
|
|
36
|
+
else if (event.type === 'approval.resolved') {
|
|
37
|
+
if (replaying)
|
|
38
|
+
waiting.delete(event.requestId);
|
|
39
|
+
else
|
|
40
|
+
onResolved(origin, event.requestId);
|
|
41
|
+
}
|
|
42
|
+
else if (replaying && (event.type === 'run.completed' || event.type === 'run.failed' || event.type === 'run.cancelled'))
|
|
43
|
+
waiting.clear();
|
|
44
|
+
};
|
|
45
|
+
let off = () => { };
|
|
46
|
+
try {
|
|
47
|
+
off = runtime.subscribe(origin.sessionId, raw => { for (const event of adapter.translateAll(raw))
|
|
48
|
+
handle(event); });
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
replaying = false;
|
|
52
|
+
}
|
|
53
|
+
watched.set(origin.sessionId, off);
|
|
54
|
+
for (const event of waiting.values())
|
|
55
|
+
onRequired(origin, event);
|
|
56
|
+
}
|
|
57
|
+
async function sync() {
|
|
58
|
+
if (!rootId || syncing)
|
|
59
|
+
return;
|
|
60
|
+
syncing = true;
|
|
61
|
+
const mine = generation;
|
|
62
|
+
try {
|
|
63
|
+
const rows = await runtime.listSessions();
|
|
64
|
+
if (mine !== generation || !rootId)
|
|
65
|
+
return;
|
|
66
|
+
for (const origin of descendantsOf(rows, rootId))
|
|
67
|
+
if (!watched.has(origin.sessionId))
|
|
68
|
+
watch(origin);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
onError(error);
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
syncing = false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function stop() {
|
|
78
|
+
generation++;
|
|
79
|
+
rootId = null;
|
|
80
|
+
if (timer) {
|
|
81
|
+
clearInterval(timer);
|
|
82
|
+
timer = null;
|
|
83
|
+
}
|
|
84
|
+
for (const off of watched.values()) {
|
|
85
|
+
try {
|
|
86
|
+
off();
|
|
87
|
+
}
|
|
88
|
+
catch { /* an unsubscribe that fails leaves nothing to undo */ }
|
|
89
|
+
}
|
|
90
|
+
watched.clear();
|
|
91
|
+
}
|
|
92
|
+
/** Watches the descendants of `sessionId` until `stop`; a second call for the same session changes nothing. */
|
|
93
|
+
function start(sessionId) {
|
|
94
|
+
if (rootId === sessionId && timer)
|
|
95
|
+
return;
|
|
96
|
+
stop();
|
|
97
|
+
rootId = sessionId;
|
|
98
|
+
timer = setInterval(() => void sync(), Math.max(50, intervalMs));
|
|
99
|
+
timer.unref?.();
|
|
100
|
+
void sync();
|
|
101
|
+
}
|
|
102
|
+
/** `sync` asks the session list now (tests, and a delegation that has just started). */
|
|
103
|
+
return { start, stop, sync, watching: () => [...watched.keys()] };
|
|
104
|
+
}
|
package/dist/tui/launch-state.js
CHANGED
|
@@ -49,6 +49,15 @@ function writeRecord(dataRoot, record) {
|
|
|
49
49
|
catch { /* the temporary file was never created */ }
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
+
/** P17: whether this profile has never launched the interface, read without recording a launch (the trust question asks). */
|
|
53
|
+
export function isFirstLaunch(dataRoot) {
|
|
54
|
+
try {
|
|
55
|
+
return readRecord(launchStatePath(dataRoot)) === null;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
52
61
|
export function bootPlan(input) {
|
|
53
62
|
const previous = readRecord(launchStatePath(input.dataRoot));
|
|
54
63
|
const build = typeof input.build === 'string' && input.build !== '' ? input.build : null;
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { t } from "../i18n/index.js";
|
|
2
2
|
import { toolDisplay } from "./tool-display.js";
|
|
3
|
-
|
|
3
|
+
/*
|
|
4
|
+
* P14 (owner, 2026-09-25, "lo spinner thinking rimane in conteggio quando gli agenti vengono dispacciati"): a tool row is
|
|
5
|
+
* born `queued` (state.ts `tool.started`) and becomes `running` only after an approval, `streaming` only when it prints.
|
|
6
|
+
* A tool that runs silently without asking — the delegation, which waits for its sub-agents, a long read, a web fetch —
|
|
7
|
+
* stayed `queued`, was not counted as active, and the line said "thinking" with its clock running. `queued` is active
|
|
8
|
+
* now; and while delegations are running the line says how many agents are working.
|
|
9
|
+
*/
|
|
10
|
+
const ACTIVE_TOOL = new Set(['queued', 'running', 'streaming']);
|
|
11
|
+
const DELEGATION_TOOLS = new Set(['delega_sottotask']);
|
|
4
12
|
export function deriveLiveActivity(state, busySince) {
|
|
5
13
|
if (!state.running) {
|
|
6
14
|
if (typeof state.sentAt === 'number')
|
|
@@ -14,6 +22,9 @@ export function deriveLiveActivity(state, busySince) {
|
|
|
14
22
|
/* R5a S4 (E5): a retry is its own state, never "thinking": attempt 2 and later of a request the provider refused. */
|
|
15
23
|
if (isRetry(state.retry))
|
|
16
24
|
return { kind: 'retrying', startedAt: state.retry.at };
|
|
25
|
+
const delegations = state.tools.filter(row => ACTIVE_TOOL.has(row.status) && row.name !== undefined && DELEGATION_TOOLS.has(row.name)).length;
|
|
26
|
+
if (delegations > 0)
|
|
27
|
+
return { kind: 'agents', startedAt: busySince, agents: delegations };
|
|
17
28
|
const tool = [...state.tools].reverse().find(row => ACTIVE_TOOL.has(row.status));
|
|
18
29
|
if (tool)
|
|
19
30
|
return tool.name ? { kind: 'tool', startedAt: busySince, tool: toolDisplay(tool.name).label } : { kind: 'tool', startedAt: busySince };
|
|
@@ -41,5 +52,5 @@ export function retryLine(retry, nowMs) {
|
|
|
41
52
|
/** Kinds that change while you look at them: the only ones that start the motion clock (idle writes nothing, R1 E5).
|
|
42
53
|
* `retrying` is not one of them: its countdown ticks once a second, and only while the wait lasts (app.ts). */
|
|
43
54
|
export function isAnimatedActivity(kind) {
|
|
44
|
-
return kind === 'starting' || kind === 'thinking' || kind === 'writing' || kind === 'tool' || kind === 'waiting' || kind === 'compacting' || kind === 'connecting' || kind === 'updating';
|
|
55
|
+
return kind === 'starting' || kind === 'thinking' || kind === 'writing' || kind === 'tool' || kind === 'agents' || kind === 'waiting' || kind === 'compacting' || kind === 'connecting' || kind === 'updating';
|
|
45
56
|
}
|
|
@@ -27,9 +27,13 @@ export function agentTreeOverlayLines(model, input) {
|
|
|
27
27
|
const selected = selectedAgentRow(model);
|
|
28
28
|
if (!selected)
|
|
29
29
|
return [...lines, fit(t('screen.agents.empty'))];
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
/* P13: Enter watches the selected agent live; on the session in use it goes back to it (Claude Code's "Enter to view",
|
|
31
|
+
not offered on the agent already watched). */
|
|
32
|
+
const detail = selected.id === input.viewingId
|
|
33
|
+
? t('screen.agents.watchingHint')
|
|
34
|
+
: selected.id === input.currentSessionId
|
|
35
|
+
? (input.viewingId ? t('screen.agents.backHint') : input.running ? t('screen.agents.steerHint') : t('screen.agents.inspectOnly'))
|
|
36
|
+
: t('screen.agents.watchHint');
|
|
33
37
|
lines.push(fit(detail));
|
|
34
38
|
return lines;
|
|
35
39
|
}
|
|
@@ -240,6 +240,8 @@ export function approvalDialogModel(pending, input) {
|
|
|
240
240
|
const question = t(questionId in QUESTION_IDS ? questionId : 'approval.question');
|
|
241
241
|
/* Q-R3-13 (R0 decision 4): a command approval on a host with no verified sandbox says so in its title. */
|
|
242
242
|
const title = t('tool.approval.title', { label }) + (input.unsandboxed === true && action.tool === 'Bash' ? ` ${t('approval.unsandboxed')}` : '');
|
|
243
|
+
/* P13 (owner decision 8): an approval a sub-agent asks for says so, as Claude Code's does ("from a subagent"). */
|
|
244
|
+
const origin = pending.origin?.kind === 'agent' ? ` ${pending.origin.task ? t('approval.origin.agent', { task: pending.origin.task }) : t('approval.origin.agentUnnamed')}` : '';
|
|
243
245
|
const change = approvalChange(pending);
|
|
244
246
|
const ruleLine = pending.rule === PATH_BOUNDARY_RULE ? 'decided by the path boundary' : pending.rule ? `rule ${pending.rule}` : undefined;
|
|
245
247
|
const explanation = pending.explanation;
|
|
@@ -253,7 +255,7 @@ export function approvalDialogModel(pending, input) {
|
|
|
253
255
|
segments = rows;
|
|
254
256
|
}
|
|
255
257
|
return {
|
|
256
|
-
title, summary, fullPayload, expanded: input.expanded, reason: pending.reason, ...(pending.rule ? { rule: pending.rule } : {}), ...(pending.runtimeHint ? { runtimeHint: pending.runtimeHint } : {}),
|
|
258
|
+
title: title + origin, summary, fullPayload, expanded: input.expanded, reason: pending.reason, ...(pending.rule ? { rule: pending.rule } : {}), ...(pending.runtimeHint ? { runtimeHint: pending.runtimeHint } : {}),
|
|
257
259
|
...(ruleLine ? { ruleLine } : {}), segments, ...(segmentsWithheld ? { segmentsWithheld } : {}), ...(pending.explanationError ? { explanationError: pending.explanationError } : {}),
|
|
258
260
|
...(input.expanded && pending.alwaysSimulation ? { alwaysPreview: alwaysPreview(pending, pending.alwaysSimulation) } : {}),
|
|
259
261
|
question, selected, ...(change ? { change } : {}),
|
|
@@ -12,6 +12,7 @@ export function createModelPickerModel(catalog, input) {
|
|
|
12
12
|
let verified = false;
|
|
13
13
|
let notice = '';
|
|
14
14
|
let label = input.provider ?? '';
|
|
15
|
+
let engine = null;
|
|
15
16
|
let abort = null;
|
|
16
17
|
return {
|
|
17
18
|
async open() {
|
|
@@ -19,6 +20,7 @@ export function createModelPickerModel(catalog, input) {
|
|
|
19
20
|
loadError = null;
|
|
20
21
|
models = [];
|
|
21
22
|
verified = false;
|
|
23
|
+
engine = null;
|
|
22
24
|
if (!input.provider) {
|
|
23
25
|
notice = t('screen.model.chooseProviderFirst');
|
|
24
26
|
return;
|
|
@@ -33,6 +35,7 @@ export function createModelPickerModel(catalog, input) {
|
|
|
33
35
|
verified = list.verified;
|
|
34
36
|
notice = list.notice;
|
|
35
37
|
label = list.label;
|
|
38
|
+
engine = list.engine ?? null;
|
|
36
39
|
const current = input.current;
|
|
37
40
|
if (current && current.startsWith(`${input.provider}:`) && !models.some(row => row.id === current)) {
|
|
38
41
|
models = [...models, ...normalizeModels([{ id: current }], input.provider, 'configured')];
|
|
@@ -52,6 +55,8 @@ export function createModelPickerModel(catalog, input) {
|
|
|
52
55
|
error: () => loadError,
|
|
53
56
|
verified: () => verified,
|
|
54
57
|
notice: () => notice,
|
|
58
|
+
/* P17: a local engine that did not answer (the setup offers R to retry). */
|
|
59
|
+
engineDown: () => engine === 'down',
|
|
55
60
|
label: () => label,
|
|
56
61
|
};
|
|
57
62
|
}
|
|
@@ -133,6 +133,32 @@ export function providerConsentOverlay({ provider, choice }) {
|
|
|
133
133
|
foot: [{ text: t('screen.provider.consentFoot'), tone: 'dim' }],
|
|
134
134
|
};
|
|
135
135
|
}
|
|
136
|
+
export function providerLoginChoiceOverlay({ label, choice, notice = null }) {
|
|
137
|
+
const options = [{ choice: 'browser', text: t('screen.provider.loginBrowser', { provider: label }) }, { choice: 'paste-key', text: t('screen.provider.loginPasteKey') }];
|
|
138
|
+
return {
|
|
139
|
+
head: [{ text: t('screen.provider.titleNamed', { provider: label }), tone: 'title' }],
|
|
140
|
+
/* A saved key that does not work now (benched) is said here too, before the person picks a way. */
|
|
141
|
+
sections: [{ lines: [...(notice ? [{ text: notice, tone: 'plain' }] : []), ...options.map(option => ({ text: `${option.choice === choice ? '›' : ' '} ${option.text}`, tone: option.choice === choice ? 'selected' : 'accent' }))], selected: (notice ? 1 : 0) + Math.max(0, options.findIndex(option => option.choice === choice)) }],
|
|
142
|
+
foot: [{ text: t('screen.provider.loginChoiceKeys'), tone: 'dim' }],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/* The sign-in under way: the address (always shown, the browser may not open), then the code the person may paste. Only the
|
|
146
|
+
MASK of a pasted code reaches this view. */
|
|
147
|
+
export function providerOAuthOverlay({ label, url, mode, opened, phase, masked, message }) {
|
|
148
|
+
const lines = [];
|
|
149
|
+
if (mode === 'paste')
|
|
150
|
+
lines.push(t('screen.provider.oauthRemote', { provider: label }));
|
|
151
|
+
else
|
|
152
|
+
lines.push(t(opened ? 'screen.provider.oauthOpened' : 'screen.provider.oauthNotOpened', { provider: label }));
|
|
153
|
+
lines.push(url);
|
|
154
|
+
const typing = phase === 'paste' || (mode === 'paste' && phase !== 'failed');
|
|
155
|
+
if (typing)
|
|
156
|
+
lines.push(t('screen.provider.oauthPaste', { masked: masked || ' ' }));
|
|
157
|
+
if (message)
|
|
158
|
+
lines.push(message);
|
|
159
|
+
const foot = phase === 'exchanging' ? t('screen.provider.oauthExchanging') : phase === 'failed' ? t('screen.provider.oauthFailedKeys') : typing ? t('screen.provider.oauthPasteKeys') : t('screen.provider.oauthWaiting');
|
|
160
|
+
return { head: [{ text: t('screen.provider.titleNamed', { provider: label }), tone: 'title' }], sections: [{ lines: lines.map(text => ({ text, tone: 'plain' })), selected: typing ? lines.findIndex(line => line.startsWith(t('screen.provider.oauthPaste', { masked: '' }).trim())) : 0 }], foot: [{ text: foot, tone: 'dim' }] };
|
|
161
|
+
}
|
|
136
162
|
// Only the MASK reaches this view, never the typed key. The key line is the "selected" row: it is where the person types.
|
|
137
163
|
export function providerKeyOverlay({ label, remediation, benched, masked, keyTestNotice, testing, result }) {
|
|
138
164
|
const lines = [...(remediation ? [remediation] : []), ...(benched ? [benched] : [])];
|
|
@@ -4,6 +4,7 @@ import { detectTerminalCapabilities } from "./terminal-capabilities.js";
|
|
|
4
4
|
import { createTerminalSessionPlan } from "./terminal-session.js";
|
|
5
5
|
import { displayWidth, truncateDisplay, truncateMiddle, wrapDisplay } from "./text-width.js";
|
|
6
6
|
import { liveTheme } from "./theme-store.js";
|
|
7
|
+
import { DEFAULT_THEME_ACCENT } from "./theme-catalog.js";
|
|
7
8
|
export const PROJECT_TRUST_GRACE_MS = 300;
|
|
8
9
|
/* E4: paths are shown as the person types them. Control, C1, line/paragraph separators, bidi controls and
|
|
9
10
|
default-ignorable characters are shown as `\uXXXX`; a backslash is doubled only where it would read as such an escape
|
|
@@ -38,7 +39,7 @@ function resourceLine(resource) {
|
|
|
38
39
|
const where = inertProjectTrustText(resource.relativePath);
|
|
39
40
|
return resource.id === resource.relativePath ? `${resource.kind} · ${where}` : `${resource.kind} · ${inertProjectTrustText(resource.id)} · ${where}`;
|
|
40
41
|
}
|
|
41
|
-
export function projectTrustPromptModel({ assessment, nestedRepositories }) {
|
|
42
|
+
export function projectTrustPromptModel({ assessment, nestedRepositories, setupStep }) {
|
|
42
43
|
const counts = new Map();
|
|
43
44
|
for (const resource of assessment.resources)
|
|
44
45
|
counts.set(resource.kind, (counts.get(resource.kind) ?? 0) + 1);
|
|
@@ -55,6 +56,7 @@ export function projectTrustPromptModel({ assessment, nestedRepositories }) {
|
|
|
55
56
|
const firstTime = assessment.trusted || assessment.reason === 'PROJECT_UNTRUSTED';
|
|
56
57
|
return {
|
|
57
58
|
title: t(firstTime ? 'firstrun.trust.title' : 'firstrun.trust.titleChanged'),
|
|
59
|
+
...(setupStep ? { setupStep: true } : {}),
|
|
58
60
|
project: inertProjectTrustText(assessment.workspace.canonicalRoot),
|
|
59
61
|
reason: reasonText(assessment),
|
|
60
62
|
resourceSummary: assessment.resources.length ? tn('firstrun.trust.files', assessment.resources.length, { detail }) : '',
|
|
@@ -118,7 +120,7 @@ export function layoutProjectTrustPrompt(model, { columns, rows, selected, platf
|
|
|
118
120
|
}
|
|
119
121
|
return { kind: 'choice', text: truncateDisplay(`${choice.id === selected ? '>' : ' '} ${label}`, width), selected: choice.id === selected };
|
|
120
122
|
});
|
|
121
|
-
const title = truncateDisplay(t('firstrun.trust.heading', { title: model.title }), width);
|
|
123
|
+
const title = truncateDisplay(t(model.setupStep ? 'firstrun.setup.trustHeading' : 'firstrun.trust.heading', { title: model.title }), width);
|
|
122
124
|
let path = wrapDisplay(model.project, width);
|
|
123
125
|
let reason = wrapWords(model.reason, width);
|
|
124
126
|
let summary = model.resourceSummary ? wrapWords(model.resourceSummary, width) : [];
|
|
@@ -209,7 +211,7 @@ export async function runProjectTrustPrompt(input, io = {}) {
|
|
|
209
211
|
}
|
|
210
212
|
const capabilities = detectTerminalCapabilities({ stdinIsTTY: true, stdoutIsTTY: true, color: input.color !== false, env });
|
|
211
213
|
const session = createTerminalSessionPlan({ capabilities, env });
|
|
212
|
-
const theme = liveTheme(capabilities, input.theme ??
|
|
214
|
+
const theme = liveTheme(capabilities, input.theme ?? DEFAULT_THEME_ACCENT).theme;
|
|
213
215
|
const model = projectTrustPromptModel(input);
|
|
214
216
|
const ids = model.choices.map(choice => choice.id);
|
|
215
217
|
const sizeStore = createTerminalSizeStore(stdout, { rows: 24, columns: 80 });
|
|
@@ -3,6 +3,7 @@ import { buildAgentTree } from "../runtime/agent-tree.js";
|
|
|
3
3
|
import { createPermissionEngine } from "../security/permission-engine.js";
|
|
4
4
|
import { createApprovalCoordinator, decisionFromChoice, decisionFromSource } from "./approval.js";
|
|
5
5
|
import { createTuiEventAdapter, denialFromEngine, isolationOf } from "./event-adapter.js";
|
|
6
|
+
import { createDescendantApprovals } from "./descendant-approvals.js";
|
|
6
7
|
import { simulatePermission } from "../security/permission-explanation.js";
|
|
7
8
|
import { KERNEL_RUN_TEXT } from "../i18n/kernel-map.js";
|
|
8
9
|
import { permissionActionFromApproval } from "../security/from-approval.js";
|
|
@@ -33,6 +34,11 @@ export function createTuiSessionController({ runtime, projectRoot, model, mode,
|
|
|
33
34
|
beforeDeny: (rid, decision) => { const target = denialTargets.get(rid); if (target)
|
|
34
35
|
adapter.noteDenial(target.toolCallId, denialFromEngine(decision, { rule: () => simulatePermission({ rules, action: target.action, projectRoot }).explanation.rule ?? null })); } });
|
|
35
36
|
const adapter = createTuiEventAdapter(secretValuesFromEnvironment(process.env));
|
|
37
|
+
/* P13: the approvals the current run's sub-agents ask for (descendant-approvals.ts), watched while that run is open. */
|
|
38
|
+
const descendants = createDescendantApprovals({ runtime, secretValues: secretValuesFromEnvironment(process.env),
|
|
39
|
+
onRequired: (origin, event) => requestApproval(origin.sessionId, event, origin),
|
|
40
|
+
onResolved: (_origin, requestId) => approvalResolved(requestId),
|
|
41
|
+
onError: error => developmentLogError('descendants.failure', error, { sessionId: current }, 'tui-controller') });
|
|
36
42
|
let unsubscribe = () => { };
|
|
37
43
|
let closed = false;
|
|
38
44
|
let current = null;
|
|
@@ -143,10 +149,13 @@ export function createTuiSessionController({ runtime, projectRoot, model, mode,
|
|
|
143
149
|
openRun = event.runId ? { runId: event.runId } : {};
|
|
144
150
|
if (replayingHistory)
|
|
145
151
|
event = { ...event, history: true };
|
|
152
|
+
else
|
|
153
|
+
descendants.start(id);
|
|
146
154
|
}
|
|
147
155
|
else if (event.type === 'run.completed' || event.type === 'run.failed' || event.type === 'run.cancelled') {
|
|
148
156
|
openRun = null;
|
|
149
157
|
runEpoch++;
|
|
158
|
+
descendants.stop();
|
|
150
159
|
}
|
|
151
160
|
onEvent(event);
|
|
152
161
|
bypassWhy(event);
|
|
@@ -161,14 +170,8 @@ export function createTuiSessionController({ runtime, projectRoot, model, mode,
|
|
|
161
170
|
else
|
|
162
171
|
requestApproval(id, event);
|
|
163
172
|
}
|
|
164
|
-
else if (event.type === 'approval.resolved')
|
|
165
|
-
|
|
166
|
-
approvalTools.delete(event.requestId);
|
|
167
|
-
answeredApprovals.add(event.requestId);
|
|
168
|
-
if (answeredApprovals.size > 256)
|
|
169
|
-
answeredApprovals.delete(answeredApprovals.values().next().value);
|
|
170
|
-
onApproval?.(null);
|
|
171
|
-
}
|
|
173
|
+
else if (event.type === 'approval.resolved')
|
|
174
|
+
approvalResolved(event.requestId);
|
|
172
175
|
else if (allowDrain && (event.type === 'run.completed' || event.type === 'run.failed') && !pendingSteer && !cancelling)
|
|
173
176
|
requestQueuedDrain(id);
|
|
174
177
|
}
|
|
@@ -214,7 +217,15 @@ export function createTuiSessionController({ runtime, projectRoot, model, mode,
|
|
|
214
217
|
const replayedApprovals = new Map();
|
|
215
218
|
const answeredApprovals = new Set();
|
|
216
219
|
const approvalTools = new Map();
|
|
217
|
-
function
|
|
220
|
+
function approvalResolved(requestId) {
|
|
221
|
+
replayedApprovals.delete(requestId);
|
|
222
|
+
approvalTools.delete(requestId);
|
|
223
|
+
answeredApprovals.add(requestId);
|
|
224
|
+
if (answeredApprovals.size > 256)
|
|
225
|
+
answeredApprovals.delete(answeredApprovals.values().next().value);
|
|
226
|
+
onApproval?.(null);
|
|
227
|
+
}
|
|
228
|
+
function requestApproval(id, event, origin) {
|
|
218
229
|
const action = permissionActionFromApproval(event.payload);
|
|
219
230
|
const epoch = runEpoch;
|
|
220
231
|
answeredApprovals.delete(event.requestId);
|
|
@@ -222,12 +233,12 @@ export function createTuiSessionController({ runtime, projectRoot, model, mode,
|
|
|
222
233
|
denialTargets.set(event.requestId, { toolCallId: event.toolCallId, action });
|
|
223
234
|
void coordinator.request(id, event.requestId, action).finally(() => denialTargets.delete(event.requestId)).then(result => {
|
|
224
235
|
if (result.pending) {
|
|
225
|
-
if (answeredApprovals.has(event.requestId) || epoch !== runEpoch || current !== id)
|
|
236
|
+
if (answeredApprovals.has(event.requestId) || epoch !== runEpoch || (origin ? !descendants.watching().includes(id) : current !== id))
|
|
226
237
|
return;
|
|
227
238
|
if (event.toolCallId)
|
|
228
239
|
approvalTools.set(event.requestId, event.toolCallId);
|
|
229
240
|
const runtimeHint = event.payload.trifecta === true ? 'trifecta' : undefined;
|
|
230
|
-
onApproval?.({ requestId: event.requestId, sessionId: id, action: result.action, rawPayload: event.payload, reason: result.reason, ...(result.rule ? { rule: result.rule } : {}), ...(runtimeHint ? { runtimeHint } : {}), ...explainApproval({ rules, action: result.action, projectRoot, persists: Boolean(paths) }) });
|
|
241
|
+
onApproval?.({ requestId: event.requestId, sessionId: id, ...(origin ? { origin: { kind: 'agent', task: origin.task } } : {}), action: result.action, rawPayload: event.payload, reason: result.reason, ...(result.rule ? { rule: result.rule } : {}), ...(runtimeHint ? { runtimeHint } : {}), ...explainApproval({ rules, action: result.action, projectRoot, persists: Boolean(paths) }) });
|
|
231
242
|
}
|
|
232
243
|
else {
|
|
233
244
|
/* R3 gate E4 (Q-R3-3): why the engine let it run, on the tool's row. */
|
|
@@ -524,6 +535,7 @@ export function createTuiSessionController({ runtime, projectRoot, model, mode,
|
|
|
524
535
|
let openRun = null;
|
|
525
536
|
function attach(id, from = 0, history = false) {
|
|
526
537
|
unsubscribe();
|
|
538
|
+
descendants.stop();
|
|
527
539
|
adapter.bindSession(id);
|
|
528
540
|
current = id;
|
|
529
541
|
openRun = null;
|
|
@@ -564,6 +576,7 @@ export function createTuiSessionController({ runtime, projectRoot, model, mode,
|
|
|
564
576
|
if (current === id && openRun === open && outcome === 'running') {
|
|
565
577
|
for (const event of waiting)
|
|
566
578
|
requestApproval(id, event);
|
|
579
|
+
descendants.start(id);
|
|
567
580
|
return;
|
|
568
581
|
}
|
|
569
582
|
if (current !== id || openRun !== open || typeof outcome !== 'string' || outcome === 'running' || outcome === 'unknown')
|
|
@@ -774,5 +787,5 @@ export function createTuiSessionController({ runtime, projectRoot, model, mode,
|
|
|
774
787
|
finally {
|
|
775
788
|
cancelling = false;
|
|
776
789
|
} }, close() { if (closed)
|
|
777
|
-
return; closed = true; developmentLog('controller.close', { sessionId: current, queueCount: queuedEntries.length }, 'info', 'tui-controller'); unsubscribe(); } };
|
|
790
|
+
return; closed = true; developmentLog('controller.close', { sessionId: current, queueCount: queuedEntries.length }, 'info', 'tui-controller'); descendants.stop(); unsubscribe(); } };
|
|
778
791
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* P17 (owner, 2026-09-25: "mini procedura guidata semplicissima al fresh install"; decisions of 2026-09-26). The first launch
|
|
3
|
+
* chains the windows that already exist: trust (the screen before anything loads) → provider → key or local engine → model,
|
|
4
|
+
* one window at a time with "Step N of 4"; Enter goes on, Esc skips the step, Ctrl+C quits, and what was done stays saved.
|
|
5
|
+
* Only the missing steps run: a key already usable (from the environment too) or a local engine that answers skips them; a
|
|
6
|
+
* credential that exists but is benched is explained by the first frame's line, not asked again (Hermes #113720).
|
|
7
|
+
* Read on 2026-09-26: Claude Code (welcome, theme, login, security notes, terminal setup, trust), Codex (welcome, auth,
|
|
8
|
+
* trust), Hermes (provider + model through the same picker as `hermes model`, skippable, `hermes setup` to redo), Pi (theme
|
|
9
|
+
* and analytics, providers later), OpenCode (no wizard), Gemini CLI (theme, then auth). `/setup` redoes it (Hermes'
|
|
10
|
+
* `hermes setup`). Without an interactive terminal it never runs (clig.dev: "never require a prompt").
|
|
11
|
+
*/
|
|
12
|
+
import { t } from "../i18n/index.js";
|
|
13
|
+
export const SETUP_TOTAL = 4;
|
|
14
|
+
const INDEX = { trust: 1, provider: 2, key: 3, model: 4 };
|
|
15
|
+
const NAME = { trust: 'firstrun.setup.stepName.trust', provider: 'firstrun.setup.stepName.provider', key: 'firstrun.setup.stepName.key', model: 'firstrun.setup.stepName.model' };
|
|
16
|
+
/** The line that tells where the person is in the setup; `escSkips:false` when Esc does something else there. */
|
|
17
|
+
export function setupStepLine(step, { escSkips = true } = {}) {
|
|
18
|
+
return t(escSkips ? 'firstrun.setup.stepLine' : 'firstrun.setup.stepLineOnly', { index: INDEX[step], total: SETUP_TOTAL, name: t(NAME[step]) });
|
|
19
|
+
}
|
|
20
|
+
/** The same window, with the setup's step on top (the window's own title stays under it). */
|
|
21
|
+
export function setupOverlay(content, step, options = {}) {
|
|
22
|
+
return { ...content, head: [{ text: setupStepLine(step, options), tone: 'title' }, ...(content.head ?? [])] };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Whether the first launch runs the setup, and from which step. `providerUsable`: the chosen provider has a key usable now
|
|
26
|
+
* (from the environment too) or needs none; `keyBenched`: it has a key that does not work now (explained, not asked again).
|
|
27
|
+
*/
|
|
28
|
+
export function setupStart(input) {
|
|
29
|
+
if (!input.firstLaunch)
|
|
30
|
+
return { start: false };
|
|
31
|
+
if (!input.providerChosen)
|
|
32
|
+
return { start: true, from: 'provider' };
|
|
33
|
+
if (input.keyBenched)
|
|
34
|
+
return { start: false };
|
|
35
|
+
if (!input.providerUsable)
|
|
36
|
+
return { start: true, from: 'key' };
|
|
37
|
+
if (!input.modelChosen)
|
|
38
|
+
return { start: true, from: 'model' };
|
|
39
|
+
return { start: false };
|
|
40
|
+
}
|
|
41
|
+
/** What the setup says when it ends: ready with the model, or where to pick it up again. */
|
|
42
|
+
export function setupEndLines(outcome) {
|
|
43
|
+
if (!outcome.ready)
|
|
44
|
+
return [t('firstrun.setup.skipped')];
|
|
45
|
+
return [t('firstrun.setup.ready', { model: outcome.model }), t('firstrun.setup.readyHints')];
|
|
46
|
+
}
|
|
@@ -2,6 +2,8 @@ import { t } from "../i18n/index.js";
|
|
|
2
2
|
const SLASH_COMMAND_META = [
|
|
3
3
|
{ name: 'help', usage: '' },
|
|
4
4
|
{ name: 'provider', usage: '' },
|
|
5
|
+
/* P17 (owner decision 4): the first launch's guided setup, again. */
|
|
6
|
+
{ name: 'setup', usage: '' },
|
|
5
7
|
{ name: 'model', usage: '[provider:model]' },
|
|
6
8
|
/* R5a (owner 2026-09-24): the reasoning effort of the current model, as a line of nodes. */
|
|
7
9
|
{ name: 'effort', usage: '[level]' },
|
|
@@ -55,7 +57,7 @@ export const SLASH_COMMANDS = SLASH_COMMAND_META.map(row => ({
|
|
|
55
57
|
const SLASH_GROUPS = ['session', 'turn', 'view', 'work', 'extensions'];
|
|
56
58
|
const GROUP_OF = {
|
|
57
59
|
resume: 'session', fork: 'session', rename: 'session', export: 'session', undo: 'session', history: 'session', clear: 'session', compact: 'session', exit: 'session',
|
|
58
|
-
provider: 'turn', model: 'turn', effort: 'turn', busy: 'turn', queue: 'turn', retry: 'turn', plan: 'turn', permissions: 'turn',
|
|
60
|
+
provider: 'turn', setup: 'turn', model: 'turn', effort: 'turn', busy: 'turn', queue: 'turn', retry: 'turn', plan: 'turn', permissions: 'turn',
|
|
59
61
|
help: 'view', status: 'view', context: 'view', usage: 'view', diff: 'view', copy: 'view', theme: 'view', redraw: 'view',
|
|
60
62
|
memory: 'work', notes: 'work', tasks: 'work', library: 'work', research: 'work', automations: 'work', forge: 'work',
|
|
61
63
|
mcp: 'extensions', hooks: 'extensions', plugins: 'extensions', doctor: 'extensions',
|
|
@@ -6,7 +6,9 @@ export function mixHex(a, amount, b) {
|
|
|
6
6
|
return `#${x.map((v, i) => Math.round(v * t + y[i] * (1 - t)).toString(16).padStart(2, '0')).join('')}`;
|
|
7
7
|
}
|
|
8
8
|
const theme = (id, label, accent, background) => ({ id, label, accent, background, muted: mixHex(accent, 62, background) });
|
|
9
|
-
|
|
9
|
+
/* P15 (owner, 2026-09-25, "vorrei mettere forge come tema principale di default"): Forge is the default from 0.2.0. A theme
|
|
10
|
+
the person chose stays theirs (it is in their configuration); the legacy migrations keep mapping to what they mapped to. */
|
|
11
|
+
export const DEFAULT_THEME_ACCENT = 'forge';
|
|
10
12
|
export const TALOS_THEME_ACCENTS = Object.freeze([
|
|
11
13
|
theme('calm', 'Calm', '#c08b3c', '#1e1f22'),
|
|
12
14
|
theme('forge', 'Forge', '#c98b32', '#080b11'),
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const CLI_VERSION = '0.1
|
|
1
|
+
export const CLI_VERSION = '0.2.1';
|
|
2
2
|
export const TALOS_BASE_COMMIT = '0c432153a288f64237e98403869d02d20d8fabc7';
|
|
@@ -60,6 +60,24 @@ function parseRecord(text, expectedRoot) {
|
|
|
60
60
|
throw coded('CHECKPOINT_STORE_INVALID');
|
|
61
61
|
return value;
|
|
62
62
|
}
|
|
63
|
+
const STAT_INDEX_SCHEMA = 'talos.cli.checkpoint-stat-index.v1';
|
|
64
|
+
async function loadStatIndex(indexPath) {
|
|
65
|
+
const rows = new Map();
|
|
66
|
+
try {
|
|
67
|
+
const value = JSON.parse(await readFile(indexPath, 'utf8'));
|
|
68
|
+
if (value?.schema !== STAT_INDEX_SCHEMA || !Array.isArray(value.rows))
|
|
69
|
+
return rows;
|
|
70
|
+
for (const row of value.rows) {
|
|
71
|
+
if (!Array.isArray(row) || row.length !== 6)
|
|
72
|
+
continue;
|
|
73
|
+
const [p, size, mtimeMs, ctimeMs, ref, verifiedAtMs] = row;
|
|
74
|
+
if (typeof p === 'string' && typeof ref === 'string' && /^[0-9a-f]{64}$/u.test(ref) && [size, mtimeMs, ctimeMs, verifiedAtMs].every(n => typeof n === 'number' && Number.isFinite(n)))
|
|
75
|
+
rows.set(p, { size, mtimeMs, ctimeMs, ref, verifiedAtMs });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch { /* missing or torn: an empty index, every file is read again */ }
|
|
79
|
+
return rows;
|
|
80
|
+
}
|
|
63
81
|
function dedupeEvidence(rows) { const byKey = new Map(); for (const row of rows)
|
|
64
82
|
byKey.set(`${row.path}\0${row.beforeHash ?? ''}\0${row.afterHash ?? ''}`, row); return [...byKey.values()]; }
|
|
65
83
|
export function createCheckpointStore({ rootDir, projectRoot, limits = CHECKPOINT_LIMITS }) {
|
|
@@ -85,7 +103,20 @@ export function createCheckpointStore({ rootDir, projectRoot, limits = CHECKPOIN
|
|
|
85
103
|
} }
|
|
86
104
|
async function replace(record) { const x = await init(); if (record.canonicalRoot !== x.canonicalRoot)
|
|
87
105
|
throw coded('CHECKPOINT_WORKSPACE_MISMATCH'); await atomicWrite(await recordPath(record.id), `${JSON.stringify(record)}\n`); }
|
|
88
|
-
|
|
106
|
+
/* P18: the stat index of the last capture (checkpoint.ts StatCache), kept in memory and in the workspace's folder so a new
|
|
107
|
+
`talos -p` process starts from it too. It is only a cache: unreadable means empty (every file read again), and a
|
|
108
|
+
failed save costs speed on the next turn, never a checkpoint. */
|
|
109
|
+
let statRows = null;
|
|
110
|
+
async function capture() {
|
|
111
|
+
const x = await init();
|
|
112
|
+
const indexPath = path.join(x.workspaceDir, 'stat-index.json');
|
|
113
|
+
statRows ??= await loadStatIndex(indexPath);
|
|
114
|
+
const next = new Map();
|
|
115
|
+
const snapshot = await captureWorkspaceSnapshot({ projectRoot: x.canonicalRoot, blobRoot, limits, statCache: { previous: statRows, next } });
|
|
116
|
+
statRows = next;
|
|
117
|
+
await atomicWrite(indexPath, `${JSON.stringify({ schema: STAT_INDEX_SCHEMA, rows: [...next].map(([p, r]) => [p, r.size, r.mtimeMs, r.ctimeMs, r.ref, r.verifiedAtMs]) })}\n`).catch(() => { });
|
|
118
|
+
return snapshot;
|
|
119
|
+
}
|
|
89
120
|
async function list() { const x = await init(); let names = []; try {
|
|
90
121
|
names = await readdir(x.recordsDir);
|
|
91
122
|
}
|