@giovannijecha/jecode 0.8.5 → 0.8.7
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/README.md +36 -9
- package/assets/tokenizers/LICENSE +21 -0
- package/assets/tokenizers/o200k-base.tiktoken.gz +0 -0
- package/dist/cli-info.js +8 -14
- package/dist/commands.js +7 -3
- package/dist/config.js +47 -53
- package/dist/context/automatic.js +16 -1
- package/dist/context/budget.js +5 -2
- package/dist/context/compactor.js +91 -37
- package/dist/context/diagnostics.js +108 -0
- package/dist/context/lifetime.js +18 -0
- package/dist/context/manager.js +67 -0
- package/dist/context/manual.js +11 -10
- package/dist/context/measurement.js +75 -0
- package/dist/context/policy.js +13 -10
- package/dist/context/request-observation.js +46 -0
- package/dist/context/request-projection.js +8 -34
- package/dist/context/request.js +22 -0
- package/dist/context/tokenizer/bpe.js +68 -0
- package/dist/context/tokenizer/o200k.js +54 -0
- package/dist/context/tokenizer/vocabulary.js +34 -0
- package/dist/controller-request.js +47 -45
- package/dist/controller.js +13 -2
- package/dist/credential-commands.js +3 -3
- package/dist/input-boundary.js +0 -62
- package/dist/launch.js +13 -9
- package/dist/model-command.js +7 -17
- package/dist/oauth-result-page.js +68 -0
- package/dist/openai-account-command.js +14 -12
- package/dist/openai-account.js +7 -5
- package/dist/openai-oauth-callback.js +15 -54
- package/dist/openai-oauth-tokens.js +9 -7
- package/dist/openai-oauth.js +8 -6
- package/dist/permission-command.js +9 -24
- package/dist/permissions.js +10 -8
- package/dist/provider-commands.js +1 -33
- package/dist/provider-errors.js +1 -10
- package/dist/provider-label.js +3 -10
- package/dist/providers/anthropic-wire.js +1 -1
- package/dist/providers/anthropic.js +3 -2
- package/dist/providers/index.js +1 -5
- package/dist/providers/input-measurement.js +85 -0
- package/dist/providers/ollama-context.js +42 -0
- package/dist/providers/ollama-endpoint.js +7 -34
- package/dist/providers/ollama-wire.js +1 -1
- package/dist/providers/ollama.js +15 -147
- package/dist/providers/openai-codex.js +7 -4
- package/dist/providers/openai-stream.js +20 -8
- package/dist/providers/openai-summary.js +37 -0
- package/dist/providers/openai-wire.js +1 -1
- package/dist/providers/openai.js +3 -1
- package/dist/sessions/bucket.js +55 -0
- package/dist/sessions/catalog-io.js +162 -0
- package/dist/sessions/catalog.js +3 -1
- package/dist/sessions/codec-messages.js +122 -0
- package/dist/sessions/codec-transcript.js +93 -0
- package/dist/sessions/codec-values.js +52 -0
- package/dist/sessions/codec.js +4 -257
- package/dist/sessions/files.js +158 -0
- package/dist/sessions/load.js +90 -0
- package/dist/sessions/snapshot.js +33 -0
- package/dist/sessions/store.js +42 -461
- package/dist/settings.js +17 -16
- package/dist/start.js +36 -53
- package/dist/timeline.js +2 -0
- package/dist/tools/file-read.js +192 -0
- package/dist/tools/file-summary.js +9 -0
- package/dist/tools/{fs.js → file-write.js} +5 -193
- package/dist/tools/glob.js +107 -0
- package/dist/tools/index.js +2 -1
- package/dist/tools/search.js +1 -105
- package/dist/tui/activity.js +1 -1
- package/dist/tui/app-input.js +34 -17
- package/dist/tui/app-workflows.js +13 -361
- package/dist/tui/app.js +55 -28
- package/dist/tui/approve.js +3 -2
- package/dist/tui/blocks.js +1 -2
- package/dist/tui/command-workflow.js +112 -0
- package/dist/tui/components/command-menu.js +7 -10
- package/dist/tui/components/menu.js +82 -43
- package/dist/tui/components/messages.js +16 -9
- package/dist/tui/components/status.js +1 -1
- package/dist/tui/components/tool-evidence.js +107 -0
- package/dist/tui/components/tool-motion.js +32 -0
- package/dist/tui/components/tool.js +48 -202
- package/dist/tui/frame.js +8 -3
- package/dist/tui/help.js +3 -2
- package/dist/tui/picker-layout.js +44 -0
- package/dist/tui/picker.js +7 -71
- package/dist/tui/screen.js +7 -0
- package/dist/tui/session-view.js +0 -1
- package/dist/tui/tool-details.js +135 -0
- package/dist/tui/transcript-grammar.js +1 -1
- package/dist/tui/transcript-view.js +30 -112
- package/dist/tui/turn-workflow.js +235 -0
- package/dist/tui/turn.js +7 -140
- package/dist/tui/view.js +4 -4
- package/dist/tui/workflow-types.js +2 -0
- package/dist/ui/render.js +0 -4
- package/package.json +16 -12
- package/dist/batch-view.js +0 -17
- package/dist/batch.js +0 -267
- package/dist/ollama-settings-command.js +0 -74
- package/dist/tui/motion.js +0 -32
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Foreground slash-command interactions and their session updates.
|
|
2
|
+
import { commandNeedsIdle, handleCommand } from "../commands.js";
|
|
3
|
+
import { compactSession } from "../context/manual.js";
|
|
4
|
+
import { resetRequestIdentity } from "../request-identity.js";
|
|
5
|
+
import { updateSettings } from "../settings.js";
|
|
6
|
+
import { selectTimeline } from "../timeline.js";
|
|
7
|
+
import { saveTranscript } from "../transcript-export.js";
|
|
8
|
+
import { transition } from "./activity.js";
|
|
9
|
+
import { cancel as cancelOpen } from "./overlay.js";
|
|
10
|
+
export function commandWorkflow(options, resetCompaction, resetInput) {
|
|
11
|
+
const { session, state, permissions, feedback } = options;
|
|
12
|
+
const choose = (picker) => new Promise((resolve) => {
|
|
13
|
+
state.command?.control.signal.throwIfAborted();
|
|
14
|
+
state.open = { picker, settle: resolve };
|
|
15
|
+
options.render();
|
|
16
|
+
});
|
|
17
|
+
async function command(text) {
|
|
18
|
+
if (state.activity !== undefined && commandNeedsIdle(text))
|
|
19
|
+
return;
|
|
20
|
+
const activity = options.startActivity("command", `Running ${text.split(/\s+/)[0]}`);
|
|
21
|
+
if (activity === undefined)
|
|
22
|
+
return;
|
|
23
|
+
const status = (label) => transition(activity, label);
|
|
24
|
+
try {
|
|
25
|
+
const outcome = await handleCommand(text, session, {
|
|
26
|
+
emit: options.commandNotice,
|
|
27
|
+
signal: activity.control.signal,
|
|
28
|
+
showHelp: () => new Promise((resolve) => {
|
|
29
|
+
activity.control.signal.throwIfAborted();
|
|
30
|
+
state.open = { help: true, settle: resolve };
|
|
31
|
+
options.render();
|
|
32
|
+
}),
|
|
33
|
+
choose,
|
|
34
|
+
dismiss: () => {
|
|
35
|
+
state.open = state.open === undefined ? undefined : cancelOpen(state.open);
|
|
36
|
+
options.render();
|
|
37
|
+
},
|
|
38
|
+
type: (field) => new Promise((resolve) => {
|
|
39
|
+
activity.control.signal.throwIfAborted();
|
|
40
|
+
state.open = { field, settle: resolve };
|
|
41
|
+
options.render();
|
|
42
|
+
}),
|
|
43
|
+
status: (said) => {
|
|
44
|
+
status(said ?? activity.label);
|
|
45
|
+
options.render();
|
|
46
|
+
},
|
|
47
|
+
reset: async () => {
|
|
48
|
+
await session.persistence?.reset();
|
|
49
|
+
resetRequestIdentity(session);
|
|
50
|
+
resetCompaction();
|
|
51
|
+
state.past.length = 0;
|
|
52
|
+
permissions.reset();
|
|
53
|
+
options.replaceTranscript([]);
|
|
54
|
+
state.committedNodeId = 0;
|
|
55
|
+
},
|
|
56
|
+
permissions,
|
|
57
|
+
exportTranscript: () => saveTranscript(options.transcriptRoot, structuredClone(state.blocks)),
|
|
58
|
+
saveSettings: async (patch) => {
|
|
59
|
+
await updateSettings(patch);
|
|
60
|
+
},
|
|
61
|
+
refreshSettings: options.refreshSettings,
|
|
62
|
+
timeline: async () => {
|
|
63
|
+
const selected = await selectTimeline(session, choose);
|
|
64
|
+
if (!selected)
|
|
65
|
+
return "unchanged";
|
|
66
|
+
resetCompaction();
|
|
67
|
+
options.replaceTranscript();
|
|
68
|
+
return session.conversation.activeNodeId === state.committedNodeId
|
|
69
|
+
? "unchanged"
|
|
70
|
+
: "selected";
|
|
71
|
+
},
|
|
72
|
+
compact: async () => {
|
|
73
|
+
if (session.conversation.activeNodeId !== state.committedNodeId) {
|
|
74
|
+
return "branch-pending";
|
|
75
|
+
}
|
|
76
|
+
const result = await compactSession(session, {
|
|
77
|
+
signal: activity.control.signal,
|
|
78
|
+
onStatus: (said) => {
|
|
79
|
+
status(said ?? activity.label);
|
|
80
|
+
options.render();
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
if (result === "compacted")
|
|
84
|
+
resetCompaction();
|
|
85
|
+
return result;
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
if (outcome === "exit")
|
|
89
|
+
state.closeWhenIdle = true;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
// Command cancellation is already visible through the dock closing and
|
|
93
|
+
// the activity ending. Keep it silent instead of replacing the footer
|
|
94
|
+
// with a redundant warning.
|
|
95
|
+
if (!activity.control.signal.aborted) {
|
|
96
|
+
feedback.show({
|
|
97
|
+
text: error.message,
|
|
98
|
+
tone: "error",
|
|
99
|
+
timeoutMs: 6_000,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
// Access can change while a turn is running. Discard even an observation
|
|
105
|
+
// from an in-flight response that used the previous connection.
|
|
106
|
+
if (text.trim().split(/\s+/)[0] === "/providers")
|
|
107
|
+
resetInput();
|
|
108
|
+
options.finishActivity(activity);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return command;
|
|
112
|
+
}
|
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { renderMenu } from "./menu.js";
|
|
2
2
|
const MAX_ROWS = 4;
|
|
3
3
|
export function renderCommandMenu(commands, selected, width, pal) {
|
|
4
4
|
const selectedIndex = selected === undefined ? 0 : Math.max(0, Math.min(commands.length - 1, selected));
|
|
5
|
-
|
|
6
|
-
const shown = commands.slice(first, last);
|
|
7
|
-
if (shown.length === 0)
|
|
5
|
+
if (commands.length === 0)
|
|
8
6
|
return { rows: [], right: "" };
|
|
7
|
+
const menu = renderMenu(commands.map((command, index) => ({
|
|
8
|
+
label: `/${command.name}`, selected: selectedIndex === index,
|
|
9
|
+
})), width, pal, { maxRows: MAX_ROWS + 2, visible: MAX_ROWS });
|
|
9
10
|
return {
|
|
10
|
-
rows:
|
|
11
|
-
|
|
12
|
-
description: command.blurb,
|
|
13
|
-
selected: selectedIndex === first + index,
|
|
14
|
-
})), width, pal),
|
|
15
|
-
right: commands.length > shown.length ? `${first + 1}–${last} / ${commands.length}` : "",
|
|
11
|
+
rows: menu.rows,
|
|
12
|
+
right: commands.length > menu.last - menu.first ? `${menu.first + 1}–${menu.last} / ${commands.length}` : "",
|
|
16
13
|
};
|
|
17
14
|
}
|
|
18
15
|
export function commandMenuLimit() {
|
|
@@ -1,12 +1,54 @@
|
|
|
1
|
-
//
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
// Ribbon rows and bounded overflow recovery shared by completion and selectors.
|
|
2
|
+
import { row } from "../../ui/render.js";
|
|
3
|
+
import { terminalText } from "../../ui/terminal-text.js";
|
|
4
|
+
import { elide, textWidth, wrapText } from "../../ui/width.js";
|
|
4
5
|
export function renderMenuRows(entries, width, pal) {
|
|
6
|
+
return entries.map((entry) => {
|
|
7
|
+
const right = summary(entry, width);
|
|
8
|
+
return row(width, [
|
|
9
|
+
{ text: entry.selected ? "● " : " ", fg: pal.focus },
|
|
10
|
+
{ text: entry.label, fg: entry.selected ? pal.ink.bright : pal.ink.muted, bold: entry.selected },
|
|
11
|
+
], right === "" ? [] : [{ text: right, fg: entry.selected ? pal.focus : pal.ink.dim }], entry.selected ? pal.surface.subtle : undefined);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
/** A missing palette measures the window without producing styled rows. */
|
|
15
|
+
export function renderMenu(entries, width, pal, options) {
|
|
16
|
+
const room = Math.max(0, options.maxRows);
|
|
17
|
+
if (room === 0)
|
|
18
|
+
return { rows: [], first: 0, last: 0 };
|
|
19
|
+
const detailLimit = options.overflow === "truncate" ? 0 : Math.min(2, room - 1);
|
|
20
|
+
let detailRows = 0;
|
|
21
|
+
// Measure every possible selection so revealing clipped identity and values
|
|
22
|
+
// does not move the dock when the selected row changes.
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
detailRows = Math.max(detailRows, detailLines(entry, width, detailLimit).length);
|
|
25
|
+
if (detailRows === detailLimit)
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
const visible = Math.min(options.visible ?? 6, room - detailRows);
|
|
29
|
+
const at = Math.max(0, entries.findIndex((entry) => entry.selected));
|
|
30
|
+
const { first, last } = menuWindow(entries.length, at, visible);
|
|
31
|
+
if (pal === undefined)
|
|
32
|
+
return { rows: [], first, last };
|
|
5
33
|
if (entries.length === 0)
|
|
34
|
+
return {
|
|
35
|
+
rows: [row(width, [{ text: "No matches. Change the filter.", fg: pal.ink.muted }])], first, last,
|
|
36
|
+
};
|
|
37
|
+
const active = entries.find((entry) => entry.selected);
|
|
38
|
+
const lines = detailLines(active, width, detailRows);
|
|
39
|
+
return {
|
|
40
|
+
rows: [
|
|
41
|
+
...renderMenuRows(entries.slice(first, last), width, pal),
|
|
42
|
+
...Array.from({ length: detailRows }, (_, index) => lines[index] === undefined ? ""
|
|
43
|
+
: row(width, [{ text: " " + lines[index], fg: pal.ink.muted }])),
|
|
44
|
+
], first, last,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function detailLines(entry, width, maxRows) {
|
|
48
|
+
if (entry === undefined || maxRows <= 0)
|
|
6
49
|
return [];
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
return entries.map((entry) => renderEntry(entry, labelWidth, width, pal));
|
|
50
|
+
const room = Math.max(1, width - 2);
|
|
51
|
+
return menuText(clippedParts(entry, width).join(" · "), room, maxRows);
|
|
10
52
|
}
|
|
11
53
|
export function menuWindow(length, selected, visible) {
|
|
12
54
|
const count = Math.max(0, length);
|
|
@@ -15,42 +57,39 @@ export function menuWindow(length, selected, visible) {
|
|
|
15
57
|
const first = Math.max(0, Math.min(at - Math.floor(room / 2), count - room));
|
|
16
58
|
return { first, last: Math.min(count, first + room) };
|
|
17
59
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
60
|
+
/** Wrap untrusted menu copy, marking a bounded final row when it omits text. */
|
|
61
|
+
export function menuText(text, width, maxRows) {
|
|
62
|
+
if (text === "" || maxRows <= 0)
|
|
63
|
+
return [];
|
|
64
|
+
const lines = wrapText(terminalText(text), Math.max(1, width));
|
|
65
|
+
return lines.slice(0, maxRows).map((line, index) => index === maxRows - 1 && lines.length > maxRows ? elide(line + " …", width) : line);
|
|
66
|
+
}
|
|
67
|
+
function fullSummary(entry) {
|
|
68
|
+
return [entry.hint, displayedValue(entry)].filter(Boolean).join(" · ");
|
|
69
|
+
}
|
|
70
|
+
function summary(entry, width) {
|
|
71
|
+
const room = Math.max(1, Math.floor(width * 0.42));
|
|
72
|
+
const value = terminalText(displayedValue(entry));
|
|
73
|
+
const hint = terminalText(entry.hint ?? "");
|
|
74
|
+
if (value === "" || hint === "")
|
|
75
|
+
return elide(value || hint, room);
|
|
76
|
+
// Keep the current policy visible even when its remembered-scope hint is long.
|
|
77
|
+
const hintRoom = room - textWidth(value) - 3;
|
|
78
|
+
return hintRoom <= 0 ? elide(value, room) : `${elide(hint, hintRoom)} · ${value}`;
|
|
79
|
+
}
|
|
80
|
+
function displayedValue(entry) {
|
|
81
|
+
if (entry.value === undefined)
|
|
82
|
+
return "";
|
|
83
|
+
return entry.selected && entry.adjustable ? `‹ ${entry.value} ›` : entry.value;
|
|
84
|
+
}
|
|
85
|
+
function clippedParts(entry, width) {
|
|
86
|
+
// Reserve the stepper's width for every peer, so moving selection cannot
|
|
87
|
+
// make the shared detail area appear or disappear.
|
|
88
|
+
const stable = entry.adjustable ? { ...entry, selected: true } : entry;
|
|
89
|
+
const right = summary(stable, width);
|
|
90
|
+
const leftRoom = Math.max(0, width - 2 - (right === "" ? 0 : textWidth(right) + 1));
|
|
91
|
+
return [
|
|
92
|
+
...(textWidth(terminalText(entry.label)) > leftRoom ? [entry.label] : []),
|
|
93
|
+
...(right !== terminalText(fullSummary(stable)) ? [fullSummary(stable)] : []),
|
|
28
94
|
];
|
|
29
|
-
if (width > 40 && entry.description !== undefined) {
|
|
30
|
-
const gap = Math.max(2, labelWidth - primaryWidth(entry));
|
|
31
|
-
primary.push({
|
|
32
|
-
text: `${" ".repeat(gap)}${entry.description}`,
|
|
33
|
-
fg: entry.selected ? pal.ink.bright : pal.ink.muted,
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
const value = entry.value === undefined
|
|
37
|
-
? undefined
|
|
38
|
-
: entry.selected && entry.adjustable === true
|
|
39
|
-
? `‹ ${entry.value} ›`
|
|
40
|
-
: entry.value;
|
|
41
|
-
const summary = [entry.hint, value]
|
|
42
|
-
.filter((part) => part !== undefined)
|
|
43
|
-
.join(" · ");
|
|
44
|
-
const rightColor = entry.selected ? pal.focus : pal.ink.muted;
|
|
45
|
-
const right = summary === "" || (width <= 40 && entry.value === undefined)
|
|
46
|
-
? []
|
|
47
|
-
: [{
|
|
48
|
-
text: elide(summary, Math.max(1, Math.floor(entry.value === undefined ? width / 4 : width * 0.45))),
|
|
49
|
-
fg: rightColor,
|
|
50
|
-
bold: entry.selected || undefined,
|
|
51
|
-
}];
|
|
52
|
-
return row(width, primary, right);
|
|
53
|
-
}
|
|
54
|
-
function primaryWidth(entry) {
|
|
55
|
-
return plainLen([{ text: entry.label }]);
|
|
56
95
|
}
|
|
@@ -1,25 +1,32 @@
|
|
|
1
1
|
import { trailingText } from "../../text-boundary.js";
|
|
2
|
-
import { blank, row } from "../../ui/render.js";
|
|
2
|
+
import { blank, hasColor, row } from "../../ui/render.js";
|
|
3
3
|
import { markdown } from "../../ui/markdown.js";
|
|
4
|
+
import { terminalText } from "../../ui/terminal-text.js";
|
|
5
|
+
import { splitByCells } from "../../ui/width.js";
|
|
4
6
|
import { transcriptLead, transcriptWidth } from "../transcript-grammar.js";
|
|
5
7
|
export const REASONING_PREVIEW_ROWS = 3;
|
|
6
8
|
const MIN_REASONING_PREVIEW_CHARS = 4_096;
|
|
7
9
|
const REASONING_PREVIEW_OVERSCAN = 12;
|
|
8
10
|
export function renderUser(block, width, pal) {
|
|
9
11
|
const inner = transcriptWidth(width);
|
|
10
|
-
const content =
|
|
12
|
+
const content = terminalText(block.text, { multiline: true }).split("\n")
|
|
13
|
+
.flatMap((line) => splitByCells(line, inner));
|
|
11
14
|
return [
|
|
12
15
|
"",
|
|
13
|
-
|
|
14
|
-
...content.map((line
|
|
15
|
-
...transcriptLead(width,
|
|
16
|
-
|
|
17
|
-
: undefined),
|
|
18
|
-
...line.segs,
|
|
16
|
+
userEdge(width, pal, "▄"),
|
|
17
|
+
...content.map((line) => row(width, [
|
|
18
|
+
...transcriptLead(width),
|
|
19
|
+
{ text: line.text, fg: pal.ink.fg },
|
|
19
20
|
], [], pal.surface.subtle)),
|
|
20
|
-
|
|
21
|
+
userEdge(width, pal, "▀"),
|
|
21
22
|
];
|
|
22
23
|
}
|
|
24
|
+
/** Half-cell colour keeps the surface light without moving surrounding text. */
|
|
25
|
+
function userEdge(width, pal, half) {
|
|
26
|
+
if (!hasColor())
|
|
27
|
+
return blank(width, pal.surface.subtle);
|
|
28
|
+
return row(width, [{ text: half.repeat(width), fg: pal.surface.subtle }]);
|
|
29
|
+
}
|
|
23
30
|
export function renderAnswer(block, width, pal) {
|
|
24
31
|
return [
|
|
25
32
|
"",
|
|
@@ -19,7 +19,7 @@ export function renderStatus(info, pal) {
|
|
|
19
19
|
];
|
|
20
20
|
return withUnseen([
|
|
21
21
|
...active,
|
|
22
|
-
{ text: " · esc to interrupt", fg: pal.ink.dim, optional: true },
|
|
22
|
+
...(info.interruptible === false ? [] : [{ text: " · esc to interrupt", fg: pal.ink.dim, optional: true }]),
|
|
23
23
|
], info.unseen, pal);
|
|
24
24
|
}
|
|
25
25
|
if (info.feedback !== undefined) {
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Selection and rendering of retained tool evidence. Never mutates its source.
|
|
2
|
+
import { row } from "../../ui/render.js";
|
|
3
|
+
import { graphemeCeiling, graphemeFloor } from "../../text-boundary.js";
|
|
4
|
+
import { transcriptLead } from "../transcript-grammar.js";
|
|
5
|
+
// Native exception headings may include a Node error code before the message.
|
|
6
|
+
const ERROR_DIAGNOSTIC = /^\s*\w*Error(?:\s+\[[^\]]+\])?\s*:|\berror\b\s*:|AssertionError/i;
|
|
7
|
+
export function toolEvidence(block) {
|
|
8
|
+
const all = block.body ?? [];
|
|
9
|
+
const changed = [];
|
|
10
|
+
let added = 0, removed = 0, regions = 0, context = 3;
|
|
11
|
+
for (const detail of all) {
|
|
12
|
+
if (detail.kind === "keep") {
|
|
13
|
+
context++;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (detail.kind !== "add" && detail.kind !== "del") {
|
|
17
|
+
context = 3;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (context >= 3)
|
|
21
|
+
regions++;
|
|
22
|
+
context = 0;
|
|
23
|
+
if (detail.kind === "add")
|
|
24
|
+
added++;
|
|
25
|
+
else
|
|
26
|
+
removed++;
|
|
27
|
+
changed.push(detail);
|
|
28
|
+
}
|
|
29
|
+
const counts = changed.length === 0 ? "" : `+${added} −${removed}`;
|
|
30
|
+
const summary = counts === "" ? "" : `${counts} · ${regions} ${regions === 1 ? "region" : "regions"}`;
|
|
31
|
+
if (all.length === 0)
|
|
32
|
+
return { details: [], counts, summary, note: "" };
|
|
33
|
+
if (block.expanded)
|
|
34
|
+
return { details: all, counts, summary, note: "ctrl+o collapse" };
|
|
35
|
+
if (changed.length > 0)
|
|
36
|
+
return {
|
|
37
|
+
details: changed.length <= 6 ? changed : [
|
|
38
|
+
...changed.slice(0, 3),
|
|
39
|
+
{ kind: "gap", text: `… ${changed.length - 6} more changed ${changed.length === 7 ? "line" : "lines"}` },
|
|
40
|
+
...changed.slice(-3),
|
|
41
|
+
], counts, summary, note: "ctrl+o full source",
|
|
42
|
+
};
|
|
43
|
+
const diagnostic = block.tone === "fail"
|
|
44
|
+
? all.find((detail) => ERROR_DIAGNOSTIC.test(detail.text))
|
|
45
|
+
?? all.find((detail) => /^not ok\b|^\s*[×✖]/i.test(detail.text)) : undefined;
|
|
46
|
+
let details = all.slice(-4);
|
|
47
|
+
if (diagnostic !== undefined && !details.includes(diagnostic))
|
|
48
|
+
details = [diagnostic, ...all.slice(-3)];
|
|
49
|
+
const hidden = all.length - details.length;
|
|
50
|
+
return {
|
|
51
|
+
details, counts, summary,
|
|
52
|
+
note: hidden > 0 ? `${hidden} ${diagnostic === undefined ? "earlier" : "other"} ${hidden === 1 ? "line" : "lines"} · ctrl+o`
|
|
53
|
+
: `${all.length} ${all.length === 1 ? "line" : "lines"}`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function renderDetail(detail, tone, width, pal, railInk = pal.rule) {
|
|
57
|
+
const rail = transcriptLead(width, { text: "│", fg: railInk });
|
|
58
|
+
if (detail.kind === "out") {
|
|
59
|
+
const base = tone === "fail" || failureLine(detail.text) ? pal.ink.removed : pal.ink.muted;
|
|
60
|
+
return row(width, [
|
|
61
|
+
...rail,
|
|
62
|
+
{ text: detail.text === "" ? " " : detail.text, fg: base },
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
if (detail.kind === "gap") {
|
|
66
|
+
return row(width, [
|
|
67
|
+
...rail,
|
|
68
|
+
{ text: detail.text, fg: pal.ink.dim, italic: true },
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
const number = detail.kind === "add" ? detail.newLine : detail.oldLine;
|
|
72
|
+
const sign = detail.kind === "add" ? "+" : detail.kind === "del" ? "-" : " ";
|
|
73
|
+
const fg = detail.kind === "add"
|
|
74
|
+
? pal.ink.added
|
|
75
|
+
: detail.kind === "del"
|
|
76
|
+
? pal.ink.removed
|
|
77
|
+
: pal.ink.dim;
|
|
78
|
+
const tint = detail.kind === "add"
|
|
79
|
+
? pal.surface.added
|
|
80
|
+
: detail.kind === "del"
|
|
81
|
+
? pal.surface.removed
|
|
82
|
+
: undefined;
|
|
83
|
+
return row(width, [
|
|
84
|
+
...rail,
|
|
85
|
+
{ text: sign, fg, bold: detail.kind !== "keep" },
|
|
86
|
+
{ text: `${String(number ?? "").padStart(4)} `, fg: pal.ink.dim },
|
|
87
|
+
...emphasized(detail.text, detail.emphasis, fg, tint),
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
90
|
+
function emphasized(text, emphasis, fg, bg) {
|
|
91
|
+
if (emphasis === undefined || emphasis.length <= 0)
|
|
92
|
+
return [{ text, fg }];
|
|
93
|
+
const requestedStart = Math.max(0, Math.min(text.length, emphasis.start));
|
|
94
|
+
const requestedEnd = Math.max(requestedStart, Math.min(text.length, requestedStart + emphasis.length));
|
|
95
|
+
const start = graphemeFloor(text, requestedStart);
|
|
96
|
+
const end = graphemeCeiling(text, requestedEnd);
|
|
97
|
+
if (start === end)
|
|
98
|
+
return [{ text, fg }];
|
|
99
|
+
return [
|
|
100
|
+
...(start === 0 ? [] : [{ text: text.slice(0, start), fg }]),
|
|
101
|
+
{ text: text.slice(start, end), fg, bg, bold: true },
|
|
102
|
+
...(end === text.length ? [] : [{ text: text.slice(end), fg }]),
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
function failureLine(line) {
|
|
106
|
+
return line.startsWith("✖") || line.startsWith("×") || line.includes("AssertionError") || /\bfailed\b/i.test(line);
|
|
107
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// One travelling light on the execution connector; evidence never animates.
|
|
2
|
+
import { hasColor } from "../../ui/render.js";
|
|
3
|
+
export function runningTool(block) {
|
|
4
|
+
return block.kind === "tool" && block.tone === "pending" &&
|
|
5
|
+
block.startedAt !== undefined && block.right === "running";
|
|
6
|
+
}
|
|
7
|
+
export function stateInk(block, pal) {
|
|
8
|
+
if (block.tone === "fail")
|
|
9
|
+
return pal.ink.removed;
|
|
10
|
+
if (block.tone === "deny")
|
|
11
|
+
return pal.ink.attention;
|
|
12
|
+
if (block.tone === "ok")
|
|
13
|
+
return pal.ink.added;
|
|
14
|
+
return runningTool(block) ? pal.accent : pal.ink.dim;
|
|
15
|
+
}
|
|
16
|
+
export function stateMark(block) {
|
|
17
|
+
return block.tone === "ok" ? "✓" : block.tone === "fail" ? "×" : block.tone === "deny" ? "!" : "○";
|
|
18
|
+
}
|
|
19
|
+
export function connectorInk(block, pal, context, position, length) {
|
|
20
|
+
if (context.reducedMotion === true || block.expanded === true || !hasColor() || !runningTool(block))
|
|
21
|
+
return pal.rule;
|
|
22
|
+
const elapsed = Math.max(0, (context.now ?? Date.now()) - block.startedAt);
|
|
23
|
+
const head = (elapsed % 2000) / 2000 * (length + 3) - 1.5;
|
|
24
|
+
const amount = Math.max(0, 1 - Math.abs(position - head) / 1.8);
|
|
25
|
+
const channel = (index) => Math.round(pal.rule[index] + (pal.accent[index] - pal.rule[index]) * amount);
|
|
26
|
+
return [channel(0), channel(1), channel(2)];
|
|
27
|
+
}
|
|
28
|
+
export function connector(text, block, pal, context, position, length) {
|
|
29
|
+
return Array.from(text, (char, index) => ({
|
|
30
|
+
text: char, fg: connectorInk(block, pal, context, position + index / 3, length),
|
|
31
|
+
}));
|
|
32
|
+
}
|