@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
|
@@ -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/frame.js
CHANGED
|
@@ -5,11 +5,15 @@
|
|
|
5
5
|
// writing it: a full repaint at streaming speed flickers and floods the pipe.
|
|
6
6
|
// So the painter keeps the last frame and emits the difference.
|
|
7
7
|
import { CSI } from "../ui/render.js";
|
|
8
|
-
import { CURSOR, SYNC, write } from "./screen.js";
|
|
9
|
-
export function painter() {
|
|
8
|
+
import { CURSOR, SYNC, write, outputReady, onDrain } from "./screen.js";
|
|
9
|
+
export function painter(output = { write, ready: outputReady, onReady: onDrain }) {
|
|
10
10
|
let previous = [];
|
|
11
11
|
return {
|
|
12
12
|
paint(rows, cursor) {
|
|
13
|
+
// The last accepted frame remains the diff base. The host redraws current
|
|
14
|
+
// state on drain, so stale intermediate frames never form another queue.
|
|
15
|
+
if (!output.ready())
|
|
16
|
+
return;
|
|
13
17
|
let out = SYNC.begin + CURSOR.hide;
|
|
14
18
|
const height = Math.max(rows.length, previous.length);
|
|
15
19
|
for (let i = 0; i < height; i++) {
|
|
@@ -24,10 +28,11 @@ export function painter() {
|
|
|
24
28
|
previous = rows.slice();
|
|
25
29
|
// One write, so a terminal that ignores the synchronization hint still
|
|
26
30
|
// gets the frame as a single arrival rather than a row at a time.
|
|
27
|
-
write(out + SYNC.end);
|
|
31
|
+
output.write(out + SYNC.end);
|
|
28
32
|
},
|
|
29
33
|
invalidate() {
|
|
30
34
|
previous = [];
|
|
31
35
|
},
|
|
36
|
+
onReady: (handler) => output.onReady(handler),
|
|
32
37
|
};
|
|
33
38
|
}
|
package/dist/tui/help.js
CHANGED
|
@@ -3,6 +3,7 @@ import { row } from "../ui/render.js";
|
|
|
3
3
|
import { textWidth } from "../ui/width.js";
|
|
4
4
|
const CONTROL_WIDTH = 18;
|
|
5
5
|
const CONTROLS = [
|
|
6
|
+
{ key: "/", description: "open commands, also while working" },
|
|
6
7
|
{ key: "up / down", description: "move through menus or history" },
|
|
7
8
|
{ key: "left / right", description: "move cursor or change a value" },
|
|
8
9
|
{ key: "ctrl+left / right", description: "move cursor by word" },
|
|
@@ -10,8 +11,8 @@ const CONTROLS = [
|
|
|
10
11
|
{ key: "enter / tab", description: "select, send/steer · complete" },
|
|
11
12
|
{ key: "alt+enter", description: "insert a new line" },
|
|
12
13
|
{ key: "esc", description: "close UI or interrupt work" },
|
|
13
|
-
{ key: "ctrl+c", description: "interrupt work or exit" },
|
|
14
|
-
{ key: "ctrl+o", description: "
|
|
14
|
+
{ key: "ctrl+c", description: "cancel menu, interrupt work or exit" },
|
|
15
|
+
{ key: "ctrl+o", description: "expand/collapse reasoning or evidence" },
|
|
15
16
|
{ key: "wheel / pgup/dn", description: "scroll the transcript" },
|
|
16
17
|
{ key: "ctrl+l", description: "redraw the screen" },
|
|
17
18
|
];
|
|
@@ -0,0 +1,44 @@
|
|
|
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, {
|
|
16
|
+
maxRows: budget - prefix - controlRows,
|
|
17
|
+
visible: picker.visible ?? 6,
|
|
18
|
+
overflow: picker.overflow,
|
|
19
|
+
});
|
|
20
|
+
const progress = found.length > menu.last - menu.first || picker.searchable
|
|
21
|
+
? `${found.length === 0 ? "0" : `${menu.first + 1}–${menu.last}`} / ${found.length}` +
|
|
22
|
+
(found.length === picker.options.length ? "" : ` · ${picker.options.length} total`)
|
|
23
|
+
: "";
|
|
24
|
+
const query = picker.query ?? "";
|
|
25
|
+
const cursor = queryRows === 0 ? undefined : {
|
|
26
|
+
row: prefix - 1,
|
|
27
|
+
col: promptCursor(query, query.length, width, { right: progress }).col,
|
|
28
|
+
};
|
|
29
|
+
if (pal === undefined)
|
|
30
|
+
return { rows: [], cursor };
|
|
31
|
+
const controls = picker.controls ?? (picker.adjust
|
|
32
|
+
? "↑↓ move · ←→ change · esc close" : "↑↓ move · enter select · esc close");
|
|
33
|
+
return {
|
|
34
|
+
rows: [
|
|
35
|
+
...(titleRows === 0 ? [] : [row(width, picker.title, queryRows > 0 || progress === "" ? [] : [{ text: progress, fg: pal.ink.dim }])]),
|
|
36
|
+
...[...target, ...note].map((text) => row(width, [{ text, fg: pal.ink.muted }])),
|
|
37
|
+
...(queryRows === 0 ? [] : [promptLine(query, query.length, width, pal, {
|
|
38
|
+
placeholder: "type to filter", right: progress,
|
|
39
|
+
}).row]),
|
|
40
|
+
...menu.rows,
|
|
41
|
+
...(controlRows === 0 ? [] : [row(width, [{ text: controls, fg: pal.ink.dim }])]),
|
|
42
|
+
], cursor,
|
|
43
|
+
};
|
|
44
|
+
}
|
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/screen.js
CHANGED
|
@@ -50,6 +50,13 @@ export function size() {
|
|
|
50
50
|
export function write(text) {
|
|
51
51
|
process.stdout.write(text);
|
|
52
52
|
}
|
|
53
|
+
export function outputReady() {
|
|
54
|
+
return !process.stdout.writableNeedDrain;
|
|
55
|
+
}
|
|
56
|
+
export function onDrain(handler) {
|
|
57
|
+
process.stdout.on("drain", handler);
|
|
58
|
+
return () => { process.stdout.off("drain", handler); };
|
|
59
|
+
}
|
|
53
60
|
export function enter(reducedMotion = false) {
|
|
54
61
|
if (active)
|
|
55
62
|
return;
|
package/dist/tui/session-view.js
CHANGED
|
@@ -14,7 +14,6 @@ export function controllerOptions(session, contextPolicy, tools = session.tools,
|
|
|
14
14
|
contextPolicy,
|
|
15
15
|
effort: session.config.effort,
|
|
16
16
|
requestIdentity: requestIdentityForSession(session),
|
|
17
|
-
maxModelRequests: session.config.maxModelRequests,
|
|
18
17
|
toolContext: { root: session.config.root },
|
|
19
18
|
...(steering === undefined ? {} : { steering }),
|
|
20
19
|
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Semantic tool evidence built from arguments, previews, and results.
|
|
2
|
+
import { graphemes } from "../text-boundary.js";
|
|
3
|
+
import { condense, diff } from "../ui/diff.js";
|
|
4
|
+
/** Unchanged rows kept either side of a change. */
|
|
5
|
+
const CONTEXT = 2;
|
|
6
|
+
/** The argument worth showing: the thing the call acts on. */
|
|
7
|
+
export function toolTarget(input) {
|
|
8
|
+
const { path, command } = input;
|
|
9
|
+
if (typeof command === "string")
|
|
10
|
+
return command;
|
|
11
|
+
if (typeof path === "string")
|
|
12
|
+
return path;
|
|
13
|
+
const rest = JSON.stringify(input);
|
|
14
|
+
return rest === "{}" ? "" : rest;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* What the call is about to do, drawn before it is allowed to do it.
|
|
18
|
+
*
|
|
19
|
+
* The tool is asked first, because only it knows what is already on disk: a
|
|
20
|
+
* write against an existing file is a replacement, and showing it as a page of
|
|
21
|
+
* additions hides exactly the part worth approving. The fallback diffs what is
|
|
22
|
+
* in the arguments, which is all there is when a tool has nothing to say.
|
|
23
|
+
*/
|
|
24
|
+
export function previewDetails(call, look) {
|
|
25
|
+
if (look !== undefined)
|
|
26
|
+
return changes(look.before, look.after);
|
|
27
|
+
const input = call.input;
|
|
28
|
+
if (call.name === "write_file" && typeof input.content === "string") {
|
|
29
|
+
return changes("", input.content);
|
|
30
|
+
}
|
|
31
|
+
if (call.name === "edit_file" && typeof input.old_text === "string") {
|
|
32
|
+
return changes(input.old_text, typeof input.new_text === "string" ? input.new_text : "");
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
/** Two texts as the rows of their difference, unchanged runs summed up. */
|
|
37
|
+
function changes(before, after) {
|
|
38
|
+
let oldLine = 1;
|
|
39
|
+
let newLine = 1;
|
|
40
|
+
const rows = [];
|
|
41
|
+
for (const changed of condense(diff(before, after), CONTEXT)) {
|
|
42
|
+
if (changed.kind === "gap") {
|
|
43
|
+
rows.push({ kind: "gap", text: `… ${changed.skipped} unchanged` });
|
|
44
|
+
oldLine += changed.skipped;
|
|
45
|
+
newLine += changed.skipped;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (changed.kind === "keep") {
|
|
49
|
+
rows.push({ kind: "keep", text: tabs(changed.text), oldLine, newLine });
|
|
50
|
+
oldLine++;
|
|
51
|
+
newLine++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (changed.kind === "del") {
|
|
55
|
+
rows.push({ kind: "del", text: tabs(changed.text), oldLine });
|
|
56
|
+
oldLine++;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
rows.push({ kind: "add", text: tabs(changed.text), newLine });
|
|
60
|
+
newLine++;
|
|
61
|
+
}
|
|
62
|
+
emphasizePairs(rows);
|
|
63
|
+
return rows.length === 0 ? undefined : rows;
|
|
64
|
+
}
|
|
65
|
+
function emphasizePairs(rows) {
|
|
66
|
+
for (let index = 0; index < rows.length - 1; index++) {
|
|
67
|
+
const removed = rows[index];
|
|
68
|
+
const added = rows[index + 1];
|
|
69
|
+
if (removed?.kind !== "del" || added?.kind !== "add")
|
|
70
|
+
continue;
|
|
71
|
+
if (rows[index - 1]?.kind === "del" || rows[index + 2]?.kind === "add")
|
|
72
|
+
continue;
|
|
73
|
+
const removedClusters = graphemes(removed.text);
|
|
74
|
+
const addedClusters = graphemes(added.text);
|
|
75
|
+
let prefix = 0;
|
|
76
|
+
let start = 0;
|
|
77
|
+
while (prefix < removedClusters.length &&
|
|
78
|
+
prefix < addedClusters.length &&
|
|
79
|
+
removedClusters[prefix] === addedClusters[prefix]) {
|
|
80
|
+
start += removedClusters[prefix].length;
|
|
81
|
+
prefix++;
|
|
82
|
+
}
|
|
83
|
+
let suffix = 0;
|
|
84
|
+
let removedSuffix = 0;
|
|
85
|
+
let addedSuffix = 0;
|
|
86
|
+
while (suffix < removedClusters.length - prefix &&
|
|
87
|
+
suffix < addedClusters.length - prefix &&
|
|
88
|
+
removedClusters[removedClusters.length - 1 - suffix] ===
|
|
89
|
+
addedClusters[addedClusters.length - 1 - suffix]) {
|
|
90
|
+
removedSuffix += removedClusters[removedClusters.length - 1 - suffix].length;
|
|
91
|
+
addedSuffix += addedClusters[addedClusters.length - 1 - suffix].length;
|
|
92
|
+
suffix++;
|
|
93
|
+
}
|
|
94
|
+
while (suffix > 0 &&
|
|
95
|
+
(!wordBoundary(removed.text, removedSuffix) || !wordBoundary(added.text, addedSuffix))) {
|
|
96
|
+
removedSuffix -= removedClusters[removedClusters.length - suffix].length;
|
|
97
|
+
addedSuffix -= addedClusters[addedClusters.length - suffix].length;
|
|
98
|
+
suffix--;
|
|
99
|
+
}
|
|
100
|
+
const removedLength = removed.text.length - start - removedSuffix;
|
|
101
|
+
const addedLength = added.text.length - start - addedSuffix;
|
|
102
|
+
if (removedLength > 0)
|
|
103
|
+
removed.emphasis = { start, length: removedLength };
|
|
104
|
+
if (addedLength > 0)
|
|
105
|
+
added.emphasis = { start, length: addedLength };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function wordBoundary(text, suffix) {
|
|
109
|
+
const at = text.length - suffix;
|
|
110
|
+
if (at <= 0 || at >= text.length)
|
|
111
|
+
return true;
|
|
112
|
+
return /\w/.test(text[at - 1]) !== /\w/.test(text[at]);
|
|
113
|
+
}
|
|
114
|
+
/** What the call left behind, for the calls whose output is worth a look. */
|
|
115
|
+
export function producedDetails(call, output) {
|
|
116
|
+
if (call.name === "run_command")
|
|
117
|
+
return outputDetails(output, "out");
|
|
118
|
+
// A read or a listing is already summed up on the right of its own row, and
|
|
119
|
+
// the model is about to say what was in it. Printing it twice helps nobody.
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
export function outputDetails(text, kind) {
|
|
123
|
+
const rows = split(text).map((line) => ({ kind, text: tabs(line) }));
|
|
124
|
+
return rows.length === 0 ? undefined : rows;
|
|
125
|
+
}
|
|
126
|
+
function split(text) {
|
|
127
|
+
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
128
|
+
while (lines.length > 0 && lines[lines.length - 1] === "")
|
|
129
|
+
lines.pop();
|
|
130
|
+
return lines.map(tabs);
|
|
131
|
+
}
|
|
132
|
+
// A tab is a width the terminal decides and the row measurement cannot see.
|
|
133
|
+
function tabs(line) {
|
|
134
|
+
return line.replace(/\t/g, " ");
|
|
135
|
+
}
|