@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,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,17 @@ 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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
discardSuppressedMotion(layout);
|
|
24
|
-
}
|
|
25
|
-
else if (scroll === 0) {
|
|
26
|
-
invalidateMovingEntries(layout, now);
|
|
20
|
+
if (blocks.length === 0 && layouts.some((layout) => layout.entries.length > 0)) {
|
|
21
|
+
layouts = [];
|
|
22
|
+
activeTools.clear();
|
|
27
23
|
}
|
|
24
|
+
const layout = layoutFor(blocks, width, palette);
|
|
25
|
+
state = { ...state, now: state.now ?? clock() };
|
|
26
|
+
if (scroll === 0)
|
|
27
|
+
invalidateRunningEntries(layout);
|
|
28
28
|
// Reflowing rows below a scroll-locked viewport would move the content
|
|
29
29
|
// being read as estimates become exact. Visible rows still resolve on
|
|
30
30
|
// demand; background work resumes when the user follows the tail again.
|
|
@@ -35,15 +35,19 @@ export function transcriptRenderer(draw = render, clock = Date.now) {
|
|
|
35
35
|
rows: visibleRows(layout, window, Math.max(0, height)),
|
|
36
36
|
maxScroll: window.maxScroll,
|
|
37
37
|
pending: scroll === 0 && layout.pending > 0,
|
|
38
|
-
animating: scroll === 0 && state.reducedMotion !== true &&
|
|
38
|
+
animating: scroll === 0 && state.reducedMotion !== true && hasColor() && hasRunningEntry(layout, window),
|
|
39
39
|
};
|
|
40
40
|
},
|
|
41
41
|
invalidate(block) {
|
|
42
42
|
if (block === undefined) {
|
|
43
43
|
layouts = [];
|
|
44
|
+
activeTools.clear();
|
|
44
45
|
return;
|
|
45
46
|
}
|
|
46
|
-
|
|
47
|
+
if (runningTool(block))
|
|
48
|
+
activeTools.add(block);
|
|
49
|
+
else
|
|
50
|
+
activeTools.delete(block);
|
|
47
51
|
for (const layout of layouts) {
|
|
48
52
|
const index = layout.positions.get(block);
|
|
49
53
|
if (index === undefined)
|
|
@@ -95,6 +99,8 @@ export function transcriptRenderer(draw = render, clock = Date.now) {
|
|
|
95
99
|
const rowCount = seeded?.block === block ? seeded.rowCount : estimatedRows(block);
|
|
96
100
|
layout.entries.push({ block, rowCount });
|
|
97
101
|
layout.positions.set(block, index);
|
|
102
|
+
if (runningTool(block))
|
|
103
|
+
activeTools.add(block);
|
|
98
104
|
layout.pending++;
|
|
99
105
|
total += rowCount;
|
|
100
106
|
layout.ends.push(total);
|
|
@@ -138,7 +144,6 @@ export function transcriptRenderer(draw = render, clock = Date.now) {
|
|
|
138
144
|
entry.rows = draw(entry.block, layout.width, layout.palette, {
|
|
139
145
|
...state,
|
|
140
146
|
previous: layout.entries[index - 1]?.block,
|
|
141
|
-
motion: motions.get(entry.block),
|
|
142
147
|
});
|
|
143
148
|
entry.rowCount = entry.rows.length;
|
|
144
149
|
layout.pending--;
|
|
@@ -188,115 +193,28 @@ export function transcriptRenderer(draw = render, clock = Date.now) {
|
|
|
188
193
|
}
|
|
189
194
|
layout.next = Math.max(layout.next, index);
|
|
190
195
|
}
|
|
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) {
|
|
196
|
+
function invalidateRunningEntries(layout) {
|
|
197
|
+
for (const block of activeTools) {
|
|
218
198
|
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);
|
|
199
|
+
if (index === undefined || !runningTool(block)) {
|
|
200
|
+
activeTools.delete(block);
|
|
201
|
+
if (index !== undefined)
|
|
202
|
+
dirty(layout, index);
|
|
229
203
|
continue;
|
|
230
204
|
}
|
|
205
|
+
// Clock-driven repaints keep elapsed time current even without motion.
|
|
231
206
|
dirty(layout, index);
|
|
232
207
|
}
|
|
233
208
|
}
|
|
234
|
-
function
|
|
235
|
-
for (const block of
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (motion !== undefined && moving(block, motion, now))
|
|
209
|
+
function hasRunningEntry(layout, window) {
|
|
210
|
+
for (const block of activeTools) {
|
|
211
|
+
const index = layout.positions.get(block);
|
|
212
|
+
if (index !== undefined && index >= window.first && index <= window.last &&
|
|
213
|
+
runningTool(block) && block.expanded !== true)
|
|
240
214
|
return true;
|
|
241
215
|
}
|
|
242
216
|
return false;
|
|
243
217
|
}
|
|
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
218
|
}
|
|
301
219
|
function compatible(layout, blocks) {
|
|
302
220
|
return layout.source === blocks &&
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// One model turn, including steering, compaction, durable settlement, and recovery.
|
|
2
|
+
import { runTurn } from "../controller.js";
|
|
3
|
+
import { resolveContextPolicy } from "../context/capacity.js";
|
|
4
|
+
import { contextManager } from "../context/manager.js";
|
|
5
|
+
import { steeringInbox } from "../steering.js";
|
|
6
|
+
import { recordAuxiliaryUsage, recordRequestInput, recordUsage } from "../usage.js";
|
|
7
|
+
import { toolSpecs } from "../tools/index.js";
|
|
8
|
+
import { requestIdentityForSession } from "../request-identity.js";
|
|
9
|
+
import { transition } from "./activity.js";
|
|
10
|
+
import { answerAt } from "./approve.js";
|
|
11
|
+
import * as edit from "./editor.js";
|
|
12
|
+
import { controllerOptions, footerInfo, turnFailure } from "./session-view.js";
|
|
13
|
+
import { transcribe } from "./turn.js";
|
|
14
|
+
const WAITING = "Waiting";
|
|
15
|
+
export function turnWorkflow(options, automaticCompaction, inputs) {
|
|
16
|
+
const { session, state, permissions, feedback } = options;
|
|
17
|
+
let activeSteering;
|
|
18
|
+
async function turn(text) {
|
|
19
|
+
const activity = options.startActivity("turn", WAITING);
|
|
20
|
+
if (activity === undefined)
|
|
21
|
+
return;
|
|
22
|
+
// Settings menus remain live, but every request, compaction, and checkpoint
|
|
23
|
+
// in this turn must retain the selection with which the turn started.
|
|
24
|
+
const runtime = { ...session, config: { ...session.config } };
|
|
25
|
+
const requestIdentity = requestIdentityForSession(session);
|
|
26
|
+
state.turnFooter = footerInfo(runtime);
|
|
27
|
+
const inbox = steeringInbox((pending, accepting) => {
|
|
28
|
+
if (activeSteering !== inbox)
|
|
29
|
+
return;
|
|
30
|
+
state.steering = accepting ? pending : undefined;
|
|
31
|
+
options.render();
|
|
32
|
+
});
|
|
33
|
+
activeSteering = inbox;
|
|
34
|
+
state.steering = 0;
|
|
35
|
+
const status = (label) => transition(activity, label);
|
|
36
|
+
const parentId = session.conversation.activeNodeId;
|
|
37
|
+
const history = session.conversation.history;
|
|
38
|
+
const modelHistory = session.conversation.contextHistory;
|
|
39
|
+
const historyStart = history.length;
|
|
40
|
+
const blockStart = state.blocks.length;
|
|
41
|
+
const createdAt = new Date().toISOString();
|
|
42
|
+
const prospectiveNodeId = session.conversation.nodes.length + 1;
|
|
43
|
+
let nodeId;
|
|
44
|
+
let context;
|
|
45
|
+
const unpersistedSteering = [];
|
|
46
|
+
const turnTools = permissions.availableTools();
|
|
47
|
+
const specs = toolSpecs(turnTools);
|
|
48
|
+
let firstPolicy = true;
|
|
49
|
+
const policy = () => {
|
|
50
|
+
let visible = firstPolicy;
|
|
51
|
+
firstPolicy = false;
|
|
52
|
+
if (visible) {
|
|
53
|
+
status("Checking context");
|
|
54
|
+
options.render();
|
|
55
|
+
}
|
|
56
|
+
return resolveContextPolicy({
|
|
57
|
+
provider: runtime.provider,
|
|
58
|
+
model: runtime.model,
|
|
59
|
+
compactionPercent: runtime.config.compactionPercent,
|
|
60
|
+
signal: activity.control.signal,
|
|
61
|
+
onStatus: (said) => {
|
|
62
|
+
visible = true;
|
|
63
|
+
status(said);
|
|
64
|
+
options.render();
|
|
65
|
+
},
|
|
66
|
+
}).finally(() => {
|
|
67
|
+
if (visible) {
|
|
68
|
+
status(WAITING);
|
|
69
|
+
options.render();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
const requestOptions = {
|
|
74
|
+
...controllerOptions(session, policy, turnTools, inbox),
|
|
75
|
+
inputMeter: inputs.forTurn(runtime.provider, requestIdentity.conversationId),
|
|
76
|
+
toolAllowed: (call) => permissions.allowed(call),
|
|
77
|
+
};
|
|
78
|
+
options.emit({ kind: "user", text });
|
|
79
|
+
const user = { role: "user", content: [{ kind: "text", text }] };
|
|
80
|
+
history.push(user);
|
|
81
|
+
modelHistory.push(structuredClone(user));
|
|
82
|
+
const events = transcribe({
|
|
83
|
+
emit: options.emit,
|
|
84
|
+
render: options.render,
|
|
85
|
+
palette: session.palette,
|
|
86
|
+
approved: (call) => permissions.approved(call),
|
|
87
|
+
remember: (call) => permissions.remember(call),
|
|
88
|
+
ask: (prompt, settle) => {
|
|
89
|
+
state.approval = { picker: prompt, settle: (index) => settle(answerAt(index)) };
|
|
90
|
+
options.render();
|
|
91
|
+
},
|
|
92
|
+
status: (text) => {
|
|
93
|
+
status(text);
|
|
94
|
+
},
|
|
95
|
+
usage: (usage) => recordUsage(session.usage, usage),
|
|
96
|
+
requestInput: (inputTokens) => recordRequestInput(session.usage, inputTokens),
|
|
97
|
+
});
|
|
98
|
+
const persist = async (checkpoint, settlement, failure) => {
|
|
99
|
+
const next = session.conversation.commit({
|
|
100
|
+
...(nodeId === undefined ? {} : { nodeId }),
|
|
101
|
+
parentId,
|
|
102
|
+
createdAt,
|
|
103
|
+
identity: {
|
|
104
|
+
providerId: runtime.provider.id,
|
|
105
|
+
model: runtime.model,
|
|
106
|
+
effort: runtime.config.effort,
|
|
107
|
+
},
|
|
108
|
+
messages: checkpoint.slice(historyStart),
|
|
109
|
+
blocks: state.blocks.slice(blockStart),
|
|
110
|
+
...(context === undefined ? {} : { context }),
|
|
111
|
+
...(failure === undefined ? {} : { failure }),
|
|
112
|
+
}, settlement);
|
|
113
|
+
await session.persistence?.checkpoint(next);
|
|
114
|
+
session.conversation = next;
|
|
115
|
+
nodeId = next.activeNodeId;
|
|
116
|
+
state.committedNodeId = next.activeNodeId;
|
|
117
|
+
unpersistedSteering.length = 0;
|
|
118
|
+
};
|
|
119
|
+
const manager = contextManager({
|
|
120
|
+
provider: runtime.provider,
|
|
121
|
+
model: runtime.model,
|
|
122
|
+
effort: runtime.config.effort,
|
|
123
|
+
system: runtime.system,
|
|
124
|
+
tools: specs,
|
|
125
|
+
maxOutputTokens: runtime.config.maxTokens,
|
|
126
|
+
historyStart,
|
|
127
|
+
nodeId: () => nodeId ?? prospectiveNodeId,
|
|
128
|
+
gate: automaticCompaction,
|
|
129
|
+
identity: requestIdentity,
|
|
130
|
+
signal: activity.control.signal,
|
|
131
|
+
onStatus(active) {
|
|
132
|
+
status(active ? "Compacting" : WAITING);
|
|
133
|
+
options.render();
|
|
134
|
+
},
|
|
135
|
+
onUsage: (usage) => recordAuxiliaryUsage(session.usage, usage),
|
|
136
|
+
});
|
|
137
|
+
events.onContext = async (checkpoint, projected, request) => {
|
|
138
|
+
const result = await manager.compact(checkpoint, projected, request);
|
|
139
|
+
context = manager.anchor;
|
|
140
|
+
return result;
|
|
141
|
+
};
|
|
142
|
+
events.onSteering = (guidance) => {
|
|
143
|
+
unpersistedSteering.push(guidance);
|
|
144
|
+
options.emit({ kind: "user", text: guidance });
|
|
145
|
+
options.render();
|
|
146
|
+
};
|
|
147
|
+
events.onCheckpoint = async (checkpoint, settlement) => {
|
|
148
|
+
await persist(checkpoint, settlement);
|
|
149
|
+
};
|
|
150
|
+
let finishReason;
|
|
151
|
+
let failed;
|
|
152
|
+
try {
|
|
153
|
+
await runTurn(history, requestOptions, events, activity.control.signal, modelHistory);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
inputs.reset();
|
|
157
|
+
const interrupted = activity.control.signal.aborted;
|
|
158
|
+
const completed = nodeId !== undefined && session.conversation.activeNodeId === nodeId &&
|
|
159
|
+
session.conversation.activeNode?.settlement === "completed";
|
|
160
|
+
if (completed) {
|
|
161
|
+
const notice = turnFailure(runtime, error, interrupted);
|
|
162
|
+
feedback.show({ text: notice.text, tone: notice.tone, timeoutMs: 6_000 });
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
finishReason = interrupted ? "interrupted" : "failed";
|
|
166
|
+
failed = { error: error, interrupted };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
let pendingSteering = inbox.close();
|
|
171
|
+
if (activeSteering === inbox)
|
|
172
|
+
activeSteering = undefined;
|
|
173
|
+
state.steering = undefined;
|
|
174
|
+
try {
|
|
175
|
+
events.finish(finishReason);
|
|
176
|
+
if (failed !== undefined) {
|
|
177
|
+
const notice = turnFailure(runtime, failed.error, failed.interrupted);
|
|
178
|
+
const settlement = failed.interrupted ? "interrupted" : "failed";
|
|
179
|
+
const failure = {
|
|
180
|
+
text: notice.text,
|
|
181
|
+
tone: failed.interrupted ? "warn" : "error",
|
|
182
|
+
};
|
|
183
|
+
options.emit(notice);
|
|
184
|
+
try {
|
|
185
|
+
await persist(closeFailedTurn(history, settlement), settlement, failure);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
// A failed persistence boundary cannot remain visible as if it had
|
|
189
|
+
// been saved. Revert to the last durable path and return the input
|
|
190
|
+
// to the composer so the user can retry without losing it.
|
|
191
|
+
options.replaceTranscript();
|
|
192
|
+
state.editor = edit.of([
|
|
193
|
+
...(nodeId === undefined ? [text] : []),
|
|
194
|
+
...unpersistedSteering,
|
|
195
|
+
...pendingSteering,
|
|
196
|
+
state.editor.text,
|
|
197
|
+
].filter((part) => part !== "").join("\n\n"));
|
|
198
|
+
state.completing = undefined;
|
|
199
|
+
pendingSteering = [];
|
|
200
|
+
feedback.show({
|
|
201
|
+
text: error.message,
|
|
202
|
+
tone: "error",
|
|
203
|
+
timeoutMs: 6_000,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
restorePendingSteering(state, pendingSteering);
|
|
210
|
+
options.finishActivity(activity);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function steer(text) {
|
|
215
|
+
return activeSteering?.offer(text) ?? "unavailable";
|
|
216
|
+
}
|
|
217
|
+
return { turn, steer };
|
|
218
|
+
}
|
|
219
|
+
function restorePendingSteering(state, messages) {
|
|
220
|
+
if (messages.length === 0)
|
|
221
|
+
return;
|
|
222
|
+
const pending = messages.join("\n\n");
|
|
223
|
+
state.editor = edit.of(state.editor.text === "" ? pending : `${pending}\n\n${state.editor.text}`);
|
|
224
|
+
state.completing = undefined;
|
|
225
|
+
}
|
|
226
|
+
function closeFailedTurn(history, settlement) {
|
|
227
|
+
const closed = [...history];
|
|
228
|
+
if (closed.at(-1)?.role === "assistant")
|
|
229
|
+
return closed;
|
|
230
|
+
const text = settlement === "interrupted"
|
|
231
|
+
? "The previous attempt was interrupted by the user before completion."
|
|
232
|
+
: "The previous attempt failed before completion.";
|
|
233
|
+
closed.push({ role: "assistant", content: [{ kind: "text", text }] });
|
|
234
|
+
return closed;
|
|
235
|
+
}
|
package/dist/tui/turn.js
CHANGED
|
@@ -3,17 +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";
|
|
7
|
-
import { condense, diff } from "../ui/diff.js";
|
|
8
6
|
import { promptFor } from "./approve.js";
|
|
7
|
+
import { toolTarget, previewDetails, producedDetails, outputDetails } from "./tool-details.js";
|
|
9
8
|
// Semantic activity labels feed the footer's compact state and timer while
|
|
10
9
|
// reasoning and tools keep the detailed work visible in the transcript.
|
|
11
10
|
const WAITING = "Waiting";
|
|
12
11
|
const THINKING = "Thinking";
|
|
13
12
|
const RESPONDING = "Responding";
|
|
14
13
|
const ASKING = "Waiting for you";
|
|
15
|
-
/** Unchanged rows kept either side of a change. */
|
|
16
|
-
const CONTEXT = 2;
|
|
17
14
|
export function transcribe(stage) {
|
|
18
15
|
// The block the stream is currently filling. A change of kind starts a new
|
|
19
16
|
// one, which is what keeps reasoning and answer from running together.
|
|
@@ -104,10 +101,10 @@ export function transcribe(stage) {
|
|
|
104
101
|
const block = {
|
|
105
102
|
kind: "tool",
|
|
106
103
|
name: call.name,
|
|
107
|
-
target:
|
|
104
|
+
target: toolTarget(call.input),
|
|
108
105
|
right: "ready",
|
|
109
106
|
tone: "pending",
|
|
110
|
-
body:
|
|
107
|
+
body: previewDetails(call, look),
|
|
111
108
|
};
|
|
112
109
|
tools.set(call.id, block);
|
|
113
110
|
stage.emit(block);
|
|
@@ -117,7 +114,7 @@ export function transcribe(stage) {
|
|
|
117
114
|
const block = tools.get(call.id);
|
|
118
115
|
if (block === undefined || block.kind !== "tool" || block.tone !== "pending")
|
|
119
116
|
return;
|
|
120
|
-
block.body =
|
|
117
|
+
block.body = outputDetails(output, "out");
|
|
121
118
|
stage.render(block);
|
|
122
119
|
},
|
|
123
120
|
onToolResult(call, result, summary) {
|
|
@@ -138,8 +135,8 @@ export function transcribe(stage) {
|
|
|
138
135
|
// A failure replaces the preview: what the call was going to do stops
|
|
139
136
|
// being the interesting part the moment it did not do it.
|
|
140
137
|
const outcome = result.isError
|
|
141
|
-
?
|
|
142
|
-
:
|
|
138
|
+
? outputDetails(result.output, "out")
|
|
139
|
+
: producedDetails(call, result.output);
|
|
143
140
|
if (outcome !== undefined)
|
|
144
141
|
block.body = outcome;
|
|
145
142
|
stage.render(block);
|
|
@@ -158,7 +155,7 @@ export function transcribe(stage) {
|
|
|
158
155
|
stage.render(block);
|
|
159
156
|
}
|
|
160
157
|
return new Promise((resolve) => {
|
|
161
|
-
stage.ask(promptFor(call,
|
|
158
|
+
stage.ask(promptFor(call, toolTarget(call.input), stage.palette), (answer) => {
|
|
162
159
|
if (answer === "always")
|
|
163
160
|
stage.remember(call);
|
|
164
161
|
const approved = answer !== "no";
|
|
@@ -204,133 +201,3 @@ function settleDuration(block) {
|
|
|
204
201
|
}
|
|
205
202
|
block.startedAt = undefined;
|
|
206
203
|
}
|
|
207
|
-
/** The argument worth showing: the thing the call acts on. */
|
|
208
|
-
function target(input) {
|
|
209
|
-
const { path, command } = input;
|
|
210
|
-
if (typeof command === "string")
|
|
211
|
-
return command;
|
|
212
|
-
if (typeof path === "string")
|
|
213
|
-
return path;
|
|
214
|
-
const rest = JSON.stringify(input);
|
|
215
|
-
return rest === "{}" ? "" : rest;
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* What the call is about to do, drawn before it is allowed to do it.
|
|
219
|
-
*
|
|
220
|
-
* The tool is asked first, because only it knows what is already on disk: a
|
|
221
|
-
* write against an existing file is a replacement, and showing it as a page of
|
|
222
|
-
* additions hides exactly the part worth approving. The fallback diffs what is
|
|
223
|
-
* in the arguments, which is all there is when a tool has nothing to say.
|
|
224
|
-
*/
|
|
225
|
-
function preview(call, look) {
|
|
226
|
-
if (look !== undefined)
|
|
227
|
-
return changes(look.before, look.after);
|
|
228
|
-
const input = call.input;
|
|
229
|
-
if (call.name === "write_file" && typeof input.content === "string") {
|
|
230
|
-
return changes("", input.content);
|
|
231
|
-
}
|
|
232
|
-
if (call.name === "edit_file" && typeof input.old_text === "string") {
|
|
233
|
-
return changes(input.old_text, typeof input.new_text === "string" ? input.new_text : "");
|
|
234
|
-
}
|
|
235
|
-
return undefined;
|
|
236
|
-
}
|
|
237
|
-
/** Two texts as the rows of their difference, unchanged runs summed up. */
|
|
238
|
-
function changes(before, after) {
|
|
239
|
-
let oldLine = 1;
|
|
240
|
-
let newLine = 1;
|
|
241
|
-
const rows = [];
|
|
242
|
-
for (const changed of condense(diff(before, after), CONTEXT)) {
|
|
243
|
-
if (changed.kind === "gap") {
|
|
244
|
-
rows.push({ kind: "gap", text: `… ${changed.skipped} unchanged` });
|
|
245
|
-
oldLine += changed.skipped;
|
|
246
|
-
newLine += changed.skipped;
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
if (changed.kind === "keep") {
|
|
250
|
-
rows.push({ kind: "keep", text: tabs(changed.text), oldLine, newLine });
|
|
251
|
-
oldLine++;
|
|
252
|
-
newLine++;
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
if (changed.kind === "del") {
|
|
256
|
-
rows.push({ kind: "del", text: tabs(changed.text), oldLine });
|
|
257
|
-
oldLine++;
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
rows.push({ kind: "add", text: tabs(changed.text), newLine });
|
|
261
|
-
newLine++;
|
|
262
|
-
}
|
|
263
|
-
emphasizePairs(rows);
|
|
264
|
-
return rows.length === 0 ? undefined : rows;
|
|
265
|
-
}
|
|
266
|
-
function emphasizePairs(rows) {
|
|
267
|
-
for (let index = 0; index < rows.length - 1; index++) {
|
|
268
|
-
const removed = rows[index];
|
|
269
|
-
const added = rows[index + 1];
|
|
270
|
-
if (removed?.kind !== "del" || added?.kind !== "add")
|
|
271
|
-
continue;
|
|
272
|
-
if (rows[index - 1]?.kind === "del" || rows[index + 2]?.kind === "add")
|
|
273
|
-
continue;
|
|
274
|
-
const removedClusters = graphemes(removed.text);
|
|
275
|
-
const addedClusters = graphemes(added.text);
|
|
276
|
-
let prefix = 0;
|
|
277
|
-
let start = 0;
|
|
278
|
-
while (prefix < removedClusters.length &&
|
|
279
|
-
prefix < addedClusters.length &&
|
|
280
|
-
removedClusters[prefix] === addedClusters[prefix]) {
|
|
281
|
-
start += removedClusters[prefix].length;
|
|
282
|
-
prefix++;
|
|
283
|
-
}
|
|
284
|
-
let suffix = 0;
|
|
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;
|
|
293
|
-
suffix++;
|
|
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;
|
|
299
|
-
suffix--;
|
|
300
|
-
}
|
|
301
|
-
const removedLength = removed.text.length - start - removedSuffix;
|
|
302
|
-
const addedLength = added.text.length - start - addedSuffix;
|
|
303
|
-
if (removedLength > 0)
|
|
304
|
-
removed.emphasis = { start, length: removedLength };
|
|
305
|
-
if (addedLength > 0)
|
|
306
|
-
added.emphasis = { start, length: addedLength };
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
function wordBoundary(text, suffix) {
|
|
310
|
-
const at = text.length - suffix;
|
|
311
|
-
if (at <= 0 || at >= text.length)
|
|
312
|
-
return true;
|
|
313
|
-
return /\w/.test(text[at - 1]) !== /\w/.test(text[at]);
|
|
314
|
-
}
|
|
315
|
-
/** What the call left behind, for the calls whose output is worth a look. */
|
|
316
|
-
function produced(call, output) {
|
|
317
|
-
if (call.name === "run_command")
|
|
318
|
-
return details(output, "out");
|
|
319
|
-
// A read or a listing is already summed up on the right of its own row, and
|
|
320
|
-
// the model is about to say what was in it. Printing it twice helps nobody.
|
|
321
|
-
return undefined;
|
|
322
|
-
}
|
|
323
|
-
function details(text, kind) {
|
|
324
|
-
const rows = split(text).map((line) => ({ kind, text: tabs(line) }));
|
|
325
|
-
return rows.length === 0 ? undefined : rows;
|
|
326
|
-
}
|
|
327
|
-
function split(text) {
|
|
328
|
-
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
329
|
-
while (lines.length > 0 && lines[lines.length - 1] === "")
|
|
330
|
-
lines.pop();
|
|
331
|
-
return lines.map(tabs);
|
|
332
|
-
}
|
|
333
|
-
// A tab is a width the terminal decides and the row measurement cannot see.
|
|
334
|
-
function tabs(line) {
|
|
335
|
-
return line.replace(/\t/g, " ");
|
|
336
|
-
}
|
package/dist/tui/view.js
CHANGED
|
@@ -39,10 +39,10 @@ export function compose(view, size, transcript = transcriptRenderer()) {
|
|
|
39
39
|
}
|
|
40
40
|
function dockRows(view, width, height) {
|
|
41
41
|
const status = renderStatus({
|
|
42
|
-
// A modal
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
// A modal names its command; background model work still needs visible
|
|
43
|
+
// status. Its interruption/steering keys are suspended while a menu owns input.
|
|
44
|
+
status: view.modal === undefined || view.turnActive ? view.status : undefined,
|
|
45
|
+
interruptible: view.modal === undefined,
|
|
46
46
|
steering: view.modal === undefined ? view.steering : undefined,
|
|
47
47
|
feedback: view.feedback,
|
|
48
48
|
readiness: view.readiness,
|
package/dist/ui/render.js
CHANGED
|
@@ -45,10 +45,6 @@ export function paint(seg) {
|
|
|
45
45
|
export function plainLen(segs) {
|
|
46
46
|
return segs.reduce((n, seg) => n + textWidth(terminalText(seg.text)), 0);
|
|
47
47
|
}
|
|
48
|
-
export function columns() {
|
|
49
|
-
const width = process.stdout.columns;
|
|
50
|
-
return typeof width === "number" && width >= 40 ? width : 80;
|
|
51
|
-
}
|
|
52
48
|
/**
|
|
53
49
|
* The margin every row keeps from the edges of the terminal: none.
|
|
54
50
|
*
|