@giovannijecha/jecode 0.8.4 → 0.8.6
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 +28 -9
- package/dist/accounts.js +47 -10
- package/dist/atomic.js +24 -14
- package/dist/batch-view.js +27 -2
- package/dist/batch.js +39 -4
- package/dist/bounded-file.js +212 -0
- package/dist/cli-info.js +0 -1
- package/dist/commands.js +2 -0
- package/dist/config.js +16 -7
- package/dist/context/automatic.js +35 -0
- package/dist/context/compactor.js +32 -2
- package/dist/context/manual.js +20 -3
- package/dist/context/request-projection.js +130 -0
- package/dist/controller-request.js +33 -11
- package/dist/credential-commands.js +25 -9
- package/dist/credentials.js +56 -18
- package/dist/directory-anchor.js +91 -0
- package/dist/file-identity.js +12 -0
- package/dist/model-command.js +5 -4
- package/dist/openai-account-command.js +23 -10
- package/dist/openai-account.js +7 -5
- package/dist/openai-oauth-callback.js +13 -4
- package/dist/openai-oauth-tokens.js +9 -7
- package/dist/openai-oauth.js +8 -6
- package/dist/permission-command.js +1 -1
- package/dist/process-lease.js +329 -0
- package/dist/provider-commands.js +11 -37
- package/dist/provider-errors.js +31 -7
- package/dist/provider-label.js +9 -3
- package/dist/providers/anthropic-stream.js +4 -1
- package/dist/providers/anthropic-wire.js +8 -3
- package/dist/providers/anthropic.js +39 -20
- package/dist/providers/catalog.js +4 -4
- package/dist/providers/failure.js +181 -0
- package/dist/providers/http.js +82 -23
- package/dist/providers/index.js +1 -5
- package/dist/providers/ollama-context.js +42 -0
- package/dist/providers/ollama-endpoint.js +7 -34
- package/dist/providers/ollama-stream.js +5 -1
- package/dist/providers/ollama.js +36 -158
- package/dist/providers/openai-codex.js +71 -45
- package/dist/providers/openai-stream.js +26 -2
- package/dist/providers/openai.js +51 -25
- package/dist/providers/sse.js +52 -8
- package/dist/request-identity.js +32 -0
- 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/lease.js +132 -49
- package/dist/sessions/load.js +90 -0
- package/dist/sessions/runtime.js +15 -8
- package/dist/sessions/snapshot.js +33 -0
- package/dist/sessions/store.js +183 -378
- package/dist/settings-command.js +10 -5
- package/dist/settings.js +75 -22
- package/dist/stable-directory.js +148 -0
- package/dist/start.js +1 -2
- package/dist/store-lock.js +68 -84
- package/dist/tools/args.js +2 -2
- 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 -171
- package/dist/tools/glob.js +107 -0
- package/dist/tools/index.js +2 -1
- package/dist/tools/search.js +66 -205
- package/dist/tools/text-boundary.js +7 -33
- package/dist/tui/app-workflows.js +9 -334
- package/dist/tui/approve.js +5 -3
- package/dist/tui/blocks.js +8 -7
- package/dist/tui/command-workflow.js +106 -0
- package/dist/tui/components/command-menu.js +7 -10
- package/dist/tui/components/footer.js +1 -1
- package/dist/tui/components/menu.js +74 -43
- package/dist/tui/components/messages.js +16 -9
- 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/feedback.js +4 -0
- package/dist/tui/help.js +1 -1
- package/dist/tui/picker-layout.js +40 -0
- package/dist/tui/picker.js +7 -71
- package/dist/tui/session-view.js +7 -2
- package/dist/tui/tool-details.js +135 -0
- package/dist/tui/transcript-grammar.js +8 -1
- package/dist/tui/transcript-view.js +26 -112
- package/dist/tui/turn-workflow.js +264 -0
- package/dist/tui/turn.js +7 -140
- package/dist/tui/workflow-types.js +2 -0
- package/dist/tui/workspace.js +21 -7
- package/dist/user-store.js +23 -31
- package/package.json +14 -15
- package/dist/ollama-settings-command.js +0 -74
- package/dist/tools/ripgrep.js +0 -230
- package/dist/tui/motion.js +0 -32
|
@@ -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
|
"",
|
|
@@ -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
|
+
}
|
|
@@ -1,210 +1,56 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
import { fitSegs, hasColor, plainLen, row } from "../../ui/render.js";
|
|
4
|
-
import { graphemeCeiling, graphemeFloor } from "../../text-boundary.js";
|
|
1
|
+
// Target-first execution records, connected to bounded, expandable evidence.
|
|
2
|
+
import { plainLen, row } from "../../ui/render.js";
|
|
5
3
|
import { toolDuration } from "../../duration.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
const OUTPUT_ROWS = 8;
|
|
9
|
-
const LIVE_OUTPUT_ROWS = 6;
|
|
10
|
-
const DIFF_ROWS = 15;
|
|
11
|
-
const TOOL_NAME_COLS = 12;
|
|
12
|
-
const TOOL_COLUMNS_AT = 64;
|
|
13
|
-
const SPINNER_MS = 80;
|
|
14
|
-
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
4
|
+
import { toolEvidence, renderDetail } from "./tool-evidence.js";
|
|
5
|
+
import { connector, connectorInk, runningTool, stateInk, stateMark } from "./tool-motion.js";
|
|
15
6
|
export function renderTool(block, width, pal, context = {}) {
|
|
16
7
|
const now = context.now ?? Date.now();
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
8
|
+
const clock = { ...context, now };
|
|
9
|
+
const evidence = toolEvidence(block);
|
|
10
|
+
const active = runningTool(block);
|
|
11
|
+
const elapsed = active ? toolDuration(Math.max(0, now - block.startedAt), true)
|
|
12
|
+
: block.durationMs === undefined ? "" : toolDuration(block.durationMs);
|
|
13
|
+
const mark = stateMark(block);
|
|
14
|
+
// The separate change summary already carries these exact counts.
|
|
15
|
+
const repeatedCount = evidence.counts !== "" && block.right.startsWith(`${evidence.counts} · `);
|
|
16
|
+
const outcome = repeatedCount ? block.right.slice(evidence.counts.length + 3) : block.right;
|
|
17
|
+
const result = [`${mark}${outcome === "" ? "" : ` ${outcome}`}`, elapsed].filter(Boolean).join(" · ");
|
|
18
|
+
const hasEvidence = evidence.details.length > 0;
|
|
19
|
+
const name = { text: block.name, fg: pal.ink.dim };
|
|
20
|
+
const right = [{ text: result, fg: stateInk(block, pal) }];
|
|
21
|
+
const separateStatus = plainLen([name, ...right]) + (hasEvidence ? 2 : 3) + 3 > width;
|
|
22
|
+
const summaryAt = separateStatus ? 3 : 2;
|
|
23
|
+
const detailStart = summaryAt + (evidence.summary === "" ? 0 : 1);
|
|
24
|
+
const length = hasEvidence ? detailStart + evidence.details.length + 1 : summaryAt;
|
|
20
25
|
const left = [
|
|
21
|
-
...
|
|
22
|
-
|
|
23
|
-
...(block.target === "" ? [] : [{ text: ` ${block.target}`, fg: pal.technical }]),
|
|
26
|
+
...connector(hasEvidence || separateStatus ? "│ " : "└─ ", block, pal, clock, 1, length),
|
|
27
|
+
name,
|
|
24
28
|
];
|
|
25
|
-
const
|
|
26
|
-
const leader = movingLeader(width, left, right, pal, context, now);
|
|
27
|
-
return [
|
|
29
|
+
const rows = [
|
|
28
30
|
...(context.continues === true ? [] : [""]),
|
|
29
|
-
row(width,
|
|
30
|
-
|
|
31
|
+
row(width, [
|
|
32
|
+
...connector("┌ ", block, pal, clock, 0, length),
|
|
33
|
+
{ text: block.target || block.name, fg: pal.technical },
|
|
34
|
+
]),
|
|
35
|
+
separateStatus ? row(width, left)
|
|
36
|
+
: row(Math.min(width, plainLen([...left, ...right]) + 3), left, right),
|
|
31
37
|
];
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
sourceIndex: all.length - limit + index,
|
|
51
|
-
})),
|
|
52
|
-
];
|
|
53
|
-
}
|
|
54
|
-
// A compact transcript audits what changed rather than repeating unchanged
|
|
55
|
-
// source. One budget covers writes and edits, with both ends retained.
|
|
56
|
-
const changed = all
|
|
57
|
-
.map((detail, sourceIndex) => ({ detail, sourceIndex }))
|
|
58
|
-
.filter(({ detail }) => detail.kind === "add" || detail.kind === "del");
|
|
59
|
-
if (changed.length <= DIFF_ROWS)
|
|
60
|
-
return changed;
|
|
61
|
-
const leading = Math.ceil(DIFF_ROWS / 2);
|
|
62
|
-
const trailing = DIFF_ROWS - leading;
|
|
63
|
-
const hidden = changed.length - DIFF_ROWS;
|
|
64
|
-
return [
|
|
65
|
-
...changed.slice(0, leading),
|
|
66
|
-
{
|
|
67
|
-
detail: {
|
|
68
|
-
kind: "gap",
|
|
69
|
-
text: `… ${hidden} more changed ${hidden === 1 ? "line" : "lines"} · ctrl+o expand`,
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
...changed.slice(-trailing),
|
|
73
|
-
];
|
|
74
|
-
}
|
|
75
|
-
function renderDetail(detail, tone, width, pal, arrivedAt, now) {
|
|
76
|
-
const rail = transcriptLead(width, { text: "│", fg: pal.rule });
|
|
77
|
-
if (detail.kind === "out") {
|
|
78
|
-
const base = tone === "fail" || failureLine(detail.text) ? pal.ink.removed : pal.ink.muted;
|
|
79
|
-
return row(width, [
|
|
80
|
-
...rail,
|
|
81
|
-
{ text: detail.text === "" ? " " : detail.text, fg: arrivalInk(base, pal, arrivedAt, now) },
|
|
82
|
-
]);
|
|
83
|
-
}
|
|
84
|
-
if (detail.kind === "gap") {
|
|
85
|
-
return row(width, [
|
|
86
|
-
...rail,
|
|
87
|
-
{ text: detail.text, fg: arrivalInk(pal.ink.dim, pal, arrivedAt, now), italic: true },
|
|
88
|
-
]);
|
|
89
|
-
}
|
|
90
|
-
const number = detail.kind === "add" ? detail.newLine : detail.oldLine;
|
|
91
|
-
const sign = detail.kind === "add" ? "+" : detail.kind === "del" ? "-" : " ";
|
|
92
|
-
const base = detail.kind === "add"
|
|
93
|
-
? pal.ink.added
|
|
94
|
-
: detail.kind === "del"
|
|
95
|
-
? pal.ink.removed
|
|
96
|
-
: pal.ink.dim;
|
|
97
|
-
const fg = arrivalInk(base, pal, arrivedAt, now);
|
|
98
|
-
const tint = detail.kind === "add"
|
|
99
|
-
? pal.surface.added
|
|
100
|
-
: detail.kind === "del"
|
|
101
|
-
? pal.surface.removed
|
|
102
|
-
: undefined;
|
|
103
|
-
return row(width, [
|
|
104
|
-
...rail,
|
|
105
|
-
{ text: sign, fg, bold: detail.kind !== "keep" },
|
|
106
|
-
{ text: `${String(number ?? "").padStart(4)} `, fg: pal.ink.dim },
|
|
107
|
-
...emphasized(detail.text, detail.emphasis, fg, tint),
|
|
108
|
-
]);
|
|
109
|
-
}
|
|
110
|
-
function emphasized(text, emphasis, fg, bg) {
|
|
111
|
-
if (emphasis === undefined || emphasis.length <= 0)
|
|
112
|
-
return [{ text, fg }];
|
|
113
|
-
const requestedStart = Math.max(0, Math.min(text.length, emphasis.start));
|
|
114
|
-
const requestedEnd = Math.max(requestedStart, Math.min(text.length, requestedStart + emphasis.length));
|
|
115
|
-
const start = graphemeFloor(text, requestedStart);
|
|
116
|
-
const end = graphemeCeiling(text, requestedEnd);
|
|
117
|
-
if (start === end)
|
|
118
|
-
return [{ text, fg }];
|
|
119
|
-
return [
|
|
120
|
-
...(start === 0 ? [] : [{ text: text.slice(0, start), fg }]),
|
|
121
|
-
{ text: text.slice(start, end), fg, bg, bold: true },
|
|
122
|
-
...(end === text.length ? [] : [{ text: text.slice(end), fg }]),
|
|
123
|
-
];
|
|
124
|
-
}
|
|
125
|
-
function toolName(block, width) {
|
|
126
|
-
if (width < TOOL_COLUMNS_AT || block.target === "")
|
|
127
|
-
return block.name;
|
|
128
|
-
return block.name.padEnd(TOOL_NAME_COLS);
|
|
129
|
-
}
|
|
130
|
-
function stateGlyph(block, context, now) {
|
|
131
|
-
if (block.tone === "pending") {
|
|
132
|
-
if (block.startedAt === undefined || block.right !== "running")
|
|
133
|
-
return "○";
|
|
134
|
-
if (context.reducedMotion === true)
|
|
135
|
-
return "○";
|
|
136
|
-
return SPINNER[Math.floor(now / SPINNER_MS) % SPINNER.length] ?? "○";
|
|
137
|
-
}
|
|
138
|
-
if (block.tone === "fail")
|
|
139
|
-
return hasColor() ? "●" : "×";
|
|
140
|
-
if (block.tone === "deny")
|
|
141
|
-
return "○";
|
|
142
|
-
return hasColor() ? "●" : "✓";
|
|
143
|
-
}
|
|
144
|
-
function resultSegments(block, pal, context, now) {
|
|
145
|
-
const status = block.right;
|
|
146
|
-
const duration = liveDuration(block, now);
|
|
147
|
-
if (status === "" && duration === "")
|
|
148
|
-
return [];
|
|
149
|
-
const ink = resultInk(block, pal, context, now);
|
|
150
|
-
return [
|
|
151
|
-
...(status === "" ? [] : [{ text: status, fg: ink }]),
|
|
152
|
-
...(duration === "" ? [] : [{ text: `${status === "" ? "" : " · "}${duration}`, fg: pal.ink.dim }]),
|
|
153
|
-
];
|
|
154
|
-
}
|
|
155
|
-
function liveDuration(block, now) {
|
|
156
|
-
if (block.tone === "pending" && block.startedAt !== undefined && block.right === "running") {
|
|
157
|
-
return toolDuration(Math.max(0, now - block.startedAt), true);
|
|
158
|
-
}
|
|
159
|
-
return block.durationMs === undefined ? "" : toolDuration(block.durationMs);
|
|
160
|
-
}
|
|
161
|
-
function stateInk(block, pal, context, now) {
|
|
162
|
-
if (block.tone === "fail")
|
|
163
|
-
return pal.ink.removed;
|
|
164
|
-
if (block.tone === "deny")
|
|
165
|
-
return pal.ink.attention;
|
|
166
|
-
if (block.tone === "pending") {
|
|
167
|
-
if (block.startedAt === undefined || context.reducedMotion === true)
|
|
168
|
-
return pal.ink.attention;
|
|
169
|
-
return mix(pal.ink.attention, pal.accent, breathe(now));
|
|
170
|
-
}
|
|
171
|
-
return pal.ink.added;
|
|
172
|
-
}
|
|
173
|
-
function resultInk(block, pal, context, now) {
|
|
174
|
-
return block.tone === "ok" ? pal.ink.muted : stateInk(block, pal, context, now);
|
|
175
|
-
}
|
|
176
|
-
function birthInk(final, initial, context, now) {
|
|
177
|
-
if (context.reducedMotion === true || context.motion === undefined)
|
|
178
|
-
return final;
|
|
179
|
-
return mix(initial, final, easeOut(interval(now, context.motion.bornAt, TOOL_BIRTH_MS)));
|
|
180
|
-
}
|
|
181
|
-
function arrivalInk(base, pal, arrivedAt, now) {
|
|
182
|
-
if (arrivedAt === undefined)
|
|
183
|
-
return base;
|
|
184
|
-
return mix(pal.ink.bright, base, easeOut(interval(now, arrivedAt, TOOL_ROW_ARRIVAL_MS)));
|
|
185
|
-
}
|
|
186
|
-
function movingLeader(width, left, right, pal, context, now) {
|
|
187
|
-
if (context.reducedMotion === true || context.motion === undefined || right.length === 0)
|
|
188
|
-
return undefined;
|
|
189
|
-
const progress = interval(now, context.motion.bornAt, TOOL_LEADER_MAX_MS);
|
|
190
|
-
if (progress >= 1)
|
|
191
|
-
return undefined;
|
|
192
|
-
const fittedRight = fitSegs(right, width);
|
|
193
|
-
const gap = width - plainLen(left) - plainLen(fittedRight) - 1;
|
|
194
|
-
if (gap < 9)
|
|
195
|
-
return undefined;
|
|
196
|
-
const cells = Array.from({ length: gap }, () => " ");
|
|
197
|
-
const travelCells = gap - 2;
|
|
198
|
-
const trail = Math.min(7, travelCells);
|
|
199
|
-
const travel = travelCells + trail;
|
|
200
|
-
const head = Math.floor(easeInOut(progress) * travel) - trail;
|
|
201
|
-
for (let offset = 0; offset < trail; offset++) {
|
|
202
|
-
const at = head + offset;
|
|
203
|
-
if (at >= 0 && at < travelCells)
|
|
204
|
-
cells[at + 1] = "·";
|
|
205
|
-
}
|
|
206
|
-
return { text: cells.join(""), fg: pal.rule };
|
|
207
|
-
}
|
|
208
|
-
function failureLine(line) {
|
|
209
|
-
return line.startsWith("✖") || line.startsWith("×") || line.includes("AssertionError") || /\bfailed\b/i.test(line);
|
|
38
|
+
if (separateStatus)
|
|
39
|
+
rows.push(row(width, [
|
|
40
|
+
...connector(hasEvidence ? "│ " : "└─ ", block, pal, clock, 2, length),
|
|
41
|
+
...right,
|
|
42
|
+
]));
|
|
43
|
+
if (!hasEvidence)
|
|
44
|
+
return rows;
|
|
45
|
+
if (evidence.summary !== "")
|
|
46
|
+
rows.push(row(width, [
|
|
47
|
+
...connector("│ ", block, pal, clock, summaryAt, length),
|
|
48
|
+
{ text: evidence.summary, fg: pal.ink.muted },
|
|
49
|
+
]));
|
|
50
|
+
rows.push(...evidence.details.map((detail, index) => renderDetail(detail, block.tone, width, pal, block.expanded ? pal.rule : connectorInk(block, pal, clock, detailStart + index, length))));
|
|
51
|
+
rows.push(row(width, [
|
|
52
|
+
...connector("└─", block, pal, clock, length - 1, length),
|
|
53
|
+
{ text: evidence.note === "" ? "" : ` ${evidence.note}`, fg: pal.ink.dim },
|
|
54
|
+
]));
|
|
55
|
+
return rows;
|
|
210
56
|
}
|
package/dist/tui/feedback.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Operational feedback belongs in the footer, not in the conversation.
|
|
2
|
+
import { CONVERSATION_LIMITS } from "../conversation.js";
|
|
2
3
|
import { providerLabel } from "../provider-label.js";
|
|
3
4
|
const INFO_MS = 2_200;
|
|
4
5
|
const WARN_MS = 4_200;
|
|
@@ -53,6 +54,9 @@ export function turnBlocker(session) {
|
|
|
53
54
|
if (session.persistence?.failure !== undefined) {
|
|
54
55
|
return { text: "session could not be saved · /new to retry", tone: "error" };
|
|
55
56
|
}
|
|
57
|
+
if (session.conversation.nodes.length >= CONVERSATION_LIMITS.nodes) {
|
|
58
|
+
return { text: "conversation reached its session limit · /new to continue", tone: "warn" };
|
|
59
|
+
}
|
|
56
60
|
const blocked = session.provider.blocked();
|
|
57
61
|
const auth = session.provider.auth;
|
|
58
62
|
const expected = auth.kind === "api-key"
|
package/dist/tui/help.js
CHANGED
|
@@ -11,7 +11,7 @@ const CONTROLS = [
|
|
|
11
11
|
{ key: "alt+enter", description: "insert a new line" },
|
|
12
12
|
{ key: "esc", description: "close UI or interrupt work" },
|
|
13
13
|
{ key: "ctrl+c", description: "interrupt work or exit" },
|
|
14
|
-
{ key: "ctrl+o", description: "
|
|
14
|
+
{ key: "ctrl+o", description: "expand/collapse reasoning or evidence" },
|
|
15
15
|
{ key: "wheel / pgup/dn", description: "scroll the transcript" },
|
|
16
16
|
{ key: "ctrl+l", description: "redraw the screen" },
|
|
17
17
|
];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Bounded Ribbon composition; filtering and selection remain in picker.ts.
|
|
2
|
+
import { row } from "../ui/render.js";
|
|
3
|
+
import { menuText, renderMenu } from "./components/menu.js";
|
|
4
|
+
import { promptCursor, promptLine } from "./components/prompt.js";
|
|
5
|
+
export function layoutPicker(picker, found, width, pal, maxRows) {
|
|
6
|
+
const budget = Math.max(0, maxRows);
|
|
7
|
+
const queryRows = picker.searchable && budget >= 2 ? 1 : 0;
|
|
8
|
+
const titleRows = picker.title.length > 0 && budget - queryRows >= 2 ? 1 : 0;
|
|
9
|
+
const controlRows = budget - queryRows - titleRows >= 2 ? 1 : 0;
|
|
10
|
+
let contextRoom = Math.max(0, budget - queryRows - titleRows - controlRows - 1);
|
|
11
|
+
const target = menuText(picker.right ?? "", width, Math.min(2, contextRoom));
|
|
12
|
+
contextRoom -= target.length;
|
|
13
|
+
const note = menuText(picker.description ?? picker.footer ?? "", width, Math.min(2, contextRoom));
|
|
14
|
+
const prefix = titleRows + target.length + note.length + queryRows;
|
|
15
|
+
const menu = renderMenu(found.map(({ option, index }) => ({ ...option, selected: index === picker.index })), width, pal, { maxRows: budget - prefix - controlRows, visible: picker.visible ?? 6 });
|
|
16
|
+
const progress = found.length > menu.last - menu.first || picker.searchable
|
|
17
|
+
? `${found.length === 0 ? "0" : `${menu.first + 1}–${menu.last}`} / ${found.length}` +
|
|
18
|
+
(found.length === picker.options.length ? "" : ` · ${picker.options.length} total`)
|
|
19
|
+
: "";
|
|
20
|
+
const query = picker.query ?? "";
|
|
21
|
+
const cursor = queryRows === 0 ? undefined : {
|
|
22
|
+
row: prefix - 1,
|
|
23
|
+
col: promptCursor(query, query.length, width, { right: progress }).col,
|
|
24
|
+
};
|
|
25
|
+
if (pal === undefined)
|
|
26
|
+
return { rows: [], cursor };
|
|
27
|
+
const controls = picker.controls ?? (picker.adjust
|
|
28
|
+
? "↑↓ move · ←→ change · esc close" : "↑↓ move · enter select · esc close");
|
|
29
|
+
return {
|
|
30
|
+
rows: [
|
|
31
|
+
...(titleRows === 0 ? [] : [row(width, picker.title, queryRows > 0 || progress === "" ? [] : [{ text: progress, fg: pal.ink.dim }])]),
|
|
32
|
+
...[...target, ...note].map((text) => row(width, [{ text, fg: pal.ink.muted }])),
|
|
33
|
+
...(queryRows === 0 ? [] : [promptLine(query, query.length, width, pal, {
|
|
34
|
+
placeholder: "type to filter", right: progress,
|
|
35
|
+
}).row]),
|
|
36
|
+
...menu.rows,
|
|
37
|
+
...(controlRows === 0 ? [] : [row(width, [{ text: controls, fg: pal.ink.dim }])]),
|
|
38
|
+
], cursor,
|
|
39
|
+
};
|
|
40
|
+
}
|
package/dist/tui/picker.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
// One interaction model for every terminal selector.
|
|
2
|
-
import {
|
|
3
|
-
import { elide, graphemes } from "../ui/width.js";
|
|
2
|
+
import { graphemes } from "../ui/width.js";
|
|
4
3
|
import { assertPromptAppend } from "../input-boundary.js";
|
|
5
|
-
import {
|
|
6
|
-
import { promptCursor, promptLine } from "./components/prompt.js";
|
|
4
|
+
import { layoutPicker } from "./picker-layout.js";
|
|
7
5
|
const WINDOW = 6;
|
|
8
6
|
export function move(picker, step) {
|
|
9
7
|
const shown = matches(picker);
|
|
@@ -53,74 +51,12 @@ export function byKey(picker, text) {
|
|
|
53
51
|
const found = picker.options.findIndex((option) => option.key === typed);
|
|
54
52
|
return found === -1 ? undefined : found;
|
|
55
53
|
}
|
|
56
|
-
export function panel(picker, width, pal, maxRows = (picker.visible ?? WINDOW) +
|
|
57
|
-
return
|
|
54
|
+
export function panel(picker, width, pal, maxRows = (picker.visible ?? WINDOW) + 6) {
|
|
55
|
+
return layoutPicker(picker, matches(picker), width, pal, maxRows).rows;
|
|
58
56
|
}
|
|
59
57
|
/** Caret for the shared query row, relative to the unframed picker body. */
|
|
60
|
-
export function caret(picker, width, maxRows = (picker.visible ?? WINDOW) +
|
|
61
|
-
return
|
|
62
|
-
}
|
|
63
|
-
function layout(picker, width, pal, maxRows) {
|
|
64
|
-
const found = matches(picker);
|
|
65
|
-
const note = picker.description ?? picker.footer;
|
|
66
|
-
const hasTitle = picker.title.length > 0;
|
|
67
|
-
const fixed = (hasTitle ? 1 : 0) +
|
|
68
|
-
(note === undefined ? 0 : 1) +
|
|
69
|
-
(picker.searchable === true ? 1 : 0);
|
|
70
|
-
const optionRoom = Math.max(1, Math.min(picker.visible ?? WINDOW, maxRows - fixed));
|
|
71
|
-
const selectedAt = Math.max(0, found.findIndex((entry) => entry.index === picker.index));
|
|
72
|
-
const { first, last } = menuWindow(found.length, selectedAt, optionRoom);
|
|
73
|
-
const shown = found.slice(first, last);
|
|
74
|
-
const colors = pal;
|
|
75
|
-
const options = colors === undefined
|
|
76
|
-
? []
|
|
77
|
-
: shown.length === 0
|
|
78
|
-
? [row(width, [{ text: "no matches", fg: colors.ink.muted }])]
|
|
79
|
-
: renderMenuRows(shown.map(({ option, index }) => ({
|
|
80
|
-
label: option.label,
|
|
81
|
-
description: option.description,
|
|
82
|
-
hint: option.hint,
|
|
83
|
-
value: option.value,
|
|
84
|
-
adjustable: option.adjustable,
|
|
85
|
-
selected: index === picker.index,
|
|
86
|
-
})), width, colors);
|
|
87
|
-
const progress = found.length > shown.length || picker.searchable === true
|
|
88
|
-
? `${found.length === 0 ? "0" : `${first + 1}–${first + shown.length}`} / ${found.length}` +
|
|
89
|
-
(found.length === picker.options.length ? "" : ` · ${picker.options.length} total`)
|
|
90
|
-
: "";
|
|
91
|
-
const titleRight = hasTitle
|
|
92
|
-
? picker.right ?? (picker.searchable === true ? undefined : progress)
|
|
93
|
-
: undefined;
|
|
94
|
-
const visibleTitleRight = titleRight === undefined
|
|
95
|
-
? undefined
|
|
96
|
-
: elide(titleRight, Math.max(1, Math.floor(width * 0.55)));
|
|
97
|
-
const queryRow = picker.searchable === true && colors !== undefined
|
|
98
|
-
? promptLine(picker.query ?? "", (picker.query ?? "").length, width, colors, {
|
|
99
|
-
placeholder: "type to filter",
|
|
100
|
-
right: progress,
|
|
101
|
-
})
|
|
102
|
-
: undefined;
|
|
103
|
-
const queryCursor = picker.searchable === true
|
|
104
|
-
? promptCursor(picker.query ?? "", (picker.query ?? "").length, width, { right: progress })
|
|
105
|
-
: undefined;
|
|
106
|
-
const queryOffset = (hasTitle ? 1 : 0) + (note === undefined ? 0 : 1);
|
|
107
|
-
return {
|
|
108
|
-
rows: colors === undefined
|
|
109
|
-
? []
|
|
110
|
-
: [
|
|
111
|
-
...(hasTitle
|
|
112
|
-
? [row(width, picker.title, visibleTitleRight === undefined || visibleTitleRight === ""
|
|
113
|
-
? []
|
|
114
|
-
: [{ text: visibleTitleRight, fg: colors.ink.dim }])]
|
|
115
|
-
: []),
|
|
116
|
-
...(note === undefined ? [] : [row(width, [{ text: note, fg: colors.ink.muted }])]),
|
|
117
|
-
...(queryRow === undefined ? [] : [queryRow.row]),
|
|
118
|
-
...options,
|
|
119
|
-
],
|
|
120
|
-
cursor: picker.searchable === true
|
|
121
|
-
? { row: queryOffset, col: queryCursor?.col ?? 0 }
|
|
122
|
-
: undefined,
|
|
123
|
-
};
|
|
58
|
+
export function caret(picker, width, maxRows = (picker.visible ?? WINDOW) + 6) {
|
|
59
|
+
return layoutPicker(picker, matches(picker), width, undefined, maxRows).cursor;
|
|
124
60
|
}
|
|
125
61
|
export function heading(label, about, pal) {
|
|
126
62
|
return [
|
|
@@ -128,7 +64,7 @@ export function heading(label, about, pal) {
|
|
|
128
64
|
{ text: about, fg: pal.ink.fg },
|
|
129
65
|
];
|
|
130
66
|
}
|
|
131
|
-
function matches(picker) {
|
|
67
|
+
export function matches(picker) {
|
|
132
68
|
const query = (picker.query ?? "").trim().toLocaleLowerCase();
|
|
133
69
|
return picker.options.flatMap((option, index) => {
|
|
134
70
|
const haystack = `${option.label} ${option.hint ?? ""} ${option.value ?? ""}`.toLocaleLowerCase();
|
package/dist/tui/session-view.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Small projections of the live session used by the shell and footer.
|
|
2
2
|
import { credentialSource } from "../credentials.js";
|
|
3
3
|
import { providerFailure } from "../provider-errors.js";
|
|
4
|
+
import { providerRouteLabel } from "../provider-label.js";
|
|
5
|
+
import { providerFailureDetails } from "../providers/failure.js";
|
|
6
|
+
import { requestIdentityForSession } from "../request-identity.js";
|
|
4
7
|
export function controllerOptions(session, contextPolicy, tools = session.tools, steering) {
|
|
5
8
|
return {
|
|
6
9
|
provider: session.provider,
|
|
@@ -10,6 +13,7 @@ export function controllerOptions(session, contextPolicy, tools = session.tools,
|
|
|
10
13
|
maxTokens: session.config.maxTokens,
|
|
11
14
|
contextPolicy,
|
|
12
15
|
effort: session.config.effort,
|
|
16
|
+
requestIdentity: requestIdentityForSession(session),
|
|
13
17
|
maxModelRequests: session.config.maxModelRequests,
|
|
14
18
|
toolContext: { root: session.config.root },
|
|
15
19
|
...(steering === undefined ? {} : { steering }),
|
|
@@ -18,6 +22,7 @@ export function controllerOptions(session, contextPolicy, tools = session.tools,
|
|
|
18
22
|
export function footerInfo(session, workspace = session.config.root) {
|
|
19
23
|
return {
|
|
20
24
|
workspace,
|
|
25
|
+
provider: providerRouteLabel(session.provider),
|
|
21
26
|
model: session.model || "no model",
|
|
22
27
|
effort: session.config.effort,
|
|
23
28
|
};
|
|
@@ -26,8 +31,8 @@ export function footerInfo(session, workspace = session.config.root) {
|
|
|
26
31
|
export function turnFailure(session, error, aborted) {
|
|
27
32
|
if (aborted)
|
|
28
33
|
return { kind: "notice", text: "[interrupted]", tone: "warn" };
|
|
29
|
-
let text = providerFailure(session.provider, error);
|
|
30
|
-
if (
|
|
34
|
+
let text = providerFailure(session.provider, error, true);
|
|
35
|
+
if (providerFailureDetails(session.provider.id, error).kind === "authentication") {
|
|
31
36
|
const auth = session.provider.auth;
|
|
32
37
|
if (auth.kind === "oauth") {
|
|
33
38
|
text += ` · reconnect ${auth.label} in /providers`;
|