@giovannijecha/jecode 0.8.4 → 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 +28 -9
- package/dist/accounts.js +47 -10
- package/dist/atomic.js +24 -14
- package/dist/batch-view.js +27 -2
- package/dist/batch.js +39 -4
- package/dist/bounded-file.js +212 -0
- package/dist/cli-info.js +0 -1
- package/dist/commands.js +2 -0
- package/dist/config.js +16 -7
- package/dist/context/automatic.js +35 -0
- package/dist/context/compactor.js +32 -2
- package/dist/context/manual.js +20 -3
- package/dist/context/request-projection.js +130 -0
- package/dist/controller-request.js +33 -11
- package/dist/credential-commands.js +25 -9
- package/dist/credentials.js +56 -18
- package/dist/directory-anchor.js +91 -0
- package/dist/file-identity.js +12 -0
- package/dist/model-command.js +5 -4
- package/dist/openai-account-command.js +23 -10
- 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/process-lease.js +329 -0
- package/dist/provider-commands.js +11 -37
- package/dist/provider-errors.js +31 -7
- package/dist/provider-label.js +9 -3
- package/dist/providers/anthropic-stream.js +4 -1
- package/dist/providers/anthropic-wire.js +8 -3
- package/dist/providers/anthropic.js +39 -20
- package/dist/providers/catalog.js +4 -4
- package/dist/providers/failure.js +181 -0
- package/dist/providers/http.js +82 -23
- 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-stream.js +5 -1
- package/dist/providers/ollama.js +36 -158
- package/dist/providers/openai-codex.js +71 -45
- package/dist/providers/openai-stream.js +26 -2
- package/dist/providers/openai.js +51 -25
- package/dist/providers/sse.js +52 -8
- package/dist/request-identity.js +32 -0
- 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/lease.js +132 -49
- package/dist/sessions/load.js +90 -0
- package/dist/sessions/runtime.js +15 -8
- package/dist/sessions/snapshot.js +33 -0
- package/dist/sessions/store.js +183 -378
- package/dist/settings-command.js +10 -5
- package/dist/settings.js +75 -22
- package/dist/stable-directory.js +148 -0
- package/dist/start.js +1 -2
- package/dist/store-lock.js +68 -84
- package/dist/tools/args.js +2 -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 -171
- package/dist/tools/glob.js +107 -0
- package/dist/tools/index.js +2 -1
- package/dist/tools/search.js +66 -205
- package/dist/tools/text-boundary.js +7 -33
- package/dist/tui/app-workflows.js +9 -334
- 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/footer.js +1 -1
- 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/feedback.js +4 -0
- 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/session-view.js +7 -2
- 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/dist/tui/workspace.js +21 -7
- package/dist/user-store.js +23 -31
- package/package.json +14 -15
- package/dist/ollama-settings-command.js +0 -74
- package/dist/tools/ripgrep.js +0 -230
- package/dist/tui/motion.js +0 -32
|
@@ -1,336 +1,11 @@
|
|
|
1
|
-
//
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { compactContext } from "../context/compactor.js";
|
|
6
|
-
import { compactSession } from "../context/manual.js";
|
|
7
|
-
import { isContextOverflow } from "../context/policy.js";
|
|
8
|
-
import { updateSettings } from "../settings.js";
|
|
9
|
-
import { steeringInbox } from "../steering.js";
|
|
10
|
-
import { saveTranscript } from "../transcript-export.js";
|
|
11
|
-
import { recordAuxiliaryUsage, recordRequestInput, recordUsage } from "../usage.js";
|
|
12
|
-
import { selectTimeline } from "../timeline.js";
|
|
13
|
-
import { transition } from "./activity.js";
|
|
14
|
-
import { answerAt } from "./approve.js";
|
|
15
|
-
import * as edit from "./editor.js";
|
|
16
|
-
import { cancel as cancelOpen } from "./overlay.js";
|
|
17
|
-
import { controllerOptions, turnFailure } from "./session-view.js";
|
|
18
|
-
import { transcribe } from "./turn.js";
|
|
19
|
-
const WAITING = "Waiting";
|
|
1
|
+
// Wire foreground workflows to the shell and their shared compaction lifetime.
|
|
2
|
+
import { automaticCompactionGate } from "../context/automatic.js";
|
|
3
|
+
import { commandWorkflow } from "./command-workflow.js";
|
|
4
|
+
import { turnWorkflow } from "./turn-workflow.js";
|
|
20
5
|
export function appWorkflows(options) {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
});
|
|
27
|
-
async function command(text) {
|
|
28
|
-
const activity = options.startActivity("command", `Running ${text.split(/\s+/)[0]}`);
|
|
29
|
-
if (activity === undefined)
|
|
30
|
-
return;
|
|
31
|
-
const status = (label) => transition(activity, label);
|
|
32
|
-
try {
|
|
33
|
-
const outcome = await handleCommand(text, session, {
|
|
34
|
-
emit: options.commandNotice,
|
|
35
|
-
signal: activity.control.signal,
|
|
36
|
-
showHelp: () => new Promise((resolve) => {
|
|
37
|
-
state.open = { help: true, settle: resolve };
|
|
38
|
-
options.render();
|
|
39
|
-
}),
|
|
40
|
-
choose,
|
|
41
|
-
dismiss: () => {
|
|
42
|
-
state.open = state.open === undefined ? undefined : cancelOpen(state.open);
|
|
43
|
-
options.render();
|
|
44
|
-
},
|
|
45
|
-
type: (field) => new Promise((resolve) => {
|
|
46
|
-
state.open = { field, settle: resolve };
|
|
47
|
-
options.render();
|
|
48
|
-
}),
|
|
49
|
-
status: (said) => {
|
|
50
|
-
status(said ?? activity.label);
|
|
51
|
-
options.render();
|
|
52
|
-
},
|
|
53
|
-
reset: async () => {
|
|
54
|
-
await session.persistence?.reset();
|
|
55
|
-
state.blocks.splice(0);
|
|
56
|
-
state.past.length = 0;
|
|
57
|
-
permissions.reset();
|
|
58
|
-
state.scroll = 0;
|
|
59
|
-
state.follow = true;
|
|
60
|
-
state.unseen = 0;
|
|
61
|
-
state.lastMaxScroll = 0;
|
|
62
|
-
state.committedNodeId = 0;
|
|
63
|
-
},
|
|
64
|
-
permissions,
|
|
65
|
-
exportTranscript: () => saveTranscript(options.transcriptRoot, state.blocks),
|
|
66
|
-
saveSettings: async (patch) => {
|
|
67
|
-
await updateSettings(patch);
|
|
68
|
-
},
|
|
69
|
-
refreshSettings: options.refreshSettings,
|
|
70
|
-
timeline: async () => {
|
|
71
|
-
const selected = await selectTimeline(session, choose);
|
|
72
|
-
if (!selected)
|
|
73
|
-
return "unchanged";
|
|
74
|
-
options.replaceTranscript();
|
|
75
|
-
return session.conversation.activeNodeId === state.committedNodeId
|
|
76
|
-
? "unchanged"
|
|
77
|
-
: "selected";
|
|
78
|
-
},
|
|
79
|
-
compact: async () => {
|
|
80
|
-
if (session.conversation.activeNodeId !== state.committedNodeId) {
|
|
81
|
-
return "branch-pending";
|
|
82
|
-
}
|
|
83
|
-
return compactSession(session, {
|
|
84
|
-
signal: activity.control.signal,
|
|
85
|
-
onStatus: (said) => {
|
|
86
|
-
status(said ?? activity.label);
|
|
87
|
-
options.render();
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
|
-
if (outcome === "exit")
|
|
93
|
-
state.closeWhenIdle = true;
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
// Command cancellation is already visible through the dock closing and
|
|
97
|
-
// the activity ending. Keep it silent instead of replacing the footer
|
|
98
|
-
// with a redundant warning.
|
|
99
|
-
if (!activity.control.signal.aborted) {
|
|
100
|
-
feedback.show({
|
|
101
|
-
text: error.message,
|
|
102
|
-
tone: "error",
|
|
103
|
-
timeoutMs: 6_000,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
finally {
|
|
108
|
-
options.finishActivity(activity);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
async function turn(text) {
|
|
112
|
-
const activity = options.startActivity("turn", WAITING);
|
|
113
|
-
if (activity === undefined)
|
|
114
|
-
return;
|
|
115
|
-
const inbox = steeringInbox((pending, accepting) => {
|
|
116
|
-
if (activeSteering !== inbox)
|
|
117
|
-
return;
|
|
118
|
-
state.steering = accepting ? pending : undefined;
|
|
119
|
-
options.render();
|
|
120
|
-
});
|
|
121
|
-
activeSteering = inbox;
|
|
122
|
-
state.steering = 0;
|
|
123
|
-
const status = (label) => transition(activity, label);
|
|
124
|
-
const parentId = session.conversation.activeNodeId;
|
|
125
|
-
const history = session.conversation.history;
|
|
126
|
-
const modelHistory = session.conversation.contextHistory;
|
|
127
|
-
const historyStart = history.length;
|
|
128
|
-
const blockStart = state.blocks.length;
|
|
129
|
-
const createdAt = new Date().toISOString();
|
|
130
|
-
const prospectiveNodeId = session.conversation.nodes.length + 1;
|
|
131
|
-
let nodeId;
|
|
132
|
-
let context;
|
|
133
|
-
const unpersistedSteering = [];
|
|
134
|
-
let firstPolicy = true;
|
|
135
|
-
const policy = () => {
|
|
136
|
-
let visible = firstPolicy;
|
|
137
|
-
firstPolicy = false;
|
|
138
|
-
if (visible) {
|
|
139
|
-
status("Checking context");
|
|
140
|
-
options.render();
|
|
141
|
-
}
|
|
142
|
-
return resolveContextPolicy({
|
|
143
|
-
provider: session.provider,
|
|
144
|
-
model: session.model,
|
|
145
|
-
compactionPercent: session.config.compactionPercent,
|
|
146
|
-
signal: activity.control.signal,
|
|
147
|
-
onStatus: (said) => {
|
|
148
|
-
visible = true;
|
|
149
|
-
status(said);
|
|
150
|
-
options.render();
|
|
151
|
-
},
|
|
152
|
-
}).finally(() => {
|
|
153
|
-
if (visible) {
|
|
154
|
-
status(WAITING);
|
|
155
|
-
options.render();
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
};
|
|
159
|
-
options.emit({ kind: "user", text });
|
|
160
|
-
const user = { role: "user", content: [{ kind: "text", text }] };
|
|
161
|
-
history.push(user);
|
|
162
|
-
modelHistory.push(structuredClone(user));
|
|
163
|
-
const events = transcribe({
|
|
164
|
-
emit: options.emit,
|
|
165
|
-
render: options.render,
|
|
166
|
-
palette: session.palette,
|
|
167
|
-
approved: (call) => permissions.approved(call),
|
|
168
|
-
remember: (call) => permissions.remember(call),
|
|
169
|
-
ask: (prompt, settle) => {
|
|
170
|
-
state.open = { picker: prompt, settle: (index) => settle(answerAt(index)) };
|
|
171
|
-
options.render();
|
|
172
|
-
},
|
|
173
|
-
status: (text) => {
|
|
174
|
-
status(text);
|
|
175
|
-
},
|
|
176
|
-
usage: (usage) => recordUsage(session.usage, usage),
|
|
177
|
-
requestInput: (inputTokens) => recordRequestInput(session.usage, inputTokens),
|
|
178
|
-
});
|
|
179
|
-
const persist = async (checkpoint, settlement, failure) => {
|
|
180
|
-
const next = session.conversation.commit({
|
|
181
|
-
...(nodeId === undefined ? {} : { nodeId }),
|
|
182
|
-
parentId,
|
|
183
|
-
createdAt,
|
|
184
|
-
identity: {
|
|
185
|
-
providerId: session.provider.id,
|
|
186
|
-
model: session.model,
|
|
187
|
-
effort: session.config.effort,
|
|
188
|
-
},
|
|
189
|
-
messages: checkpoint.slice(historyStart),
|
|
190
|
-
blocks: state.blocks.slice(blockStart),
|
|
191
|
-
...(context === undefined ? {} : { context }),
|
|
192
|
-
...(failure === undefined ? {} : { failure }),
|
|
193
|
-
}, settlement);
|
|
194
|
-
await session.persistence?.checkpoint(next);
|
|
195
|
-
session.conversation = next;
|
|
196
|
-
nodeId = next.activeNodeId;
|
|
197
|
-
state.committedNodeId = next.activeNodeId;
|
|
198
|
-
unpersistedSteering.length = 0;
|
|
199
|
-
};
|
|
200
|
-
const compact = async (checkpoint, projected, request) => {
|
|
201
|
-
if (request.reason === "overflow" &&
|
|
202
|
-
(request.error === undefined || !isContextOverflow(request.error))) {
|
|
203
|
-
return undefined;
|
|
204
|
-
}
|
|
205
|
-
const force = request.reason === "overflow";
|
|
206
|
-
const result = await compactContext({
|
|
207
|
-
provider: session.provider,
|
|
208
|
-
model: session.model,
|
|
209
|
-
effort: session.config.effort,
|
|
210
|
-
context: projected,
|
|
211
|
-
turn: checkpoint.slice(historyStart),
|
|
212
|
-
nodeId: nodeId ?? prospectiveNodeId,
|
|
213
|
-
coveredMessages: context?.messageCount ?? 0,
|
|
214
|
-
lastInputTokens: Math.max(session.usage.lastInputTokens, request.inputTokens),
|
|
215
|
-
estimatedInputTokens: request.inputTokens,
|
|
216
|
-
signal: activity.control.signal,
|
|
217
|
-
force,
|
|
218
|
-
policy: request.policy,
|
|
219
|
-
onBegin: () => {
|
|
220
|
-
status("Compacting");
|
|
221
|
-
options.render();
|
|
222
|
-
},
|
|
223
|
-
onEnd: () => {
|
|
224
|
-
status(WAITING);
|
|
225
|
-
options.render();
|
|
226
|
-
},
|
|
227
|
-
});
|
|
228
|
-
if (result === undefined)
|
|
229
|
-
return undefined;
|
|
230
|
-
context = result.anchor;
|
|
231
|
-
if (result.usage !== undefined)
|
|
232
|
-
recordAuxiliaryUsage(session.usage, result.usage);
|
|
233
|
-
return result.messages;
|
|
234
|
-
};
|
|
235
|
-
events.onContext = compact;
|
|
236
|
-
events.onSteering = (guidance) => {
|
|
237
|
-
unpersistedSteering.push(guidance);
|
|
238
|
-
options.emit({ kind: "user", text: guidance });
|
|
239
|
-
options.render();
|
|
240
|
-
};
|
|
241
|
-
events.onCheckpoint = async (checkpoint, settlement, projected) => {
|
|
242
|
-
await persist(checkpoint, settlement);
|
|
243
|
-
const compacted = await compact(checkpoint, projected, {
|
|
244
|
-
reason: "budget",
|
|
245
|
-
policy: await policy(),
|
|
246
|
-
inputTokens: session.usage.lastInputTokens,
|
|
247
|
-
});
|
|
248
|
-
if (compacted !== undefined)
|
|
249
|
-
await persist(checkpoint, settlement);
|
|
250
|
-
return compacted;
|
|
251
|
-
};
|
|
252
|
-
let finishReason;
|
|
253
|
-
let failed;
|
|
254
|
-
try {
|
|
255
|
-
await runTurn(history, controllerOptions(session, policy, permissions.availableTools(), inbox), events, activity.control.signal, modelHistory);
|
|
256
|
-
}
|
|
257
|
-
catch (error) {
|
|
258
|
-
const interrupted = activity.control.signal.aborted;
|
|
259
|
-
const completed = nodeId !== undefined && session.conversation.activeNodeId === nodeId &&
|
|
260
|
-
session.conversation.activeNode?.settlement === "completed";
|
|
261
|
-
if (completed) {
|
|
262
|
-
const notice = turnFailure(session, error, interrupted);
|
|
263
|
-
feedback.show({ text: notice.text, tone: notice.tone, timeoutMs: 6_000 });
|
|
264
|
-
}
|
|
265
|
-
else {
|
|
266
|
-
finishReason = interrupted ? "interrupted" : "failed";
|
|
267
|
-
failed = { error: error, interrupted };
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
finally {
|
|
271
|
-
let pendingSteering = inbox.close();
|
|
272
|
-
if (activeSteering === inbox)
|
|
273
|
-
activeSteering = undefined;
|
|
274
|
-
state.steering = undefined;
|
|
275
|
-
try {
|
|
276
|
-
events.finish(finishReason);
|
|
277
|
-
if (failed !== undefined) {
|
|
278
|
-
const notice = turnFailure(session, failed.error, failed.interrupted);
|
|
279
|
-
const settlement = failed.interrupted ? "interrupted" : "failed";
|
|
280
|
-
const failure = {
|
|
281
|
-
text: notice.text,
|
|
282
|
-
tone: failed.interrupted ? "warn" : "error",
|
|
283
|
-
};
|
|
284
|
-
options.emit(notice);
|
|
285
|
-
try {
|
|
286
|
-
await persist(closeFailedTurn(history, settlement), settlement, failure);
|
|
287
|
-
}
|
|
288
|
-
catch (error) {
|
|
289
|
-
// A failed persistence boundary cannot remain visible as if it had
|
|
290
|
-
// been saved. Revert to the last durable path and return the input
|
|
291
|
-
// to the composer so the user can retry without losing it.
|
|
292
|
-
options.replaceTranscript();
|
|
293
|
-
state.editor = edit.of([
|
|
294
|
-
...(nodeId === undefined ? [text] : []),
|
|
295
|
-
...unpersistedSteering,
|
|
296
|
-
...pendingSteering,
|
|
297
|
-
state.editor.text,
|
|
298
|
-
].filter((part) => part !== "").join("\n\n"));
|
|
299
|
-
state.completing = undefined;
|
|
300
|
-
pendingSteering = [];
|
|
301
|
-
feedback.show({
|
|
302
|
-
text: error.message,
|
|
303
|
-
tone: "error",
|
|
304
|
-
timeoutMs: 6_000,
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
finally {
|
|
310
|
-
restorePendingSteering(state, pendingSteering);
|
|
311
|
-
options.finishActivity(activity);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
function steer(text) {
|
|
316
|
-
return activeSteering?.offer(text) ?? "unavailable";
|
|
317
|
-
}
|
|
318
|
-
return { command, turn, steer };
|
|
319
|
-
}
|
|
320
|
-
function restorePendingSteering(state, messages) {
|
|
321
|
-
if (messages.length === 0)
|
|
322
|
-
return;
|
|
323
|
-
const pending = messages.join("\n\n");
|
|
324
|
-
state.editor = edit.of(state.editor.text === "" ? pending : `${pending}\n\n${state.editor.text}`);
|
|
325
|
-
state.completing = undefined;
|
|
326
|
-
}
|
|
327
|
-
function closeFailedTurn(history, settlement) {
|
|
328
|
-
const closed = [...history];
|
|
329
|
-
if (closed.at(-1)?.role === "assistant")
|
|
330
|
-
return closed;
|
|
331
|
-
const text = settlement === "interrupted"
|
|
332
|
-
? "The previous attempt was interrupted by the user before completion."
|
|
333
|
-
: "The previous attempt failed before completion.";
|
|
334
|
-
closed.push({ role: "assistant", content: [{ kind: "text", text }] });
|
|
335
|
-
return closed;
|
|
6
|
+
const automaticCompaction = automaticCompactionGate();
|
|
7
|
+
return {
|
|
8
|
+
command: commandWorkflow(options, () => automaticCompaction.reset()),
|
|
9
|
+
...turnWorkflow(options, automaticCompaction),
|
|
10
|
+
};
|
|
336
11
|
}
|
package/dist/tui/approve.js
CHANGED
|
@@ -16,13 +16,15 @@ export { scopeFor };
|
|
|
16
16
|
export function promptFor(call, target, pal) {
|
|
17
17
|
const scope = scopeFor(call);
|
|
18
18
|
const options = [
|
|
19
|
-
{ label: "Yes, once", hint: "
|
|
20
|
-
{ label: `Yes, ${scopeNoun(scope)} for the session`, hint: "a", key: "a"
|
|
21
|
-
|
|
19
|
+
{ label: "Yes, once", hint: "y", key: "y", description: "Approve only this call. Ask again next time." },
|
|
20
|
+
{ label: `Yes, ${scopeNoun(scope)} for the session`, hint: "a", key: "a",
|
|
21
|
+
description: `Reuse approval for ${scopeNoun(scope)} in this session.` },
|
|
22
|
+
{ label: "No, and say why", hint: "n", key: "n", description: "Do not run this call. Return feedback to the model." },
|
|
22
23
|
];
|
|
23
24
|
return {
|
|
24
25
|
title: [{ text: question(call.name), fg: pal.ink.attention, bold: true }],
|
|
25
26
|
right: `${call.name}${target === "" ? "" : ` · ${target}`}`,
|
|
27
|
+
controls: "↑↓ choose · enter confirm · esc deny",
|
|
26
28
|
options,
|
|
27
29
|
index: 0,
|
|
28
30
|
};
|
package/dist/tui/blocks.js
CHANGED
|
@@ -2,23 +2,24 @@
|
|
|
2
2
|
import { renderAnswer, renderReasoning, renderUser } from "./components/messages.js";
|
|
3
3
|
import { renderNotice } from "./components/misc.js";
|
|
4
4
|
import { renderTool } from "./components/tool.js";
|
|
5
|
+
import { insetTranscript, modelTranscriptWidth } from "./transcript-grammar.js";
|
|
5
6
|
export function render(block, width, pal, context = {}) {
|
|
7
|
+
const inner = modelTranscriptWidth(width);
|
|
6
8
|
switch (block.kind) {
|
|
7
9
|
case "user":
|
|
8
10
|
return renderUser(block, width, pal);
|
|
9
11
|
case "answer":
|
|
10
|
-
return renderAnswer(block,
|
|
12
|
+
return insetTranscript(renderAnswer(block, inner, pal));
|
|
11
13
|
case "reasoning":
|
|
12
|
-
return renderReasoning(block,
|
|
14
|
+
return insetTranscript(renderReasoning(block, inner, pal, {
|
|
13
15
|
continues: context.previous?.kind === "reasoning",
|
|
14
|
-
});
|
|
16
|
+
}));
|
|
15
17
|
case "tool":
|
|
16
|
-
return renderTool(block,
|
|
17
|
-
continues: context.previous?.kind === "
|
|
18
|
+
return insetTranscript(renderTool(block, inner, pal, {
|
|
19
|
+
continues: context.previous?.kind === "reasoning",
|
|
18
20
|
now: context.now,
|
|
19
|
-
motion: context.motion,
|
|
20
21
|
reducedMotion: context.reducedMotion,
|
|
21
|
-
});
|
|
22
|
+
}));
|
|
22
23
|
case "notice":
|
|
23
24
|
return renderNotice(block, width, pal);
|
|
24
25
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Foreground slash-command interactions and their session updates.
|
|
2
|
+
import { handleCommand } from "../commands.js";
|
|
3
|
+
import { compactSession } from "../context/manual.js";
|
|
4
|
+
import { resetRequestIdentity } from "../request-identity.js";
|
|
5
|
+
import { updateSettings } from "../settings.js";
|
|
6
|
+
import { selectTimeline } from "../timeline.js";
|
|
7
|
+
import { saveTranscript } from "../transcript-export.js";
|
|
8
|
+
import { transition } from "./activity.js";
|
|
9
|
+
import { cancel as cancelOpen } from "./overlay.js";
|
|
10
|
+
export function commandWorkflow(options, resetCompaction) {
|
|
11
|
+
const { session, state, permissions, feedback } = options;
|
|
12
|
+
const choose = (picker) => new Promise((resolve) => {
|
|
13
|
+
state.open = { picker, settle: resolve };
|
|
14
|
+
options.render();
|
|
15
|
+
});
|
|
16
|
+
async function command(text) {
|
|
17
|
+
const activity = options.startActivity("command", `Running ${text.split(/\s+/)[0]}`);
|
|
18
|
+
if (activity === undefined)
|
|
19
|
+
return;
|
|
20
|
+
const status = (label) => transition(activity, label);
|
|
21
|
+
try {
|
|
22
|
+
const outcome = await handleCommand(text, session, {
|
|
23
|
+
emit: options.commandNotice,
|
|
24
|
+
signal: activity.control.signal,
|
|
25
|
+
showHelp: () => new Promise((resolve) => {
|
|
26
|
+
state.open = { help: true, settle: resolve };
|
|
27
|
+
options.render();
|
|
28
|
+
}),
|
|
29
|
+
choose,
|
|
30
|
+
dismiss: () => {
|
|
31
|
+
state.open = state.open === undefined ? undefined : cancelOpen(state.open);
|
|
32
|
+
options.render();
|
|
33
|
+
},
|
|
34
|
+
type: (field) => new Promise((resolve) => {
|
|
35
|
+
state.open = { field, settle: resolve };
|
|
36
|
+
options.render();
|
|
37
|
+
}),
|
|
38
|
+
status: (said) => {
|
|
39
|
+
status(said ?? activity.label);
|
|
40
|
+
options.render();
|
|
41
|
+
},
|
|
42
|
+
reset: async () => {
|
|
43
|
+
await session.persistence?.reset();
|
|
44
|
+
resetRequestIdentity(session);
|
|
45
|
+
resetCompaction();
|
|
46
|
+
state.blocks.splice(0);
|
|
47
|
+
state.past.length = 0;
|
|
48
|
+
permissions.reset();
|
|
49
|
+
state.scroll = 0;
|
|
50
|
+
state.follow = true;
|
|
51
|
+
state.unseen = 0;
|
|
52
|
+
state.lastMaxScroll = 0;
|
|
53
|
+
state.committedNodeId = 0;
|
|
54
|
+
},
|
|
55
|
+
permissions,
|
|
56
|
+
exportTranscript: () => saveTranscript(options.transcriptRoot, state.blocks),
|
|
57
|
+
saveSettings: async (patch) => {
|
|
58
|
+
await updateSettings(patch);
|
|
59
|
+
},
|
|
60
|
+
refreshSettings: options.refreshSettings,
|
|
61
|
+
timeline: async () => {
|
|
62
|
+
const selected = await selectTimeline(session, choose);
|
|
63
|
+
if (!selected)
|
|
64
|
+
return "unchanged";
|
|
65
|
+
options.replaceTranscript();
|
|
66
|
+
return session.conversation.activeNodeId === state.committedNodeId
|
|
67
|
+
? "unchanged"
|
|
68
|
+
: "selected";
|
|
69
|
+
},
|
|
70
|
+
compact: async () => {
|
|
71
|
+
if (session.conversation.activeNodeId !== state.committedNodeId) {
|
|
72
|
+
return "branch-pending";
|
|
73
|
+
}
|
|
74
|
+
const result = await compactSession(session, {
|
|
75
|
+
signal: activity.control.signal,
|
|
76
|
+
onStatus: (said) => {
|
|
77
|
+
status(said ?? activity.label);
|
|
78
|
+
options.render();
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
if (result === "compacted")
|
|
82
|
+
resetCompaction();
|
|
83
|
+
return result;
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
if (outcome === "exit")
|
|
87
|
+
state.closeWhenIdle = true;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
// Command cancellation is already visible through the dock closing and
|
|
91
|
+
// the activity ending. Keep it silent instead of replacing the footer
|
|
92
|
+
// with a redundant warning.
|
|
93
|
+
if (!activity.control.signal.aborted) {
|
|
94
|
+
feedback.show({
|
|
95
|
+
text: error.message,
|
|
96
|
+
tone: "error",
|
|
97
|
+
timeoutMs: 6_000,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
options.finishActivity(activity);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return command;
|
|
106
|
+
}
|
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { renderMenu } from "./menu.js";
|
|
2
2
|
const MAX_ROWS = 4;
|
|
3
3
|
export function renderCommandMenu(commands, selected, width, pal) {
|
|
4
4
|
const selectedIndex = selected === undefined ? 0 : Math.max(0, Math.min(commands.length - 1, selected));
|
|
5
|
-
|
|
6
|
-
const shown = commands.slice(first, last);
|
|
7
|
-
if (shown.length === 0)
|
|
5
|
+
if (commands.length === 0)
|
|
8
6
|
return { rows: [], right: "" };
|
|
7
|
+
const menu = renderMenu(commands.map((command, index) => ({
|
|
8
|
+
label: `/${command.name}`, description: command.blurb, selected: selectedIndex === index,
|
|
9
|
+
})), width, pal, { maxRows: MAX_ROWS + 2, visible: MAX_ROWS });
|
|
9
10
|
return {
|
|
10
|
-
rows:
|
|
11
|
-
|
|
12
|
-
description: command.blurb,
|
|
13
|
-
selected: selectedIndex === first + index,
|
|
14
|
-
})), width, pal),
|
|
15
|
-
right: commands.length > shown.length ? `${first + 1}–${last} / ${commands.length}` : "",
|
|
11
|
+
rows: menu.rows,
|
|
12
|
+
right: commands.length > menu.last - menu.first ? `${menu.first + 1}–${menu.last} / ${commands.length}` : "",
|
|
16
13
|
};
|
|
17
14
|
}
|
|
18
15
|
export function commandMenuLimit() {
|
|
@@ -10,7 +10,7 @@ export function renderFooter(info, status, width, pal) {
|
|
|
10
10
|
function identity(info, cols) {
|
|
11
11
|
if (cols <= 0)
|
|
12
12
|
return "";
|
|
13
|
-
const core = `${info.model || "no model"} · ${info.effort}`;
|
|
13
|
+
const core = `${info.provider || "Provider"} · ${info.model || "no model"} · ${info.effort}`;
|
|
14
14
|
if (textWidth(core) >= cols || info.workspace === "")
|
|
15
15
|
return elide(core, cols);
|
|
16
16
|
const divider = " · ";
|
|
@@ -1,12 +1,57 @@
|
|
|
1
|
-
//
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
// Ribbon rows and a stable detail area shared by completion and selectors.
|
|
2
|
+
import { row } from "../../ui/render.js";
|
|
3
|
+
import { terminalText } from "../../ui/terminal-text.js";
|
|
4
|
+
import { elide, textWidth, wrapText } from "../../ui/width.js";
|
|
4
5
|
export function renderMenuRows(entries, width, pal) {
|
|
6
|
+
return entries.map((entry) => {
|
|
7
|
+
const right = summary(entry, width);
|
|
8
|
+
return row(width, [
|
|
9
|
+
{ text: entry.selected ? "● " : " ", fg: pal.focus },
|
|
10
|
+
{ text: entry.label, fg: entry.selected ? pal.ink.bright : pal.ink.muted, bold: entry.selected },
|
|
11
|
+
], right === "" ? [] : [{ text: right, fg: entry.selected ? pal.focus : pal.ink.dim }], entry.selected ? pal.surface.subtle : undefined);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
/** A missing palette measures the window without producing styled rows. */
|
|
15
|
+
export function renderMenu(entries, width, pal, options) {
|
|
16
|
+
const room = Math.max(0, options.maxRows);
|
|
17
|
+
if (room === 0)
|
|
18
|
+
return { rows: [], first: 0, last: 0 };
|
|
19
|
+
const detailLimit = Math.min(2, room - 1);
|
|
20
|
+
let detailRows = 0;
|
|
21
|
+
// Measure every possible selection to keep the dock stable without reserving
|
|
22
|
+
// a second row when all details fit on one line at this width.
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
detailRows = Math.max(detailRows, detailLines(entry, width, detailLimit).length);
|
|
25
|
+
if (detailRows === detailLimit)
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
const visible = Math.min(options.visible ?? 6, room - detailRows);
|
|
29
|
+
const at = Math.max(0, entries.findIndex((entry) => entry.selected));
|
|
30
|
+
const { first, last } = menuWindow(entries.length, at, visible);
|
|
31
|
+
if (pal === undefined)
|
|
32
|
+
return { rows: [], first, last };
|
|
5
33
|
if (entries.length === 0)
|
|
34
|
+
return {
|
|
35
|
+
rows: [row(width, [{ text: "No matches. Change the filter.", fg: pal.ink.muted }])], first, last,
|
|
36
|
+
};
|
|
37
|
+
const active = entries.find((entry) => entry.selected);
|
|
38
|
+
const lines = detailLines(active, width, detailRows);
|
|
39
|
+
return {
|
|
40
|
+
rows: [
|
|
41
|
+
...renderMenuRows(entries.slice(first, last), width, pal),
|
|
42
|
+
...Array.from({ length: detailRows }, (_, index) => lines[index] === undefined ? ""
|
|
43
|
+
: row(width, [{ text: " " + lines[index], fg: pal.ink.muted }])),
|
|
44
|
+
], first, last,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function detailLines(entry, width, maxRows) {
|
|
48
|
+
if (entry === undefined || maxRows <= 0)
|
|
6
49
|
return [];
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
50
|
+
// Complete identity and values take priority over explanatory copy.
|
|
51
|
+
const room = Math.max(1, width - 2);
|
|
52
|
+
const lines = menuText(clippedParts(entry, width).join(" · "), room, maxRows);
|
|
53
|
+
lines.push(...menuText(entry.description ?? "", room, maxRows - lines.length));
|
|
54
|
+
return lines;
|
|
10
55
|
}
|
|
11
56
|
export function menuWindow(length, selected, visible) {
|
|
12
57
|
const count = Math.max(0, length);
|
|
@@ -15,42 +60,28 @@ export function menuWindow(length, selected, visible) {
|
|
|
15
60
|
const first = Math.max(0, Math.min(at - Math.floor(room / 2), count - room));
|
|
16
61
|
return { first, last: Math.min(count, first + room) };
|
|
17
62
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
63
|
+
/** Wrap untrusted menu copy, marking a bounded final row when it omits text. */
|
|
64
|
+
export function menuText(text, width, maxRows) {
|
|
65
|
+
if (text === "" || maxRows <= 0)
|
|
66
|
+
return [];
|
|
67
|
+
const lines = wrapText(terminalText(text), Math.max(1, width));
|
|
68
|
+
return lines.slice(0, maxRows).map((line, index) => index === maxRows - 1 && lines.length > maxRows ? elide(line + " …", width) : line);
|
|
69
|
+
}
|
|
70
|
+
function fullSummary(entry) {
|
|
71
|
+
const value = entry.value === undefined ? "" : entry.selected && entry.adjustable ? "‹ " + entry.value + " ›" : entry.value;
|
|
72
|
+
return [entry.hint, value].filter(Boolean).join(" · ");
|
|
73
|
+
}
|
|
74
|
+
function summary(entry, width) {
|
|
75
|
+
return elide(terminalText(fullSummary(entry)), Math.max(1, Math.floor(width * 0.42)));
|
|
76
|
+
}
|
|
77
|
+
function clippedParts(entry, width) {
|
|
78
|
+
// Reserve the stepper's width for every peer, so moving selection cannot
|
|
79
|
+
// make the shared detail area appear or disappear.
|
|
80
|
+
const stable = entry.adjustable ? { ...entry, selected: true } : entry;
|
|
81
|
+
const right = summary(stable, width);
|
|
82
|
+
const leftRoom = Math.max(0, width - 2 - (right === "" ? 0 : textWidth(right) + 1));
|
|
83
|
+
return [
|
|
84
|
+
...(textWidth(terminalText(entry.label)) > leftRoom ? [entry.label] : []),
|
|
85
|
+
...(right !== terminalText(fullSummary(stable)) ? [fullSummary(stable)] : []),
|
|
28
86
|
];
|
|
29
|
-
if (width > 40 && entry.description !== undefined) {
|
|
30
|
-
const gap = Math.max(2, labelWidth - primaryWidth(entry));
|
|
31
|
-
primary.push({
|
|
32
|
-
text: `${" ".repeat(gap)}${entry.description}`,
|
|
33
|
-
fg: entry.selected ? pal.ink.bright : pal.ink.muted,
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
const value = entry.value === undefined
|
|
37
|
-
? undefined
|
|
38
|
-
: entry.selected && entry.adjustable === true
|
|
39
|
-
? `‹ ${entry.value} ›`
|
|
40
|
-
: entry.value;
|
|
41
|
-
const summary = [entry.hint, value]
|
|
42
|
-
.filter((part) => part !== undefined)
|
|
43
|
-
.join(" · ");
|
|
44
|
-
const rightColor = entry.selected ? pal.focus : pal.ink.muted;
|
|
45
|
-
const right = summary === "" || (width <= 40 && entry.value === undefined)
|
|
46
|
-
? []
|
|
47
|
-
: [{
|
|
48
|
-
text: elide(summary, Math.max(1, Math.floor(entry.value === undefined ? width / 4 : width * 0.45))),
|
|
49
|
-
fg: rightColor,
|
|
50
|
-
bold: entry.selected || undefined,
|
|
51
|
-
}];
|
|
52
|
-
return row(width, primary, right);
|
|
53
|
-
}
|
|
54
|
-
function primaryWidth(entry) {
|
|
55
|
-
return plainLen([{ text: entry.label }]);
|
|
56
87
|
}
|