@giovannijecha/jecode 0.8.5 → 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 +22 -5
- package/dist/batch-view.js +27 -2
- package/dist/batch.js +2 -0
- package/dist/cli-info.js +0 -1
- package/dist/config.js +14 -6
- package/dist/credential-commands.js +3 -3
- package/dist/openai-account-command.js +14 -12
- 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/provider-commands.js +1 -33
- package/dist/provider-errors.js +1 -10
- package/dist/provider-label.js +3 -10
- package/dist/providers/anthropic.js +0 -1
- 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.js +13 -147
- package/dist/providers/openai-codex.js +4 -4
- package/dist/providers/openai.js +0 -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-command.js +10 -5
- package/dist/settings.js +16 -15
- package/dist/start.js +1 -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 -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/app-workflows.js +8 -361
- 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/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/help.js +1 -1
- package/dist/tui/picker-layout.js +40 -0
- package/dist/tui/picker.js +7 -71
- 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/package.json +12 -12
- package/dist/ollama-settings-command.js +0 -74
- package/dist/tui/motion.js +0 -32
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();
|
|
@@ -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
|
+
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Shared transcript inset and a bounded measure for model prose and tool records.
|
|
2
2
|
const GUTTER = 2;
|
|
3
|
+
const MODEL_MEASURE = 86;
|
|
4
|
+
export function modelTranscriptWidth(width) {
|
|
5
|
+
return Math.max(8, Math.min(width - GUTTER * 2, MODEL_MEASURE));
|
|
6
|
+
}
|
|
7
|
+
export function insetTranscript(rows) {
|
|
8
|
+
return rows.map((line) => line === "" ? "" : `${" ".repeat(GUTTER)}${line}`);
|
|
9
|
+
}
|
|
3
10
|
export function transcriptGutter(_width) {
|
|
4
11
|
return GUTTER;
|
|
5
12
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Incremental transcript layout for long-running sessions.
|
|
2
|
+
import { hasColor } from "../ui/render.js";
|
|
3
|
+
import { runningTool } from "./components/tool-motion.js";
|
|
2
4
|
import { render } from "./blocks.js";
|
|
3
|
-
import { TOOL_BIRTH_MS, TOOL_LEADER_MAX_MS, TOOL_ROW_ARRIVAL_MS, TOOL_SETTLE_MS, } from "./motion.js";
|
|
4
5
|
const MAX_LAYOUTS = 2;
|
|
5
6
|
const REFLOW_BLOCKS_PER_FRAME = 128;
|
|
6
7
|
/**
|
|
@@ -13,18 +14,13 @@ const REFLOW_BLOCKS_PER_FRAME = 128;
|
|
|
13
14
|
*/
|
|
14
15
|
export function transcriptRenderer(draw = render, clock = Date.now) {
|
|
15
16
|
let layouts = [];
|
|
16
|
-
const
|
|
17
|
-
const activeMotion = new Set();
|
|
17
|
+
const activeTools = new Set();
|
|
18
18
|
return {
|
|
19
19
|
viewport(blocks, width, height, scroll, palette, state = {}) {
|
|
20
20
|
const layout = layoutFor(blocks, width, palette);
|
|
21
|
-
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
else if (scroll === 0) {
|
|
26
|
-
invalidateMovingEntries(layout, now);
|
|
27
|
-
}
|
|
21
|
+
state = { ...state, now: state.now ?? clock() };
|
|
22
|
+
if (scroll === 0)
|
|
23
|
+
invalidateRunningEntries(layout);
|
|
28
24
|
// Reflowing rows below a scroll-locked viewport would move the content
|
|
29
25
|
// being read as estimates become exact. Visible rows still resolve on
|
|
30
26
|
// demand; background work resumes when the user follows the tail again.
|
|
@@ -35,15 +31,19 @@ export function transcriptRenderer(draw = render, clock = Date.now) {
|
|
|
35
31
|
rows: visibleRows(layout, window, Math.max(0, height)),
|
|
36
32
|
maxScroll: window.maxScroll,
|
|
37
33
|
pending: scroll === 0 && layout.pending > 0,
|
|
38
|
-
animating: scroll === 0 && state.reducedMotion !== true &&
|
|
34
|
+
animating: scroll === 0 && state.reducedMotion !== true && hasColor() && hasRunningEntry(layout, window),
|
|
39
35
|
};
|
|
40
36
|
},
|
|
41
37
|
invalidate(block) {
|
|
42
38
|
if (block === undefined) {
|
|
43
39
|
layouts = [];
|
|
40
|
+
activeTools.clear();
|
|
44
41
|
return;
|
|
45
42
|
}
|
|
46
|
-
|
|
43
|
+
if (runningTool(block))
|
|
44
|
+
activeTools.add(block);
|
|
45
|
+
else
|
|
46
|
+
activeTools.delete(block);
|
|
47
47
|
for (const layout of layouts) {
|
|
48
48
|
const index = layout.positions.get(block);
|
|
49
49
|
if (index === undefined)
|
|
@@ -95,6 +95,8 @@ export function transcriptRenderer(draw = render, clock = Date.now) {
|
|
|
95
95
|
const rowCount = seeded?.block === block ? seeded.rowCount : estimatedRows(block);
|
|
96
96
|
layout.entries.push({ block, rowCount });
|
|
97
97
|
layout.positions.set(block, index);
|
|
98
|
+
if (runningTool(block))
|
|
99
|
+
activeTools.add(block);
|
|
98
100
|
layout.pending++;
|
|
99
101
|
total += rowCount;
|
|
100
102
|
layout.ends.push(total);
|
|
@@ -138,7 +140,6 @@ export function transcriptRenderer(draw = render, clock = Date.now) {
|
|
|
138
140
|
entry.rows = draw(entry.block, layout.width, layout.palette, {
|
|
139
141
|
...state,
|
|
140
142
|
previous: layout.entries[index - 1]?.block,
|
|
141
|
-
motion: motions.get(entry.block),
|
|
142
143
|
});
|
|
143
144
|
entry.rowCount = entry.rows.length;
|
|
144
145
|
layout.pending--;
|
|
@@ -188,115 +189,28 @@ export function transcriptRenderer(draw = render, clock = Date.now) {
|
|
|
188
189
|
}
|
|
189
190
|
layout.next = Math.max(layout.next, index);
|
|
190
191
|
}
|
|
191
|
-
function
|
|
192
|
-
|
|
193
|
-
return;
|
|
194
|
-
let motion = motions.get(block);
|
|
195
|
-
// A settled historical block may be invalidated by Ctrl+O. It should not
|
|
196
|
-
// replay its original arrival animation after resume.
|
|
197
|
-
if (motion === undefined) {
|
|
198
|
-
if (block.tone !== "pending")
|
|
199
|
-
return;
|
|
200
|
-
motion = {
|
|
201
|
-
bornAt: now,
|
|
202
|
-
rowsAt: (block.body ?? []).map(() => now),
|
|
203
|
-
rowKeys: (block.body ?? []).map(detailKey),
|
|
204
|
-
tone: block.tone,
|
|
205
|
-
};
|
|
206
|
-
motions.set(block, motion);
|
|
207
|
-
activeMotion.add(block);
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
if (motion.tone === "pending" && block.tone !== "pending")
|
|
211
|
-
motion.settledAt = now;
|
|
212
|
-
motion.tone = block.tone;
|
|
213
|
-
reconcileRows(motion, block.body ?? [], now);
|
|
214
|
-
activeMotion.add(block);
|
|
215
|
-
}
|
|
216
|
-
function invalidateMovingEntries(layout, now) {
|
|
217
|
-
for (const block of activeMotion) {
|
|
192
|
+
function invalidateRunningEntries(layout) {
|
|
193
|
+
for (const block of activeTools) {
|
|
218
194
|
const index = layout.positions.get(block);
|
|
219
|
-
if (index === undefined) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const motion = motions.get(block);
|
|
224
|
-
if (motion === undefined || !moving(block, motion, now)) {
|
|
225
|
-
// Paint the resting state once before leaving the motion registry;
|
|
226
|
-
// otherwise the cache could retain the final in-between frame.
|
|
227
|
-
dirty(layout, index);
|
|
228
|
-
activeMotion.delete(block);
|
|
195
|
+
if (index === undefined || !runningTool(block)) {
|
|
196
|
+
activeTools.delete(block);
|
|
197
|
+
if (index !== undefined)
|
|
198
|
+
dirty(layout, index);
|
|
229
199
|
continue;
|
|
230
200
|
}
|
|
201
|
+
// Clock-driven repaints keep elapsed time current even without motion.
|
|
231
202
|
dirty(layout, index);
|
|
232
203
|
}
|
|
233
204
|
}
|
|
234
|
-
function
|
|
235
|
-
for (const block of
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (motion !== undefined && moving(block, motion, now))
|
|
205
|
+
function hasRunningEntry(layout, window) {
|
|
206
|
+
for (const block of activeTools) {
|
|
207
|
+
const index = layout.positions.get(block);
|
|
208
|
+
if (index !== undefined && index >= window.first && index <= window.last &&
|
|
209
|
+
runningTool(block) && block.expanded !== true)
|
|
240
210
|
return true;
|
|
241
211
|
}
|
|
242
212
|
return false;
|
|
243
213
|
}
|
|
244
|
-
function discardSuppressedMotion(layout) {
|
|
245
|
-
for (const block of activeMotion) {
|
|
246
|
-
if (layout.positions.get(block) !== undefined && block.kind === "tool" && block.tone === "pending") {
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
activeMotion.delete(block);
|
|
250
|
-
motions.delete(block);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
function reconcileRows(motion, details, now) {
|
|
255
|
-
const available = new Map();
|
|
256
|
-
for (let index = 0; index < motion.rowKeys.length; index++) {
|
|
257
|
-
const key = motion.rowKeys[index];
|
|
258
|
-
if (key === undefined)
|
|
259
|
-
continue;
|
|
260
|
-
const bucket = available.get(key) ?? { values: [], next: 0 };
|
|
261
|
-
bucket.values.push(motion.rowsAt[index] ?? motion.bornAt);
|
|
262
|
-
available.set(key, bucket);
|
|
263
|
-
}
|
|
264
|
-
const rowKeys = details.map(detailKey);
|
|
265
|
-
const rowsAt = rowKeys.map((key) => {
|
|
266
|
-
const bucket = available.get(key);
|
|
267
|
-
if (bucket === undefined || bucket.next >= bucket.values.length)
|
|
268
|
-
return now;
|
|
269
|
-
const value = bucket.values[bucket.next] ?? now;
|
|
270
|
-
bucket.next++;
|
|
271
|
-
return value;
|
|
272
|
-
});
|
|
273
|
-
motion.rowKeys = rowKeys;
|
|
274
|
-
motion.rowsAt = rowsAt;
|
|
275
|
-
}
|
|
276
|
-
function detailKey(detail) {
|
|
277
|
-
if (detail.kind === "out" || detail.kind === "gap") {
|
|
278
|
-
return JSON.stringify([detail.kind, detail.text]);
|
|
279
|
-
}
|
|
280
|
-
return JSON.stringify([
|
|
281
|
-
detail.kind,
|
|
282
|
-
detail.oldLine ?? "",
|
|
283
|
-
detail.newLine ?? "",
|
|
284
|
-
detail.emphasis?.start ?? "",
|
|
285
|
-
detail.emphasis?.length ?? "",
|
|
286
|
-
detail.text,
|
|
287
|
-
]);
|
|
288
|
-
}
|
|
289
|
-
function moving(block, motion, now) {
|
|
290
|
-
if (block.kind === "tool" && block.tone === "pending" &&
|
|
291
|
-
block.startedAt !== undefined && block.right === "running")
|
|
292
|
-
return true;
|
|
293
|
-
let until = motion.bornAt + Math.max(TOOL_BIRTH_MS, TOOL_LEADER_MAX_MS);
|
|
294
|
-
if (motion.settledAt !== undefined)
|
|
295
|
-
until = Math.max(until, motion.settledAt + TOOL_SETTLE_MS);
|
|
296
|
-
for (const arrivedAt of motion.rowsAt) {
|
|
297
|
-
until = Math.max(until, arrivedAt + TOOL_ROW_ARRIVAL_MS);
|
|
298
|
-
}
|
|
299
|
-
return now < until;
|
|
300
214
|
}
|
|
301
215
|
function compatible(layout, blocks) {
|
|
302
216
|
return layout.source === blocks &&
|