@giovannijecha/jecode 0.7.3 → 0.8.0
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 +104 -29
- package/dist/atomic.js +14 -0
- package/dist/batch.js +5 -2
- package/dist/cli-info.js +1 -1
- package/dist/config.js +4 -1
- package/dist/context/policy.js +14 -7
- package/dist/controller-request.js +1 -1
- package/dist/controller.js +39 -15
- package/dist/duration.js +8 -0
- package/dist/oauth-http.js +2 -1
- package/dist/openai-account.js +12 -8
- package/dist/openai-oauth-callback.js +2 -1
- package/dist/providers/anthropic-stream.js +8 -0
- package/dist/providers/anthropic-wire.js +22 -10
- package/dist/providers/http.js +4 -3
- package/dist/providers/ollama-stream.js +7 -0
- package/dist/providers/ollama-wire.js +11 -3
- package/dist/providers/openai-codex.js +0 -2
- package/dist/providers/openai-stream.js +57 -2
- package/dist/providers/openai-wire.js +27 -12
- package/dist/providers/sse.js +115 -38
- package/dist/sessions/codec.js +13 -5
- package/dist/sessions/store.js +2 -1
- package/dist/settings-command.js +0 -5
- package/dist/settings.js +0 -2
- package/dist/steering.js +58 -0
- package/dist/text-boundary.js +47 -0
- package/dist/timeline.js +2 -1
- package/dist/tools/args.js +1 -1
- package/dist/tools/fs.js +5 -0
- package/dist/tools/text-boundary.js +1 -31
- package/dist/transcript.js +6 -1
- package/dist/tui/activity.js +20 -5
- package/dist/tui/app-input.js +31 -6
- package/dist/tui/app-workflows.js +57 -14
- package/dist/tui/app.js +18 -3
- package/dist/tui/blocks.js +7 -2
- package/dist/tui/components/messages.js +20 -17
- package/dist/tui/components/misc.js +7 -6
- package/dist/tui/components/status.js +12 -1
- package/dist/tui/components/tool.js +152 -50
- package/dist/tui/help.js +1 -1
- package/dist/tui/motion.js +32 -0
- package/dist/tui/session-view.js +3 -2
- package/dist/tui/transcript-grammar.js +25 -0
- package/dist/tui/transcript-view.js +132 -7
- package/dist/tui/turn.js +78 -39
- package/dist/tui/view.js +4 -2
- package/dist/ui/diff.js +51 -16
- package/dist/ui/render.js +17 -27
- package/dist/ui/theme.js +19 -18
- package/dist/ui/width.js +31 -22
- package/dist/usage.js +5 -1
- package/package.json +1 -1
package/dist/tui/turn.js
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
// The controller speaks in stream events and tool results; the screen speaks in
|
|
4
4
|
// blocks. This is the whole of the translation, kept out of the shell so that
|
|
5
5
|
// neither has to know how the other is built.
|
|
6
|
+
import { graphemes } from "../text-boundary.js";
|
|
6
7
|
import { condense, diff } from "../ui/diff.js";
|
|
7
8
|
import { promptFor } from "./approve.js";
|
|
8
9
|
// Semantic activity labels feed the footer's compact state and timer while
|
|
9
10
|
// reasoning and tools keep the detailed work visible in the transcript.
|
|
10
11
|
const WAITING = "Waiting";
|
|
11
12
|
const THINKING = "Thinking";
|
|
12
|
-
const
|
|
13
|
+
const RESPONDING = "Responding";
|
|
13
14
|
const ASKING = "Waiting for you";
|
|
14
15
|
/** Unchanged rows kept either side of a change. */
|
|
15
16
|
const CONTEXT = 2;
|
|
@@ -18,10 +19,7 @@ export function transcribe(stage) {
|
|
|
18
19
|
// one, which is what keeps reasoning and answer from running together.
|
|
19
20
|
let open;
|
|
20
21
|
const tools = new Map();
|
|
21
|
-
|
|
22
|
-
let steps = 1;
|
|
23
|
-
let tool = 1;
|
|
24
|
-
let toolTotal = 1;
|
|
22
|
+
const progress = new Map();
|
|
25
23
|
const close = () => {
|
|
26
24
|
const block = open?.block;
|
|
27
25
|
const changed = block?.kind === "reasoning" && block.live === true;
|
|
@@ -40,30 +38,50 @@ export function transcribe(stage) {
|
|
|
40
38
|
continue;
|
|
41
39
|
block.tone = "fail";
|
|
42
40
|
block.right = reason ?? "failed";
|
|
43
|
-
block
|
|
41
|
+
settleDuration(block);
|
|
44
42
|
stage.render(block);
|
|
45
43
|
}
|
|
46
44
|
},
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
onToolPreparing(call, current, total) {
|
|
46
|
+
progress.set(call.id, { current, total });
|
|
47
|
+
const changed = close();
|
|
48
|
+
if (changed !== undefined)
|
|
49
|
+
stage.render(changed);
|
|
50
|
+
stage.status(toolStatus("Preparing", call, progress));
|
|
51
51
|
stage.render();
|
|
52
52
|
},
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
onToolStart(call, current, total) {
|
|
54
|
+
progress.set(call.id, { current, total });
|
|
55
|
+
stage.status(toolStatus("Running", call, progress));
|
|
56
|
+
const block = tools.get(call.id);
|
|
57
|
+
if (block !== undefined && block.kind === "tool") {
|
|
58
|
+
block.right = "running";
|
|
59
|
+
block.startedAt = Date.now();
|
|
60
|
+
block.durationMs = undefined;
|
|
61
|
+
}
|
|
62
|
+
stage.render(block);
|
|
56
63
|
},
|
|
57
64
|
onUsage(usage) {
|
|
58
65
|
stage.usage?.(usage);
|
|
59
66
|
},
|
|
67
|
+
onRequestInput(inputTokens) {
|
|
68
|
+
stage.requestInput?.(inputTokens);
|
|
69
|
+
},
|
|
60
70
|
onStatus(status) {
|
|
61
71
|
stage.status(status);
|
|
62
72
|
stage.render();
|
|
63
73
|
},
|
|
64
74
|
onStream(event) {
|
|
75
|
+
if (event.kind === "tool") {
|
|
76
|
+
const changed = close();
|
|
77
|
+
if (changed !== undefined)
|
|
78
|
+
stage.render(changed);
|
|
79
|
+
stage.status(`Preparing ${event.name ?? "tool"}`);
|
|
80
|
+
stage.render();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
65
83
|
const kind = event.kind === "thinking" ? "reasoning" : "answer";
|
|
66
|
-
stage.status(kind === "reasoning" ? THINKING :
|
|
84
|
+
stage.status(kind === "reasoning" ? THINKING : RESPONDING);
|
|
67
85
|
if (open === undefined || open.kind !== kind) {
|
|
68
86
|
const changed = close();
|
|
69
87
|
if (changed !== undefined)
|
|
@@ -83,15 +101,13 @@ export function transcribe(stage) {
|
|
|
83
101
|
const changed = close();
|
|
84
102
|
if (changed !== undefined)
|
|
85
103
|
stage.render(changed);
|
|
86
|
-
stage.status(`Running ${call.name}${toolTotal > 1 ? ` · tool ${tool}/${toolTotal}` : ""}${step > 1 ? ` · step ${step}/${steps}` : ""}`);
|
|
87
104
|
const block = {
|
|
88
105
|
kind: "tool",
|
|
89
106
|
name: call.name,
|
|
90
107
|
target: target(call.input),
|
|
91
|
-
right: "
|
|
108
|
+
right: "ready",
|
|
92
109
|
tone: "pending",
|
|
93
110
|
body: preview(call, look),
|
|
94
|
-
startedAt: Date.now(),
|
|
95
111
|
};
|
|
96
112
|
tools.set(call.id, block);
|
|
97
113
|
stage.emit(block);
|
|
@@ -105,7 +121,7 @@ export function transcribe(stage) {
|
|
|
105
121
|
stage.render(block);
|
|
106
122
|
},
|
|
107
123
|
onToolResult(call, result, summary) {
|
|
108
|
-
stage.status(
|
|
124
|
+
stage.status(WAITING);
|
|
109
125
|
const block = tools.get(call.id);
|
|
110
126
|
if (block === undefined || block.kind !== "tool")
|
|
111
127
|
return;
|
|
@@ -118,7 +134,7 @@ export function transcribe(stage) {
|
|
|
118
134
|
}
|
|
119
135
|
block.tone = result.isError ? "fail" : "ok";
|
|
120
136
|
block.right = summary ?? "";
|
|
121
|
-
block
|
|
137
|
+
settleDuration(block);
|
|
122
138
|
// A failure replaces the preview: what the call was going to do stops
|
|
123
139
|
// being the interesting part the moment it did not do it.
|
|
124
140
|
const outcome = result.isError
|
|
@@ -130,14 +146,8 @@ export function transcribe(stage) {
|
|
|
130
146
|
},
|
|
131
147
|
approve(call) {
|
|
132
148
|
const block = tools.get(call.id);
|
|
133
|
-
if (stage.approved(call))
|
|
134
|
-
if (block !== undefined && block.kind === "tool") {
|
|
135
|
-
block.right = "running";
|
|
136
|
-
block.startedAt ??= Date.now();
|
|
137
|
-
stage.render(block);
|
|
138
|
-
}
|
|
149
|
+
if (stage.approved(call))
|
|
139
150
|
return Promise.resolve(true);
|
|
140
|
-
}
|
|
141
151
|
const changed = close();
|
|
142
152
|
if (changed !== undefined)
|
|
143
153
|
stage.render(changed);
|
|
@@ -156,8 +166,8 @@ export function transcribe(stage) {
|
|
|
156
166
|
if (settled !== undefined && settled.kind === "tool") {
|
|
157
167
|
if (approved) {
|
|
158
168
|
settled.tone = "pending";
|
|
159
|
-
settled.right = "
|
|
160
|
-
settled.startedAt =
|
|
169
|
+
settled.right = "ready";
|
|
170
|
+
settled.startedAt = undefined;
|
|
161
171
|
}
|
|
162
172
|
else {
|
|
163
173
|
settled.tone = "deny";
|
|
@@ -165,7 +175,9 @@ export function transcribe(stage) {
|
|
|
165
175
|
settled.startedAt = undefined;
|
|
166
176
|
}
|
|
167
177
|
}
|
|
168
|
-
stage.status(approved
|
|
178
|
+
stage.status(approved
|
|
179
|
+
? toolStatus("Preparing", call, progress)
|
|
180
|
+
: WAITING);
|
|
169
181
|
stage.render(settled);
|
|
170
182
|
resolve(approved);
|
|
171
183
|
});
|
|
@@ -173,14 +185,24 @@ export function transcribe(stage) {
|
|
|
173
185
|
},
|
|
174
186
|
};
|
|
175
187
|
}
|
|
188
|
+
function toolStatus(phase, call, progress) {
|
|
189
|
+
const position = progress.get(call.id);
|
|
190
|
+
const tool = position !== undefined && position.total > 1
|
|
191
|
+
? ` · tool ${position.current}/${position.total}`
|
|
192
|
+
: "";
|
|
193
|
+
return `${phase} ${call.name}${tool}`;
|
|
194
|
+
}
|
|
176
195
|
function pendingApproval(body) {
|
|
177
196
|
const added = body?.filter((detail) => detail.kind === "add").length ?? 0;
|
|
178
197
|
const removed = body?.filter((detail) => detail.kind === "del").length ?? 0;
|
|
179
198
|
const summary = added + removed === 0 ? "" : `+${added} −${removed} · `;
|
|
180
199
|
return `${summary}pending approval`;
|
|
181
200
|
}
|
|
182
|
-
function
|
|
183
|
-
|
|
201
|
+
function settleDuration(block) {
|
|
202
|
+
if (block.startedAt !== undefined) {
|
|
203
|
+
block.durationMs = Math.max(0, Date.now() - block.startedAt);
|
|
204
|
+
}
|
|
205
|
+
block.startedAt = undefined;
|
|
184
206
|
}
|
|
185
207
|
/** The argument worth showing: the thing the call acts on. */
|
|
186
208
|
function target(input) {
|
|
@@ -249,18 +271,35 @@ function emphasizePairs(rows) {
|
|
|
249
271
|
continue;
|
|
250
272
|
if (rows[index - 1]?.kind === "del" || rows[index + 2]?.kind === "add")
|
|
251
273
|
continue;
|
|
274
|
+
const removedClusters = graphemes(removed.text);
|
|
275
|
+
const addedClusters = graphemes(added.text);
|
|
276
|
+
let prefix = 0;
|
|
252
277
|
let start = 0;
|
|
253
|
-
while (
|
|
254
|
-
|
|
278
|
+
while (prefix < removedClusters.length &&
|
|
279
|
+
prefix < addedClusters.length &&
|
|
280
|
+
removedClusters[prefix] === addedClusters[prefix]) {
|
|
281
|
+
start += removedClusters[prefix].length;
|
|
282
|
+
prefix++;
|
|
283
|
+
}
|
|
255
284
|
let suffix = 0;
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
285
|
+
let removedSuffix = 0;
|
|
286
|
+
let addedSuffix = 0;
|
|
287
|
+
while (suffix < removedClusters.length - prefix &&
|
|
288
|
+
suffix < addedClusters.length - prefix &&
|
|
289
|
+
removedClusters[removedClusters.length - 1 - suffix] ===
|
|
290
|
+
addedClusters[addedClusters.length - 1 - suffix]) {
|
|
291
|
+
removedSuffix += removedClusters[removedClusters.length - 1 - suffix].length;
|
|
292
|
+
addedSuffix += addedClusters[addedClusters.length - 1 - suffix].length;
|
|
259
293
|
suffix++;
|
|
260
|
-
|
|
294
|
+
}
|
|
295
|
+
while (suffix > 0 &&
|
|
296
|
+
(!wordBoundary(removed.text, removedSuffix) || !wordBoundary(added.text, addedSuffix))) {
|
|
297
|
+
removedSuffix -= removedClusters[removedClusters.length - suffix].length;
|
|
298
|
+
addedSuffix -= addedClusters[addedClusters.length - suffix].length;
|
|
261
299
|
suffix--;
|
|
262
|
-
|
|
263
|
-
const
|
|
300
|
+
}
|
|
301
|
+
const removedLength = removed.text.length - start - removedSuffix;
|
|
302
|
+
const addedLength = added.text.length - start - addedSuffix;
|
|
264
303
|
if (removedLength > 0)
|
|
265
304
|
removed.emphasis = { start, length: removedLength };
|
|
266
305
|
if (addedLength > 0)
|
package/dist/tui/view.js
CHANGED
|
@@ -25,7 +25,7 @@ export function compose(view, size, transcript = transcriptRenderer()) {
|
|
|
25
25
|
// Any spare height belongs above the conversation, so short sessions grow
|
|
26
26
|
// upward from the fixed dock rhythm instead of leaving a changing hole
|
|
27
27
|
// beneath the latest reply.
|
|
28
|
-
const viewport = transcript.viewport(view.blocks, width, transcriptHeight, view.scroll, view.pal, { now: view.now });
|
|
28
|
+
const viewport = transcript.viewport(view.blocks, width, transcriptHeight, view.scroll, view.pal, { now: view.now, reducedMotion: view.reducedMotion });
|
|
29
29
|
const cursor = dock.cursor === undefined
|
|
30
30
|
? undefined
|
|
31
31
|
: { row: transcriptHeight + dock.cursor.row, col: dock.cursor.col };
|
|
@@ -34,6 +34,7 @@ export function compose(view, size, transcript = transcriptRenderer()) {
|
|
|
34
34
|
cursor,
|
|
35
35
|
maxScroll: viewport.maxScroll,
|
|
36
36
|
transcriptPending: viewport.pending,
|
|
37
|
+
transcriptAnimating: viewport.animating,
|
|
37
38
|
};
|
|
38
39
|
}
|
|
39
40
|
function dockRows(view, width, height) {
|
|
@@ -42,6 +43,7 @@ function dockRows(view, width, height) {
|
|
|
42
43
|
// use the footer while that interaction is open instead of repeating a
|
|
43
44
|
// generic "Running /…" label beside it.
|
|
44
45
|
status: view.modal === undefined ? view.status : undefined,
|
|
46
|
+
steering: view.modal === undefined ? view.steering : undefined,
|
|
45
47
|
feedback: view.feedback,
|
|
46
48
|
readiness: view.readiness,
|
|
47
49
|
unseen: view.unseen ?? 0,
|
|
@@ -87,5 +89,5 @@ function tooSmall(height, width, view) {
|
|
|
87
89
|
{ text: elide(`need ${MIN_COLS}×${MIN_ROWS}`, Math.max(1, width)), fg: view.pal.ink.dim },
|
|
88
90
|
]);
|
|
89
91
|
}
|
|
90
|
-
return { rows, maxScroll: 0, transcriptPending: false };
|
|
92
|
+
return { rows, maxScroll: 0, transcriptPending: false, transcriptAnimating: false };
|
|
91
93
|
}
|
package/dist/ui/diff.js
CHANGED
|
@@ -8,24 +8,37 @@
|
|
|
8
8
|
* Above this many cells the table stops being worth building.
|
|
9
9
|
*
|
|
10
10
|
* The diff is quadratic in the two line counts, and it is drawn between two
|
|
11
|
-
* keystrokes.
|
|
12
|
-
*
|
|
11
|
+
* keystrokes. Common edges are removed before this limit is applied, so a
|
|
12
|
+
* local edit in a large file stays local. If the changed middle itself crosses
|
|
13
|
+
* the ceiling, the honest answer is the coarse one rather than a frame the
|
|
14
|
+
* user waits for.
|
|
13
15
|
*/
|
|
14
16
|
const CEILING = 250_000;
|
|
15
17
|
export function diff(before, after) {
|
|
16
18
|
const a = lines(before);
|
|
17
19
|
const b = lines(after);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
const prefix = commonPrefix(a, b);
|
|
21
|
+
const suffix = commonSuffix(a, b, prefix);
|
|
22
|
+
const aEnd = a.length - suffix;
|
|
23
|
+
const bEnd = b.length - suffix;
|
|
24
|
+
const middleA = a.slice(prefix, aEnd);
|
|
25
|
+
const middleB = b.slice(prefix, bEnd);
|
|
26
|
+
const rows = a.slice(0, prefix).map(keep);
|
|
27
|
+
if (middleA.length * middleB.length > CEILING) {
|
|
28
|
+
for (const line of middleA)
|
|
29
|
+
rows.push(del(line));
|
|
30
|
+
for (const line of middleB)
|
|
31
|
+
rows.push(add(line));
|
|
32
|
+
appendSuffix(rows, a, aEnd);
|
|
33
|
+
return rows;
|
|
20
34
|
}
|
|
21
|
-
const table = common(
|
|
22
|
-
const rows = [];
|
|
35
|
+
const table = common(middleA, middleB);
|
|
23
36
|
let i = 0;
|
|
24
37
|
let j = 0;
|
|
25
|
-
const width =
|
|
26
|
-
while (i <
|
|
27
|
-
if (
|
|
28
|
-
rows.push(
|
|
38
|
+
const width = middleB.length + 1;
|
|
39
|
+
while (i < middleA.length && j < middleB.length) {
|
|
40
|
+
if (middleA[i] === middleB[j]) {
|
|
41
|
+
rows.push(keep(middleA[i]));
|
|
29
42
|
i++;
|
|
30
43
|
j++;
|
|
31
44
|
continue;
|
|
@@ -33,20 +46,39 @@ export function diff(before, after) {
|
|
|
33
46
|
// Deletions first on a tie, so a replaced line reads old-then-new — the
|
|
34
47
|
// order the eye expects, and the order every other diff prints.
|
|
35
48
|
if (table[(i + 1) * width + j] >= table[i * width + j + 1]) {
|
|
36
|
-
rows.push(del(
|
|
49
|
+
rows.push(del(middleA[i]));
|
|
37
50
|
i++;
|
|
38
51
|
}
|
|
39
52
|
else {
|
|
40
|
-
rows.push(add(
|
|
53
|
+
rows.push(add(middleB[j]));
|
|
41
54
|
j++;
|
|
42
55
|
}
|
|
43
56
|
}
|
|
44
|
-
while (i <
|
|
45
|
-
rows.push(del(
|
|
46
|
-
while (j <
|
|
47
|
-
rows.push(add(
|
|
57
|
+
while (i < middleA.length)
|
|
58
|
+
rows.push(del(middleA[i++]));
|
|
59
|
+
while (j < middleB.length)
|
|
60
|
+
rows.push(add(middleB[j++]));
|
|
61
|
+
appendSuffix(rows, a, aEnd);
|
|
48
62
|
return rows;
|
|
49
63
|
}
|
|
64
|
+
function commonPrefix(a, b) {
|
|
65
|
+
const limit = Math.min(a.length, b.length);
|
|
66
|
+
let length = 0;
|
|
67
|
+
while (length < limit && a[length] === b[length])
|
|
68
|
+
length++;
|
|
69
|
+
return length;
|
|
70
|
+
}
|
|
71
|
+
function commonSuffix(a, b, prefix) {
|
|
72
|
+
const limit = Math.min(a.length, b.length) - prefix;
|
|
73
|
+
let length = 0;
|
|
74
|
+
while (length < limit && a[a.length - length - 1] === b[b.length - length - 1])
|
|
75
|
+
length++;
|
|
76
|
+
return length;
|
|
77
|
+
}
|
|
78
|
+
function appendSuffix(rows, source, start) {
|
|
79
|
+
for (let index = start; index < source.length; index++)
|
|
80
|
+
rows.push(keep(source[index]));
|
|
81
|
+
}
|
|
50
82
|
/**
|
|
51
83
|
* Drop the unchanged stretches, keeping `context` rows either side.
|
|
52
84
|
*
|
|
@@ -109,3 +141,6 @@ function add(text) {
|
|
|
109
141
|
function del(text) {
|
|
110
142
|
return { kind: "del", text };
|
|
111
143
|
}
|
|
144
|
+
function keep(text) {
|
|
145
|
+
return { kind: "keep", text };
|
|
146
|
+
}
|
package/dist/ui/render.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Composing a terminal row out of styled segments. Pure: every function here
|
|
2
2
|
// returns a string or rows of segments, none of them write anywhere.
|
|
3
3
|
import { terminalText } from "./terminal-text.js";
|
|
4
|
-
import { elide, textWidth, wrapText } from "./width.js";
|
|
4
|
+
import { elide, splitByCells, textWidth, wrapText } from "./width.js";
|
|
5
5
|
// Built rather than written literally: a raw escape byte in the source is
|
|
6
6
|
// invisible in a diff and in a code review, which is how they survive.
|
|
7
7
|
const ESC = String.fromCharCode(27);
|
|
@@ -125,9 +125,7 @@ export function blank(width, ground) {
|
|
|
125
125
|
/**
|
|
126
126
|
* A full-width divider.
|
|
127
127
|
*
|
|
128
|
-
* Edge to edge
|
|
129
|
-
* behind a user's message: two elements that stop at different columns read as
|
|
130
|
-
* two grids, and the eye finds the discrepancy before it finds the text.
|
|
128
|
+
* Edge to edge so the composer and dock keep one exact terminal boundary.
|
|
131
129
|
*/
|
|
132
130
|
export function rule(width, color) {
|
|
133
131
|
return paint({ text: "─".repeat(Math.max(0, width)), fg: color });
|
|
@@ -166,13 +164,13 @@ export function flow(segs, max, continuation = []) {
|
|
|
166
164
|
let used = 0;
|
|
167
165
|
let pending;
|
|
168
166
|
const room = () => Math.max(1, max - (rows.length === 0 ? 0 : lead));
|
|
169
|
-
const add = (tok) => {
|
|
167
|
+
const add = (tok, width = textWidth(tok.text)) => {
|
|
170
168
|
const last = current[current.length - 1];
|
|
171
169
|
if (last !== undefined && sameStyle(last, tok.seg))
|
|
172
170
|
last.text += tok.text;
|
|
173
171
|
else
|
|
174
172
|
current.push({ ...tok.seg, text: tok.text });
|
|
175
|
-
used +=
|
|
173
|
+
used += width;
|
|
176
174
|
};
|
|
177
175
|
const flush = () => {
|
|
178
176
|
rows.push(rows.length === 0 ? current : [...continuation.map(copy), ...current]);
|
|
@@ -191,39 +189,31 @@ export function flow(segs, max, continuation = []) {
|
|
|
191
189
|
continue;
|
|
192
190
|
}
|
|
193
191
|
const space = pending === undefined ? 0 : textWidth(pending.text);
|
|
194
|
-
|
|
195
|
-
if (used + space +
|
|
192
|
+
const wordWidth = textWidth(tok.text);
|
|
193
|
+
if (used + space + wordWidth > room() && current.length > 0)
|
|
196
194
|
flush();
|
|
197
195
|
else if (pending !== undefined) {
|
|
198
|
-
add(pending);
|
|
196
|
+
add(pending, space);
|
|
199
197
|
pending = undefined;
|
|
200
198
|
}
|
|
201
199
|
// A word wider than any row is spent across rows: with autowrap off, what
|
|
202
200
|
// overflows is not ugly, it is gone.
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
201
|
+
if (wordWidth > room() - used) {
|
|
202
|
+
const chunks = splitByCells(tok.text, room() - used, Math.max(1, max - lead));
|
|
203
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
204
|
+
add({ ...tok, text: chunk.text }, chunk.width);
|
|
205
|
+
if (index + 1 < chunks.length)
|
|
206
|
+
flush();
|
|
207
|
+
}
|
|
208
|
+
continue;
|
|
210
209
|
}
|
|
211
|
-
if (
|
|
212
|
-
add(
|
|
210
|
+
if (tok.text !== "")
|
|
211
|
+
add(tok, wordWidth);
|
|
213
212
|
}
|
|
214
213
|
if (current.length > 0 || rows.length === 0)
|
|
215
214
|
flush();
|
|
216
215
|
return rows;
|
|
217
216
|
}
|
|
218
|
-
function clipTo(text, cols) {
|
|
219
|
-
let out = "";
|
|
220
|
-
for (const char of text) {
|
|
221
|
-
if (textWidth(out + char) > cols)
|
|
222
|
-
break;
|
|
223
|
-
out += char;
|
|
224
|
-
}
|
|
225
|
-
return out;
|
|
226
|
-
}
|
|
227
217
|
function copy(seg) {
|
|
228
218
|
return { ...seg };
|
|
229
219
|
}
|
package/dist/ui/theme.js
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
// Jecode's fixed terminal identity. Components depend on semantic tokens, not
|
|
2
|
-
// literal colours, while the product exposes one deliberate dark
|
|
3
|
-
// Jecode's fixed dark
|
|
4
|
-
//
|
|
2
|
+
// literal colours, while the product exposes one deliberate dark Slate look.
|
|
3
|
+
// Jecode's fixed dark Slate baseline. The exported name stays stable while
|
|
4
|
+
// cooler, quieter values leave the transcript bright enough to read and let
|
|
5
|
+
// live state carry the colour.
|
|
5
6
|
// Components depend on these roles rather than embedding presentation values.
|
|
6
7
|
export const STEEL = {
|
|
7
|
-
accent: [
|
|
8
|
-
technical: [
|
|
9
|
-
focus: [
|
|
10
|
-
rule: [
|
|
8
|
+
accent: [124, 164, 222],
|
|
9
|
+
technical: [126, 186, 208],
|
|
10
|
+
focus: [124, 164, 222],
|
|
11
|
+
rule: [44, 60, 78],
|
|
11
12
|
ink: {
|
|
12
|
-
fg: [
|
|
13
|
-
bright: [
|
|
14
|
-
muted: [
|
|
15
|
-
dim: [
|
|
16
|
-
attention: [
|
|
17
|
-
added: [
|
|
18
|
-
removed: [
|
|
13
|
+
fg: [214, 219, 226],
|
|
14
|
+
bright: [241, 244, 248],
|
|
15
|
+
muted: [149, 160, 174],
|
|
16
|
+
dim: [99, 111, 125],
|
|
17
|
+
attention: [226, 188, 112],
|
|
18
|
+
added: [138, 190, 150],
|
|
19
|
+
removed: [223, 120, 120],
|
|
19
20
|
},
|
|
20
21
|
surface: {
|
|
21
|
-
subtle: [
|
|
22
|
-
added: [
|
|
23
|
-
removed: [
|
|
24
|
-
attention: [
|
|
22
|
+
subtle: [23, 29, 37],
|
|
23
|
+
added: [21, 52, 33],
|
|
24
|
+
removed: [58, 24, 27],
|
|
25
|
+
attention: [58, 47, 20],
|
|
25
26
|
},
|
|
26
27
|
};
|
package/dist/ui/width.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// cell at all. Every alignment in the UI depends on this file being right:
|
|
7
7
|
// the right-hand column, a ground band, the cursor. Nothing measures with
|
|
8
8
|
// `.length`.
|
|
9
|
+
import { graphemes, segmentGraphemes } from "../text-boundary.js";
|
|
10
|
+
export { graphemes } from "../text-boundary.js";
|
|
9
11
|
/** Ranges the terminal draws two cells wide (East Asian Wide and Fullwidth). */
|
|
10
12
|
const WIDE = [
|
|
11
13
|
[0x1100, 0x115f],
|
|
@@ -93,16 +95,6 @@ function inRanges(code, ranges) {
|
|
|
93
95
|
// in source is a byte nobody reviews.
|
|
94
96
|
const VS15 = String.fromCodePoint(0xfe0e);
|
|
95
97
|
const VS16 = String.fromCodePoint(0xfe0f);
|
|
96
|
-
// Grapheme segmentation is in the standard library, so a family emoji built
|
|
97
|
-
// out of five code points and three joiners counts as the one thing the
|
|
98
|
-
// terminal actually draws.
|
|
99
|
-
const SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
100
|
-
export function graphemes(text) {
|
|
101
|
-
const out = [];
|
|
102
|
-
for (const { segment } of SEGMENTER.segment(text))
|
|
103
|
-
out.push(segment);
|
|
104
|
-
return out;
|
|
105
|
-
}
|
|
106
98
|
/**
|
|
107
99
|
* Cells taken by one grapheme cluster.
|
|
108
100
|
*
|
|
@@ -126,17 +118,38 @@ export function charWidth(cluster) {
|
|
|
126
118
|
}
|
|
127
119
|
export function textWidth(text) {
|
|
128
120
|
let total = 0;
|
|
129
|
-
for (const cluster of
|
|
121
|
+
for (const cluster of segmentGraphemes(text))
|
|
130
122
|
total += charWidth(cluster.segment);
|
|
131
123
|
return total;
|
|
132
124
|
}
|
|
125
|
+
/** Split text into grapheme-safe chunks without rescanning any suffix. */
|
|
126
|
+
export function splitByCells(text, firstCols, followingCols = firstCols) {
|
|
127
|
+
if (text === "")
|
|
128
|
+
return [{ text, width: 0 }];
|
|
129
|
+
const chunks = [];
|
|
130
|
+
let start = 0;
|
|
131
|
+
let used = 0;
|
|
132
|
+
let room = Math.max(1, firstCols);
|
|
133
|
+
for (const { index, segment } of segmentGraphemes(text)) {
|
|
134
|
+
const width = charWidth(segment);
|
|
135
|
+
if (index > start && used + width > room) {
|
|
136
|
+
chunks.push({ text: text.slice(start, index), width: used });
|
|
137
|
+
start = index;
|
|
138
|
+
used = 0;
|
|
139
|
+
room = Math.max(1, followingCols);
|
|
140
|
+
}
|
|
141
|
+
used += width;
|
|
142
|
+
}
|
|
143
|
+
chunks.push({ text: text.slice(start), width: used });
|
|
144
|
+
return chunks;
|
|
145
|
+
}
|
|
133
146
|
/** The longest prefix of `text` that fits in `cols` cells. */
|
|
134
147
|
export function clip(text, cols) {
|
|
135
148
|
if (cols <= 0)
|
|
136
149
|
return "";
|
|
137
150
|
let out = "";
|
|
138
151
|
let used = 0;
|
|
139
|
-
for (const { segment } of
|
|
152
|
+
for (const { segment } of segmentGraphemes(text)) {
|
|
140
153
|
const w = charWidth(segment);
|
|
141
154
|
if (used + w > cols)
|
|
142
155
|
break;
|
|
@@ -214,17 +227,13 @@ export function wrapText(text, max, continuation = "") {
|
|
|
214
227
|
flush();
|
|
215
228
|
if (w > room()) {
|
|
216
229
|
// Too long for any row: spend whole rows on it until it fits.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
rest = rest.slice(head.length);
|
|
224
|
-
flush();
|
|
230
|
+
const chunks = splitByCells(word, room(), Math.max(1, max - lead));
|
|
231
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
232
|
+
line = chunk.text;
|
|
233
|
+
width = chunk.width;
|
|
234
|
+
if (index + 1 < chunks.length)
|
|
235
|
+
flush();
|
|
225
236
|
}
|
|
226
|
-
line = rest;
|
|
227
|
-
width = textWidth(rest);
|
|
228
237
|
continue;
|
|
229
238
|
}
|
|
230
239
|
line = line === "" ? word : `${line} ${word}`;
|
package/dist/usage.js
CHANGED
|
@@ -12,9 +12,12 @@ export function emptyUsage() {
|
|
|
12
12
|
}
|
|
13
13
|
export function recordUsage(total, next) {
|
|
14
14
|
total.requests += 1;
|
|
15
|
-
total.lastInputTokens = next.inputTokens;
|
|
16
15
|
addUsage(total, next);
|
|
17
16
|
}
|
|
17
|
+
/** Replace context pressure without inventing provider-reported usage. */
|
|
18
|
+
export function recordRequestInput(total, inputTokens) {
|
|
19
|
+
total.lastInputTokens = inputTokens;
|
|
20
|
+
}
|
|
18
21
|
/** Account for an internal request without replacing the main context signal. */
|
|
19
22
|
export function recordAuxiliaryUsage(total, next) {
|
|
20
23
|
total.requests += 1;
|
|
@@ -32,6 +35,7 @@ export function usageFromHistory(messages) {
|
|
|
32
35
|
for (const message of messages) {
|
|
33
36
|
if (message.role === "assistant" && message.usage !== undefined) {
|
|
34
37
|
recordUsage(total, message.usage);
|
|
38
|
+
recordRequestInput(total, message.usage.inputTokens);
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
41
|
return total;
|