@giovannijecha/jecode 0.7.2 → 0.7.4
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 +87 -23
- package/dist/atomic.js +14 -0
- package/dist/batch.js +26 -19
- package/dist/cli-info.js +1 -1
- package/dist/config.js +3 -2
- package/dist/context/budget.js +37 -0
- package/dist/context/compactor.js +9 -2
- package/dist/context/estimate.js +31 -0
- package/dist/context/policy.js +24 -14
- package/dist/controller-request.js +27 -4
- package/dist/controller.js +6 -4
- package/dist/conversation.js +48 -31
- package/dist/oauth-http.js +2 -1
- package/dist/openai-oauth-callback.js +2 -1
- package/dist/providers/anthropic.js +1 -1
- package/dist/providers/http.js +8 -5
- package/dist/providers/ollama.js +1 -1
- package/dist/providers/openai-codex.js +1 -3
- package/dist/providers/openai.js +1 -1
- package/dist/providers/sse.js +117 -40
- package/dist/providers/stream-limits.js +14 -2
- package/dist/sessions/store.js +2 -1
- package/dist/text-boundary.js +47 -0
- package/dist/timeline.js +2 -1
- package/dist/tools/fs.js +85 -38
- package/dist/tools/search.js +106 -44
- package/dist/tools/shell.js +14 -6
- package/dist/tools/text-boundary.js +7 -25
- package/dist/tui/app-state.js +0 -1
- package/dist/tui/app-workflows.js +40 -30
- package/dist/tui/app.js +12 -14
- package/dist/tui/blocks.js +0 -2
- package/dist/tui/components/messages.js +2 -5
- package/dist/tui/components/tool.js +11 -10
- package/dist/tui/session-view.js +2 -1
- package/dist/tui/transcript-view.js +178 -106
- package/dist/tui/turn.js +29 -8
- package/dist/tui/view.js +8 -3
- package/dist/ui/diff.js +51 -16
- package/dist/ui/render.js +16 -24
- package/dist/ui/width.js +31 -22
- package/dist/usage.js +5 -1
- package/package.json +2 -1
package/dist/tui/app.js
CHANGED
|
@@ -21,7 +21,7 @@ import { appWorkflows } from "./app-workflows.js";
|
|
|
21
21
|
import { sessionPermissions } from "../permissions.js";
|
|
22
22
|
import { resumePicker } from "./resume.js";
|
|
23
23
|
const FRAME_MS = 16;
|
|
24
|
-
const
|
|
24
|
+
const ACTIVITY_REFRESH_MS = 1_000;
|
|
25
25
|
/** How long a lone escape waits to prove it is not the start of a sequence. */
|
|
26
26
|
const ESCAPE_MS = 25;
|
|
27
27
|
export async function runApp(session, transcriptRoot, environment = {}) {
|
|
@@ -36,7 +36,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
36
36
|
const permissions = sessionPermissions(session.tools, session.config.autoApprove);
|
|
37
37
|
let closed;
|
|
38
38
|
let frameTimer;
|
|
39
|
-
let
|
|
39
|
+
let activityTimer;
|
|
40
40
|
let escapeTimer;
|
|
41
41
|
let stopResize = () => { };
|
|
42
42
|
let stopInput = () => { };
|
|
@@ -70,8 +70,6 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
70
70
|
: activityStatus(state.activity, state.status ?? state.activity.label, now),
|
|
71
71
|
feedback: state.feedback,
|
|
72
72
|
readiness: turnBlocker(session),
|
|
73
|
-
spin: state.spin,
|
|
74
|
-
reducedMotion: session.config.reducedMotion,
|
|
75
73
|
now,
|
|
76
74
|
modal: overlay.shown(state.open),
|
|
77
75
|
menu: completionOptions(state.completing),
|
|
@@ -98,6 +96,8 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
98
96
|
}
|
|
99
97
|
state.lastMaxScroll = frame.maxScroll;
|
|
100
98
|
paint.paint(frame.rows, frame.cursor);
|
|
99
|
+
if (frame.transcriptPending)
|
|
100
|
+
render();
|
|
101
101
|
};
|
|
102
102
|
// Streaming produces a token at a time; painting at that rate is wasted
|
|
103
103
|
// work, so repaints coalesce onto one frame.
|
|
@@ -144,8 +144,8 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
144
144
|
live = false;
|
|
145
145
|
safely(stopInput);
|
|
146
146
|
safely(stopResize);
|
|
147
|
-
if (
|
|
148
|
-
clearInterval(
|
|
147
|
+
if (activityTimer !== undefined)
|
|
148
|
+
clearInterval(activityTimer);
|
|
149
149
|
if (frameTimer !== undefined)
|
|
150
150
|
clearTimeout(frameTimer);
|
|
151
151
|
if (escapeTimer !== undefined)
|
|
@@ -194,9 +194,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
194
194
|
const activity = begin(kind, label);
|
|
195
195
|
state.activity = activity;
|
|
196
196
|
state.status = label;
|
|
197
|
-
|
|
198
|
-
if (!session.config.reducedMotion)
|
|
199
|
-
state.spin++;
|
|
197
|
+
activityTimer = setInterval(() => guard(() => {
|
|
200
198
|
let activeTool;
|
|
201
199
|
for (let index = state.blocks.length - 1; index >= 0; index--) {
|
|
202
200
|
const block = state.blocks[index];
|
|
@@ -206,16 +204,16 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
206
204
|
break;
|
|
207
205
|
}
|
|
208
206
|
render(activeTool);
|
|
209
|
-
}),
|
|
207
|
+
}), ACTIVITY_REFRESH_MS);
|
|
210
208
|
render();
|
|
211
209
|
return activity;
|
|
212
210
|
}
|
|
213
211
|
function finishActivity(activity) {
|
|
214
212
|
if (state.activity !== activity)
|
|
215
213
|
return;
|
|
216
|
-
if (
|
|
217
|
-
clearInterval(
|
|
218
|
-
|
|
214
|
+
if (activityTimer !== undefined)
|
|
215
|
+
clearInterval(activityTimer);
|
|
216
|
+
activityTimer = undefined;
|
|
219
217
|
state.activity = undefined;
|
|
220
218
|
state.status = undefined;
|
|
221
219
|
state.open = overlay.cancel(state.open);
|
|
@@ -295,7 +293,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
295
293
|
terminal.enter(session.config.reducedMotion);
|
|
296
294
|
stopResize = terminal.onResize(() => guard(() => {
|
|
297
295
|
paint.invalidate();
|
|
298
|
-
|
|
296
|
+
render();
|
|
299
297
|
}));
|
|
300
298
|
stopInput = terminal.onInput((chunk) => guard(() => {
|
|
301
299
|
if (escapeTimer !== undefined)
|
package/dist/tui/blocks.js
CHANGED
|
@@ -13,8 +13,6 @@ export function render(block, width, pal, context = {}) {
|
|
|
13
13
|
case "tool":
|
|
14
14
|
return renderTool(block, width, pal, {
|
|
15
15
|
continues: context.previous?.kind === "tool",
|
|
16
|
-
spin: context.spin,
|
|
17
|
-
reducedMotion: context.reducedMotion,
|
|
18
16
|
now: context.now,
|
|
19
17
|
});
|
|
20
18
|
case "notice":
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { trailingText } from "../../text-boundary.js";
|
|
1
2
|
import { blank, row } from "../../ui/render.js";
|
|
2
3
|
import { markdown } from "../../ui/markdown.js";
|
|
3
4
|
const PAD = 1;
|
|
@@ -42,9 +43,5 @@ export function reasoningPreviewSource(text, width) {
|
|
|
42
43
|
return { text, truncated: false };
|
|
43
44
|
// A compact view only needs its visible tail. The complete text remains on
|
|
44
45
|
// the block for expansion after the reasoning stream is sealed.
|
|
45
|
-
|
|
46
|
-
const code = text.charCodeAt(start);
|
|
47
|
-
if (code >= 0xdc00 && code <= 0xdfff)
|
|
48
|
-
start--;
|
|
49
|
-
return { text: text.slice(start), truncated: true };
|
|
46
|
+
return { text: trailingText(text, limit), truncated: true };
|
|
50
47
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// A compact execution trace: state and identity on one rail, evidence below.
|
|
2
2
|
import { hasColor, row } from "../../ui/render.js";
|
|
3
|
+
import { graphemeCeiling, graphemeFloor } from "../../text-boundary.js";
|
|
3
4
|
const OUTPUT_ROWS = 8;
|
|
4
5
|
const LIVE_OUTPUT_ROWS = 6;
|
|
5
6
|
const DIFF_ROWS = 15;
|
|
@@ -10,7 +11,7 @@ export function renderTool(block, width, pal, context = {}) {
|
|
|
10
11
|
...(context.continues === true ? [] : [""]),
|
|
11
12
|
row(width, [
|
|
12
13
|
{ text: " " },
|
|
13
|
-
{ text: `${stateGlyph(block
|
|
14
|
+
{ text: `${stateGlyph(block)} `, fg: statusInk(block.tone, pal), bold: true },
|
|
14
15
|
{ text: block.name, fg: pal.ink.bright, bold: true },
|
|
15
16
|
...(block.target === "" ? [] : [{ text: ` ${block.target}`, fg: pal.technical }]),
|
|
16
17
|
], right === "" ? [] : [{ text: right, fg: statusInk(block.tone, pal) }]),
|
|
@@ -73,21 +74,21 @@ function renderDetail(detail, tone, width, pal) {
|
|
|
73
74
|
function emphasized(text, emphasis, fg) {
|
|
74
75
|
if (emphasis === undefined || emphasis.length <= 0)
|
|
75
76
|
return [{ text, fg }];
|
|
76
|
-
const
|
|
77
|
-
const
|
|
77
|
+
const requestedStart = Math.max(0, Math.min(text.length, emphasis.start));
|
|
78
|
+
const requestedEnd = Math.max(requestedStart, Math.min(text.length, requestedStart + emphasis.length));
|
|
79
|
+
const start = graphemeFloor(text, requestedStart);
|
|
80
|
+
const end = graphemeCeiling(text, requestedEnd);
|
|
81
|
+
if (start === end)
|
|
82
|
+
return [{ text, fg }];
|
|
78
83
|
return [
|
|
79
84
|
...(start === 0 ? [] : [{ text: text.slice(0, start), fg }]),
|
|
80
85
|
{ text: text.slice(start, end), fg, inverse: true },
|
|
81
86
|
...(end === text.length ? [] : [{ text: text.slice(end), fg }]),
|
|
82
87
|
];
|
|
83
88
|
}
|
|
84
|
-
function stateGlyph(block
|
|
85
|
-
if (block.tone === "pending")
|
|
86
|
-
|
|
87
|
-
return "◌";
|
|
88
|
-
const frames = ["◐", "◓", "◑", "◒"];
|
|
89
|
-
return frames[Math.floor((context.spin ?? 0) / 2) % frames.length];
|
|
90
|
-
}
|
|
89
|
+
function stateGlyph(block) {
|
|
90
|
+
if (block.tone === "pending")
|
|
91
|
+
return "◌";
|
|
91
92
|
if (block.tone === "fail")
|
|
92
93
|
return hasColor() ? "●" : "×";
|
|
93
94
|
if (block.tone === "deny")
|
package/dist/tui/session-view.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
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
|
-
export function controllerOptions(session, tools = session.tools) {
|
|
4
|
+
export function controllerOptions(session, contextPolicy, tools = session.tools) {
|
|
5
5
|
return {
|
|
6
6
|
provider: session.provider,
|
|
7
7
|
tools,
|
|
8
8
|
model: session.model,
|
|
9
9
|
system: session.system,
|
|
10
10
|
maxTokens: session.config.maxTokens,
|
|
11
|
+
contextPolicy,
|
|
11
12
|
effort: session.config.effort,
|
|
12
13
|
maxSteps: session.config.maxSteps,
|
|
13
14
|
toolContext: { root: session.config.root },
|
|
@@ -1,138 +1,210 @@
|
|
|
1
1
|
// Incremental transcript layout for long-running sessions.
|
|
2
2
|
import { render } from "./blocks.js";
|
|
3
|
+
const MAX_LAYOUTS = 2;
|
|
4
|
+
const REFLOW_BLOCKS_PER_FRAME = 128;
|
|
3
5
|
/**
|
|
4
|
-
* Keep
|
|
6
|
+
* Keep recent widths and reflow a bounded working set on each frame.
|
|
5
7
|
*
|
|
6
|
-
* The
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* The visible window is always rendered before it is returned. Older rows use
|
|
9
|
+
* their last known height only to guide scrolling while the remaining blocks
|
|
10
|
+
* are refreshed over later frames. This keeps resize responsive without a
|
|
11
|
+
* worker, hidden loop, or unbounded multi-width cache.
|
|
10
12
|
*/
|
|
11
13
|
export function transcriptRenderer(draw = render) {
|
|
12
|
-
let
|
|
13
|
-
let layoutWidth = -1;
|
|
14
|
-
let layoutPalette;
|
|
15
|
-
let entries = [];
|
|
16
|
-
let ends = [];
|
|
17
|
-
let positions = new WeakMap();
|
|
18
|
-
const dirty = new Set();
|
|
14
|
+
let layouts = [];
|
|
19
15
|
return {
|
|
20
16
|
viewport(blocks, width, height, scroll, palette, state = {}) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
rows.push(...blockRows.slice(Math.max(0, start - at), Math.min(blockRows.length, end - at)));
|
|
34
|
-
at = ends[index] ?? at;
|
|
35
|
-
index++;
|
|
36
|
-
}
|
|
37
|
-
while (rows.length < visibleHeight)
|
|
38
|
-
rows.unshift("");
|
|
39
|
-
return { rows, maxScroll };
|
|
17
|
+
const layout = layoutFor(blocks, width, palette);
|
|
18
|
+
// Reflowing rows below a scroll-locked viewport would move the content
|
|
19
|
+
// being read as estimates become exact. Visible rows still resolve on
|
|
20
|
+
// demand; background work resumes when the user follows the tail again.
|
|
21
|
+
if (scroll === 0)
|
|
22
|
+
reflowBatch(layout, state);
|
|
23
|
+
const window = reflowWindow(layout, Math.max(0, height), scroll, state);
|
|
24
|
+
return {
|
|
25
|
+
rows: visibleRows(layout, window, Math.max(0, height)),
|
|
26
|
+
maxScroll: window.maxScroll,
|
|
27
|
+
pending: scroll === 0 && layout.pending > 0,
|
|
28
|
+
};
|
|
40
29
|
},
|
|
41
30
|
invalidate(block) {
|
|
42
|
-
if (block
|
|
43
|
-
|
|
31
|
+
if (block === undefined) {
|
|
32
|
+
layouts = [];
|
|
44
33
|
return;
|
|
45
34
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
35
|
+
for (const layout of layouts) {
|
|
36
|
+
const index = layout.positions.get(block);
|
|
37
|
+
if (index === undefined)
|
|
38
|
+
continue;
|
|
39
|
+
const entry = layout.entries[index];
|
|
40
|
+
if (entry?.rows !== undefined) {
|
|
41
|
+
entry.rows = undefined;
|
|
42
|
+
layout.pending++;
|
|
43
|
+
}
|
|
44
|
+
layout.next = Math.max(layout.next, index);
|
|
45
|
+
}
|
|
51
46
|
},
|
|
52
47
|
};
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
48
|
+
function layoutFor(blocks, width, palette) {
|
|
49
|
+
const cachedAt = layouts.findIndex((layout) => layout.width === width && layout.palette === palette);
|
|
50
|
+
if (cachedAt !== -1) {
|
|
51
|
+
const cached = layouts[cachedAt];
|
|
52
|
+
layouts.splice(cachedAt, 1);
|
|
53
|
+
if (compatible(cached, blocks)) {
|
|
54
|
+
append(cached, blocks);
|
|
55
|
+
layouts.push(cached);
|
|
56
|
+
return cached;
|
|
57
|
+
}
|
|
61
58
|
}
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
const seed = [...layouts].reverse().find((layout) => compatiblePrefix(layout, blocks));
|
|
60
|
+
const created = createLayout(blocks, width, palette, seed);
|
|
61
|
+
layouts.push(created);
|
|
62
|
+
if (layouts.length > MAX_LAYOUTS)
|
|
63
|
+
layouts.shift();
|
|
64
|
+
return created;
|
|
64
65
|
}
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
66
|
+
function createLayout(blocks, width, palette, seed) {
|
|
67
|
+
const layout = {
|
|
68
|
+
source: blocks,
|
|
69
|
+
width,
|
|
70
|
+
palette,
|
|
71
|
+
entries: [],
|
|
72
|
+
ends: [],
|
|
73
|
+
positions: new WeakMap(),
|
|
74
|
+
pending: 0,
|
|
75
|
+
next: -1,
|
|
76
|
+
};
|
|
77
|
+
append(layout, blocks, seed);
|
|
78
|
+
return layout;
|
|
74
79
|
}
|
|
75
|
-
function append(
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
function append(layout, blocks, seed) {
|
|
81
|
+
const firstAdded = layout.entries.length;
|
|
82
|
+
let total = layout.ends.at(-1) ?? 0;
|
|
83
|
+
for (let index = firstAdded; index < blocks.length; index++) {
|
|
78
84
|
const block = blocks[index];
|
|
79
85
|
if (block === undefined)
|
|
80
86
|
continue;
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
const seeded = seed?.entries[index];
|
|
88
|
+
const rowCount = seeded?.block === block ? seeded.rowCount : estimatedRows(block);
|
|
89
|
+
layout.entries.push({ block, rowCount });
|
|
90
|
+
layout.positions.set(block, index);
|
|
91
|
+
layout.pending++;
|
|
92
|
+
total += rowCount;
|
|
93
|
+
layout.ends.push(total);
|
|
94
|
+
}
|
|
95
|
+
if (layout.entries.length > firstAdded) {
|
|
96
|
+
layout.next = Math.max(layout.next, layout.entries.length - 1);
|
|
87
97
|
}
|
|
88
98
|
}
|
|
89
|
-
function
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (changed.length === 0)
|
|
98
|
-
return;
|
|
99
|
-
const first = changed[0];
|
|
100
|
-
for (const index of changed) {
|
|
101
|
-
const entry = entries[index];
|
|
102
|
-
if (entry !== undefined) {
|
|
103
|
-
entry.rows = draw(entry.block, width, palette, {
|
|
104
|
-
...state,
|
|
105
|
-
previous: entries[index - 1]?.block,
|
|
106
|
-
});
|
|
99
|
+
function reflowBatch(layout, state) {
|
|
100
|
+
let remaining = REFLOW_BLOCKS_PER_FRAME;
|
|
101
|
+
let firstChanged = layout.entries.length;
|
|
102
|
+
let index = layout.next;
|
|
103
|
+
while (index >= 0 && remaining > 0 && layout.pending > 0) {
|
|
104
|
+
if (renderEntry(layout, index, state)) {
|
|
105
|
+
firstChanged = Math.min(firstChanged, index);
|
|
106
|
+
remaining--;
|
|
107
107
|
}
|
|
108
|
+
index--;
|
|
108
109
|
}
|
|
109
|
-
|
|
110
|
+
layout.next = index;
|
|
111
|
+
if (firstChanged < layout.entries.length)
|
|
112
|
+
recomputeEnds(layout, firstChanged);
|
|
110
113
|
}
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
function reflowWindow(layout, height, scroll, state) {
|
|
115
|
+
while (true) {
|
|
116
|
+
const window = visibleWindow(layout, height, scroll);
|
|
117
|
+
let firstChanged = layout.entries.length;
|
|
118
|
+
for (let index = window.first; index <= window.last; index++) {
|
|
119
|
+
if (renderEntry(layout, index, state))
|
|
120
|
+
firstChanged = Math.min(firstChanged, index);
|
|
121
|
+
}
|
|
122
|
+
if (firstChanged === layout.entries.length)
|
|
123
|
+
return window;
|
|
124
|
+
recomputeEnds(layout, firstChanged);
|
|
116
125
|
}
|
|
117
126
|
}
|
|
118
|
-
function
|
|
119
|
-
|
|
127
|
+
function renderEntry(layout, index, state) {
|
|
128
|
+
const entry = layout.entries[index];
|
|
129
|
+
if (entry === undefined || entry.rows !== undefined)
|
|
120
130
|
return false;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
entries[
|
|
124
|
-
|
|
131
|
+
entry.rows = draw(entry.block, layout.width, layout.palette, {
|
|
132
|
+
...state,
|
|
133
|
+
previous: layout.entries[index - 1]?.block,
|
|
134
|
+
});
|
|
135
|
+
entry.rowCount = entry.rows.length;
|
|
136
|
+
layout.pending--;
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
function recomputeEnds(layout, from) {
|
|
140
|
+
let total = from === 0 ? 0 : (layout.ends[from - 1] ?? 0);
|
|
141
|
+
for (let index = from; index < layout.entries.length; index++) {
|
|
142
|
+
total += layout.entries[index]?.rowCount ?? 0;
|
|
143
|
+
layout.ends[index] = total;
|
|
144
|
+
}
|
|
125
145
|
}
|
|
126
|
-
function
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
146
|
+
function visibleWindow(layout, height, scroll) {
|
|
147
|
+
const totalRows = layout.ends.at(-1) ?? 0;
|
|
148
|
+
const maxScroll = Math.max(0, totalRows - height);
|
|
149
|
+
const offset = Math.min(Math.max(0, scroll), maxScroll);
|
|
150
|
+
const start = Math.max(0, totalRows - height - offset);
|
|
151
|
+
const end = start + height;
|
|
152
|
+
const first = firstEndingAfter(layout.ends, start);
|
|
153
|
+
let last = first - 1;
|
|
154
|
+
let at = first === 0 ? 0 : (layout.ends[first - 1] ?? 0);
|
|
155
|
+
while (last + 1 < layout.entries.length && at < end) {
|
|
156
|
+
last++;
|
|
157
|
+
at = layout.ends[last] ?? at;
|
|
135
158
|
}
|
|
136
|
-
return
|
|
159
|
+
return { first, last, start, end, maxScroll };
|
|
160
|
+
}
|
|
161
|
+
function visibleRows(layout, window, height) {
|
|
162
|
+
const rows = [];
|
|
163
|
+
let index = window.first;
|
|
164
|
+
let at = index === 0 ? 0 : (layout.ends[index - 1] ?? 0);
|
|
165
|
+
while (index <= window.last && at < window.end) {
|
|
166
|
+
const blockRows = layout.entries[index]?.rows ?? [];
|
|
167
|
+
rows.push(...blockRows.slice(Math.max(0, window.start - at), Math.min(blockRows.length, window.end - at)));
|
|
168
|
+
at = layout.ends[index] ?? at;
|
|
169
|
+
index++;
|
|
170
|
+
}
|
|
171
|
+
while (rows.length < height)
|
|
172
|
+
rows.unshift("");
|
|
173
|
+
return rows;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function compatible(layout, blocks) {
|
|
177
|
+
return layout.source === blocks &&
|
|
178
|
+
blocks.length >= layout.entries.length &&
|
|
179
|
+
!edgeChanged(layout, blocks);
|
|
180
|
+
}
|
|
181
|
+
function compatiblePrefix(layout, blocks) {
|
|
182
|
+
if (layout.entries.length === 0 || blocks.length === 0)
|
|
183
|
+
return true;
|
|
184
|
+
const shared = Math.min(layout.entries.length, blocks.length);
|
|
185
|
+
return layout.entries[0]?.block === blocks[0] &&
|
|
186
|
+
layout.entries[shared - 1]?.block === blocks[shared - 1];
|
|
187
|
+
}
|
|
188
|
+
function edgeChanged(layout, blocks) {
|
|
189
|
+
if (layout.entries.length === 0)
|
|
190
|
+
return false;
|
|
191
|
+
const lastShared = Math.min(blocks.length, layout.entries.length) - 1;
|
|
192
|
+
return lastShared < 0 ||
|
|
193
|
+
layout.entries[0]?.block !== blocks[0] ||
|
|
194
|
+
layout.entries[lastShared]?.block !== blocks[lastShared];
|
|
195
|
+
}
|
|
196
|
+
function estimatedRows(block) {
|
|
197
|
+
return block.kind === "user" ? 4 : 2;
|
|
198
|
+
}
|
|
199
|
+
function firstEndingAfter(ends, row) {
|
|
200
|
+
let low = 0;
|
|
201
|
+
let high = ends.length;
|
|
202
|
+
while (low < high) {
|
|
203
|
+
const middle = Math.floor((low + high) / 2);
|
|
204
|
+
if ((ends[middle] ?? 0) <= row)
|
|
205
|
+
low = middle + 1;
|
|
206
|
+
else
|
|
207
|
+
high = middle;
|
|
137
208
|
}
|
|
209
|
+
return low;
|
|
138
210
|
}
|
package/dist/tui/turn.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
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
|
|
@@ -57,6 +58,9 @@ export function transcribe(stage) {
|
|
|
57
58
|
onUsage(usage) {
|
|
58
59
|
stage.usage?.(usage);
|
|
59
60
|
},
|
|
61
|
+
onRequestInput(inputTokens) {
|
|
62
|
+
stage.requestInput?.(inputTokens);
|
|
63
|
+
},
|
|
60
64
|
onStatus(status) {
|
|
61
65
|
stage.status(status);
|
|
62
66
|
stage.render();
|
|
@@ -249,18 +253,35 @@ function emphasizePairs(rows) {
|
|
|
249
253
|
continue;
|
|
250
254
|
if (rows[index - 1]?.kind === "del" || rows[index + 2]?.kind === "add")
|
|
251
255
|
continue;
|
|
256
|
+
const removedClusters = graphemes(removed.text);
|
|
257
|
+
const addedClusters = graphemes(added.text);
|
|
258
|
+
let prefix = 0;
|
|
252
259
|
let start = 0;
|
|
253
|
-
while (
|
|
254
|
-
|
|
260
|
+
while (prefix < removedClusters.length &&
|
|
261
|
+
prefix < addedClusters.length &&
|
|
262
|
+
removedClusters[prefix] === addedClusters[prefix]) {
|
|
263
|
+
start += removedClusters[prefix].length;
|
|
264
|
+
prefix++;
|
|
265
|
+
}
|
|
255
266
|
let suffix = 0;
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
267
|
+
let removedSuffix = 0;
|
|
268
|
+
let addedSuffix = 0;
|
|
269
|
+
while (suffix < removedClusters.length - prefix &&
|
|
270
|
+
suffix < addedClusters.length - prefix &&
|
|
271
|
+
removedClusters[removedClusters.length - 1 - suffix] ===
|
|
272
|
+
addedClusters[addedClusters.length - 1 - suffix]) {
|
|
273
|
+
removedSuffix += removedClusters[removedClusters.length - 1 - suffix].length;
|
|
274
|
+
addedSuffix += addedClusters[addedClusters.length - 1 - suffix].length;
|
|
259
275
|
suffix++;
|
|
260
|
-
|
|
276
|
+
}
|
|
277
|
+
while (suffix > 0 &&
|
|
278
|
+
(!wordBoundary(removed.text, removedSuffix) || !wordBoundary(added.text, addedSuffix))) {
|
|
279
|
+
removedSuffix -= removedClusters[removedClusters.length - suffix].length;
|
|
280
|
+
addedSuffix -= addedClusters[addedClusters.length - suffix].length;
|
|
261
281
|
suffix--;
|
|
262
|
-
|
|
263
|
-
const
|
|
282
|
+
}
|
|
283
|
+
const removedLength = removed.text.length - start - removedSuffix;
|
|
284
|
+
const addedLength = added.text.length - start - addedSuffix;
|
|
264
285
|
if (removedLength > 0)
|
|
265
286
|
removed.emphasis = { start, length: removedLength };
|
|
266
287
|
if (addedLength > 0)
|
package/dist/tui/view.js
CHANGED
|
@@ -25,11 +25,16 @@ 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, {
|
|
28
|
+
const viewport = transcript.viewport(view.blocks, width, transcriptHeight, view.scroll, view.pal, { now: view.now });
|
|
29
29
|
const cursor = dock.cursor === undefined
|
|
30
30
|
? undefined
|
|
31
31
|
: { row: transcriptHeight + dock.cursor.row, col: dock.cursor.col };
|
|
32
|
-
return {
|
|
32
|
+
return {
|
|
33
|
+
rows: [...viewport.rows, ...dock.rows],
|
|
34
|
+
cursor,
|
|
35
|
+
maxScroll: viewport.maxScroll,
|
|
36
|
+
transcriptPending: viewport.pending,
|
|
37
|
+
};
|
|
33
38
|
}
|
|
34
39
|
function dockRows(view, width, height) {
|
|
35
40
|
const status = renderStatus({
|
|
@@ -82,5 +87,5 @@ function tooSmall(height, width, view) {
|
|
|
82
87
|
{ text: elide(`need ${MIN_COLS}×${MIN_ROWS}`, Math.max(1, width)), fg: view.pal.ink.dim },
|
|
83
88
|
]);
|
|
84
89
|
}
|
|
85
|
-
return { rows, maxScroll: 0 };
|
|
90
|
+
return { rows, maxScroll: 0, transcriptPending: false };
|
|
86
91
|
}
|