@giovannijecha/jecode 0.7.4 → 0.8.1
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 +25 -13
- package/dist/batch.js +1 -1
- package/dist/cli-info.js +1 -1
- package/dist/config.js +4 -1
- package/dist/controller.js +34 -11
- package/dist/duration.js +8 -0
- package/dist/openai-account.js +12 -8
- package/dist/providers/anthropic-stream.js +8 -0
- package/dist/providers/anthropic-wire.js +22 -10
- package/dist/providers/ollama-stream.js +7 -0
- package/dist/providers/ollama-wire.js +11 -3
- package/dist/providers/openai-stream.js +57 -2
- package/dist/providers/openai-wire.js +27 -12
- package/dist/sessions/codec.js +13 -5
- package/dist/settings-command.js +0 -5
- package/dist/settings.js +0 -2
- package/dist/steering.js +58 -0
- package/dist/tools/args.js +1 -1
- package/dist/transcript.js +6 -1
- package/dist/tui/activity.js +20 -5
- package/dist/tui/app-input.js +31 -6
- package/dist/tui/app-workflows.js +55 -13
- package/dist/tui/app.js +18 -3
- package/dist/tui/blocks.js +8 -2
- package/dist/tui/components/messages.js +19 -12
- package/dist/tui/components/misc.js +7 -6
- package/dist/tui/components/status.js +12 -1
- package/dist/tui/components/tool.js +143 -46
- package/dist/tui/help.js +1 -1
- package/dist/tui/motion.js +32 -0
- package/dist/tui/session-view.js +3 -2
- package/dist/tui/transcript-grammar.js +21 -0
- package/dist/tui/transcript-view.js +132 -7
- package/dist/tui/turn.js +49 -31
- package/dist/tui/view.js +4 -2
- package/dist/ui/inline.js +7 -7
- package/dist/ui/render.js +1 -3
- package/dist/ui/theme.js +19 -18
- package/package.json +1 -1
package/dist/settings-command.js
CHANGED
|
@@ -41,9 +41,6 @@ export async function settingsCommand(session, host) {
|
|
|
41
41
|
case "maxTokens":
|
|
42
42
|
await numberSetting(session, host, "maxTokens", "max output tokens");
|
|
43
43
|
break;
|
|
44
|
-
case "maxSteps":
|
|
45
|
-
await numberSetting(session, host, "maxSteps", "max tool steps");
|
|
46
|
-
break;
|
|
47
44
|
case "compactionPercent":
|
|
48
45
|
await compactionSetting(session, host);
|
|
49
46
|
break;
|
|
@@ -81,7 +78,6 @@ function settingsItems(values) {
|
|
|
81
78
|
action: "maxTokens",
|
|
82
79
|
option: { label: "max output tokens", value: String(values.maxTokens) },
|
|
83
80
|
}]),
|
|
84
|
-
{ action: "maxSteps", option: { label: "max tool steps", value: String(values.maxSteps) } },
|
|
85
81
|
{
|
|
86
82
|
action: "compactionPercent",
|
|
87
83
|
option: { label: "context compaction", value: `${values.compactionPercent}%` },
|
|
@@ -102,7 +98,6 @@ function settingsValues(session) {
|
|
|
102
98
|
model: session.model,
|
|
103
99
|
effort: session.config.effort,
|
|
104
100
|
...(session.provider.id === "openai-codex" ? {} : { maxTokens: session.config.maxTokens }),
|
|
105
|
-
maxSteps: session.config.maxSteps,
|
|
106
101
|
compactionPercent: session.config.compactionPercent,
|
|
107
102
|
reducedMotion: session.config.reducedMotion,
|
|
108
103
|
};
|
package/dist/settings.js
CHANGED
|
@@ -59,7 +59,6 @@ function normalize(value) {
|
|
|
59
59
|
const effort = member(value["effort"], EFFORTS);
|
|
60
60
|
const reducedMotion = typeof value["reducedMotion"] === "boolean" ? value["reducedMotion"] : undefined;
|
|
61
61
|
const maxTokens = positiveInteger(value["maxTokens"]);
|
|
62
|
-
const maxSteps = positiveInteger(value["maxSteps"]);
|
|
63
62
|
const compactionPercent = percentage(value["compactionPercent"]);
|
|
64
63
|
return {
|
|
65
64
|
...(provider === undefined ? {} : { provider }),
|
|
@@ -68,7 +67,6 @@ function normalize(value) {
|
|
|
68
67
|
...(effort === undefined ? {} : { effort }),
|
|
69
68
|
...(reducedMotion === undefined ? {} : { reducedMotion }),
|
|
70
69
|
...(maxTokens === undefined ? {} : { maxTokens }),
|
|
71
|
-
...(maxSteps === undefined ? {} : { maxSteps }),
|
|
72
70
|
...(compactionPercent === undefined ? {} : { compactionPercent }),
|
|
73
71
|
};
|
|
74
72
|
}
|
package/dist/steering.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// A bounded cooperative inbox for guidance submitted during an active turn.
|
|
2
|
+
export const MAX_STEERING_MESSAGES = 8;
|
|
3
|
+
export const MAX_STEERING_CODE_UNITS = 32_768;
|
|
4
|
+
/**
|
|
5
|
+
* Build one inbox for one model turn. `drainOrClose` is the atomic completion
|
|
6
|
+
* handshake: pending guidance keeps the turn open; an empty queue closes it
|
|
7
|
+
* before the final checkpoint so late input cannot disappear.
|
|
8
|
+
*/
|
|
9
|
+
export function steeringInbox(changed = () => { }) {
|
|
10
|
+
let accepting = true;
|
|
11
|
+
let codeUnits = 0;
|
|
12
|
+
const messages = [];
|
|
13
|
+
const take = () => {
|
|
14
|
+
if (messages.length === 0)
|
|
15
|
+
return [];
|
|
16
|
+
const drained = messages.splice(0);
|
|
17
|
+
codeUnits = 0;
|
|
18
|
+
changed(0, accepting);
|
|
19
|
+
return drained;
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
offer(text) {
|
|
23
|
+
if (!accepting)
|
|
24
|
+
return "closed";
|
|
25
|
+
if (messages.length >= MAX_STEERING_MESSAGES ||
|
|
26
|
+
codeUnits + text.length > MAX_STEERING_CODE_UNITS)
|
|
27
|
+
return "full";
|
|
28
|
+
messages.push(text);
|
|
29
|
+
codeUnits += text.length;
|
|
30
|
+
changed(messages.length, true);
|
|
31
|
+
return "queued";
|
|
32
|
+
},
|
|
33
|
+
drain: take,
|
|
34
|
+
drainOrClose() {
|
|
35
|
+
if (messages.length > 0)
|
|
36
|
+
return { messages: take(), closed: false };
|
|
37
|
+
accepting = false;
|
|
38
|
+
changed(0, false);
|
|
39
|
+
return { messages: [], closed: true };
|
|
40
|
+
},
|
|
41
|
+
close() {
|
|
42
|
+
if (!accepting && messages.length === 0)
|
|
43
|
+
return [];
|
|
44
|
+
accepting = false;
|
|
45
|
+
if (messages.length === 0) {
|
|
46
|
+
changed(0, false);
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
return take();
|
|
50
|
+
},
|
|
51
|
+
get pending() {
|
|
52
|
+
return messages.length;
|
|
53
|
+
},
|
|
54
|
+
get accepting() {
|
|
55
|
+
return accepting;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
package/dist/tools/args.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// tool takes three arguments and this is the whole job.
|
|
3
3
|
//
|
|
4
4
|
// Every throw here becomes an is_error tool result the model can read and
|
|
5
|
-
// correct on the next
|
|
5
|
+
// correct on the next request, so the messages are written for that reader.
|
|
6
6
|
export function requireString(args, name, allowEmpty = false) {
|
|
7
7
|
const value = args[name];
|
|
8
8
|
if (typeof value !== "string" || (!allowEmpty && value === "")) {
|
package/dist/transcript.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// A portable transcript: screen blocks in, Markdown out.
|
|
2
2
|
import { terminalText } from "./ui/terminal-text.js";
|
|
3
|
+
import { toolDuration } from "./duration.js";
|
|
3
4
|
export function defaultTranscriptName(now = new Date()) {
|
|
4
5
|
return `jecode-transcript-${now.toISOString().replace(/[-:.]/g, "")}.md`;
|
|
5
6
|
}
|
|
@@ -17,7 +18,11 @@ export function transcriptMarkdown(blocks) {
|
|
|
17
18
|
out.push("<details>", "<summary>Reasoning</summary>", "", safeMultiline(block.text), "", "</details>", "");
|
|
18
19
|
break;
|
|
19
20
|
case "tool":
|
|
20
|
-
|
|
21
|
+
const outcome = [
|
|
22
|
+
block.right === "" ? undefined : safeInline(block.right),
|
|
23
|
+
block.durationMs === undefined ? undefined : toolDuration(block.durationMs),
|
|
24
|
+
].filter((part) => part !== undefined).join(" · ");
|
|
25
|
+
out.push(`- **${safeInline(block.name)}**${block.target === "" ? "" : ` \`${inlineCode(block.target)}\``}${outcome === "" ? "" : ` — ${outcome}`}`);
|
|
21
26
|
if ((block.body?.length ?? 0) > 0) {
|
|
22
27
|
out.push("", "````text", ...(block.body ?? []).map(detail), "````", "");
|
|
23
28
|
}
|
package/dist/tui/activity.js
CHANGED
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
// One foreground operation owns cancellation and elapsed time.
|
|
2
2
|
export function begin(kind, label, now = Date.now()) {
|
|
3
|
-
return {
|
|
3
|
+
return {
|
|
4
|
+
kind,
|
|
5
|
+
label,
|
|
6
|
+
control: new AbortController(),
|
|
7
|
+
startedAt: now,
|
|
8
|
+
phase: { label, startedAt: now },
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/** Change the visible phase without restarting its timer on repeated stream chunks. */
|
|
12
|
+
export function transition(activity, label, now = Date.now()) {
|
|
13
|
+
if (activity.phase.label === label)
|
|
14
|
+
return;
|
|
15
|
+
activity.phase = { label, startedAt: now };
|
|
4
16
|
}
|
|
5
17
|
export function elapsed(activity, now = Date.now()) {
|
|
6
|
-
|
|
18
|
+
return since(activity.startedAt, now);
|
|
19
|
+
}
|
|
20
|
+
export function activityStatus(activity, now = Date.now()) {
|
|
21
|
+
return `${activity.phase.label} · ${since(activity.phase.startedAt, now)}`;
|
|
22
|
+
}
|
|
23
|
+
function since(startedAt, now) {
|
|
24
|
+
const seconds = Math.max(0, Math.floor((now - startedAt) / 1_000));
|
|
7
25
|
if (seconds < 60)
|
|
8
26
|
return `${seconds}s`;
|
|
9
27
|
const minutes = Math.floor(seconds / 60);
|
|
10
28
|
return `${minutes}m ${String(seconds % 60).padStart(2, "0")}s`;
|
|
11
29
|
}
|
|
12
|
-
export function activityStatus(activity, label = activity.label, now = Date.now()) {
|
|
13
|
-
return `${label} · ${elapsed(activity, now)}`;
|
|
14
|
-
}
|
package/dist/tui/app-input.js
CHANGED
|
@@ -69,6 +69,8 @@ export function appInput(options) {
|
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
71
71
|
case "tab": {
|
|
72
|
+
if (state.activity !== undefined)
|
|
73
|
+
return;
|
|
72
74
|
const completion = state.completing ?? activateCompletion(state.editor.text);
|
|
73
75
|
const completed = completion === undefined ? undefined : selectedCompletion(completion);
|
|
74
76
|
if (completed !== undefined) {
|
|
@@ -107,7 +109,7 @@ export function appInput(options) {
|
|
|
107
109
|
const edited = applyKey(state.editor, key);
|
|
108
110
|
if (edited !== undefined) {
|
|
109
111
|
state.editor = edited;
|
|
110
|
-
state.completing = activateCompletion(edited.text);
|
|
112
|
+
state.completing = state.activity === undefined ? activateCompletion(edited.text) : undefined;
|
|
111
113
|
}
|
|
112
114
|
}
|
|
113
115
|
function recall(step) {
|
|
@@ -131,9 +133,26 @@ export function appInput(options) {
|
|
|
131
133
|
}
|
|
132
134
|
function submit() {
|
|
133
135
|
const text = state.editor.text.trim();
|
|
134
|
-
if (text === ""
|
|
136
|
+
if (text === "")
|
|
135
137
|
return;
|
|
136
138
|
const isCommand = text.startsWith("/");
|
|
139
|
+
if (state.activity !== undefined) {
|
|
140
|
+
if (isCommand) {
|
|
141
|
+
keep("Slash commands run after the active work finishes");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const result = actions.steer(text);
|
|
145
|
+
if (result !== "queued") {
|
|
146
|
+
keep(result === "full"
|
|
147
|
+
? "Steering queue is full"
|
|
148
|
+
: result === "closed"
|
|
149
|
+
? "The turn is finishing"
|
|
150
|
+
: "Wait for the active command to finish");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
accept(text);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
137
156
|
if (!isCommand) {
|
|
138
157
|
const blocker = turnBlocker(session);
|
|
139
158
|
if (blocker !== undefined) {
|
|
@@ -141,6 +160,13 @@ export function appInput(options) {
|
|
|
141
160
|
return;
|
|
142
161
|
}
|
|
143
162
|
}
|
|
163
|
+
accept(text);
|
|
164
|
+
if (isCommand)
|
|
165
|
+
void actions.command(text);
|
|
166
|
+
else
|
|
167
|
+
void actions.turn(text);
|
|
168
|
+
}
|
|
169
|
+
function accept(text) {
|
|
144
170
|
state.editor = edit.EMPTY;
|
|
145
171
|
state.recall = -1;
|
|
146
172
|
state.draft = "";
|
|
@@ -149,10 +175,9 @@ export function appInput(options) {
|
|
|
149
175
|
state.scroll = 0;
|
|
150
176
|
state.follow = true;
|
|
151
177
|
state.unseen = 0;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
void actions.turn(text);
|
|
178
|
+
}
|
|
179
|
+
function keep(text) {
|
|
180
|
+
feedback.show({ text: `${text} · prompt kept`, tone: "warn", timeoutMs: 4_000 });
|
|
156
181
|
}
|
|
157
182
|
return { handle };
|
|
158
183
|
}
|
|
@@ -6,9 +6,11 @@ import { compactContext } from "../context/compactor.js";
|
|
|
6
6
|
import { compactSession } from "../context/manual.js";
|
|
7
7
|
import { isContextOverflow } from "../context/policy.js";
|
|
8
8
|
import { updateSettings } from "../settings.js";
|
|
9
|
+
import { steeringInbox } from "../steering.js";
|
|
9
10
|
import { saveTranscript } from "../transcript-export.js";
|
|
10
11
|
import { recordAuxiliaryUsage, recordRequestInput, recordUsage } from "../usage.js";
|
|
11
12
|
import { selectTimeline } from "../timeline.js";
|
|
13
|
+
import { transition } from "./activity.js";
|
|
12
14
|
import { answerAt } from "./approve.js";
|
|
13
15
|
import * as edit from "./editor.js";
|
|
14
16
|
import { cancel as cancelOpen } from "./overlay.js";
|
|
@@ -17,6 +19,7 @@ import { transcribe } from "./turn.js";
|
|
|
17
19
|
const WAITING = "Waiting";
|
|
18
20
|
export function appWorkflows(options) {
|
|
19
21
|
const { session, state, permissions, feedback } = options;
|
|
22
|
+
let activeSteering;
|
|
20
23
|
const choose = (picker) => new Promise((resolve) => {
|
|
21
24
|
state.open = { picker, settle: resolve };
|
|
22
25
|
options.render();
|
|
@@ -25,6 +28,7 @@ export function appWorkflows(options) {
|
|
|
25
28
|
const activity = options.startActivity("command", `Running ${text.split(/\s+/)[0]}`);
|
|
26
29
|
if (activity === undefined)
|
|
27
30
|
return;
|
|
31
|
+
const status = (label) => transition(activity, label);
|
|
28
32
|
try {
|
|
29
33
|
const outcome = await handleCommand(text, session, {
|
|
30
34
|
emit: options.commandNotice,
|
|
@@ -43,7 +47,7 @@ export function appWorkflows(options) {
|
|
|
43
47
|
options.render();
|
|
44
48
|
}),
|
|
45
49
|
status: (said) => {
|
|
46
|
-
|
|
50
|
+
status(said ?? activity.label);
|
|
47
51
|
options.render();
|
|
48
52
|
},
|
|
49
53
|
reset: async () => {
|
|
@@ -78,8 +82,8 @@ export function appWorkflows(options) {
|
|
|
78
82
|
}
|
|
79
83
|
return compactSession(session, {
|
|
80
84
|
signal: activity.control.signal,
|
|
81
|
-
onStatus: (
|
|
82
|
-
|
|
85
|
+
onStatus: (said) => {
|
|
86
|
+
status(said ?? activity.label);
|
|
83
87
|
options.render();
|
|
84
88
|
},
|
|
85
89
|
});
|
|
@@ -108,6 +112,15 @@ export function appWorkflows(options) {
|
|
|
108
112
|
const activity = options.startActivity("turn", WAITING);
|
|
109
113
|
if (activity === undefined)
|
|
110
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);
|
|
111
124
|
const parentId = session.conversation.activeNodeId;
|
|
112
125
|
const history = session.conversation.history;
|
|
113
126
|
const modelHistory = session.conversation.contextHistory;
|
|
@@ -117,12 +130,13 @@ export function appWorkflows(options) {
|
|
|
117
130
|
const prospectiveNodeId = session.conversation.nodes.length + 1;
|
|
118
131
|
let nodeId;
|
|
119
132
|
let context;
|
|
133
|
+
const unpersistedSteering = [];
|
|
120
134
|
let firstPolicy = true;
|
|
121
135
|
const policy = () => {
|
|
122
136
|
let visible = firstPolicy;
|
|
123
137
|
firstPolicy = false;
|
|
124
138
|
if (visible) {
|
|
125
|
-
|
|
139
|
+
status("Checking context");
|
|
126
140
|
options.render();
|
|
127
141
|
}
|
|
128
142
|
return resolveContextPolicy({
|
|
@@ -130,14 +144,14 @@ export function appWorkflows(options) {
|
|
|
130
144
|
model: session.model,
|
|
131
145
|
compactionPercent: session.config.compactionPercent,
|
|
132
146
|
signal: activity.control.signal,
|
|
133
|
-
onStatus: (
|
|
147
|
+
onStatus: (said) => {
|
|
134
148
|
visible = true;
|
|
135
|
-
|
|
149
|
+
status(said);
|
|
136
150
|
options.render();
|
|
137
151
|
},
|
|
138
152
|
}).finally(() => {
|
|
139
153
|
if (visible) {
|
|
140
|
-
|
|
154
|
+
status(WAITING);
|
|
141
155
|
options.render();
|
|
142
156
|
}
|
|
143
157
|
});
|
|
@@ -157,7 +171,7 @@ export function appWorkflows(options) {
|
|
|
157
171
|
options.render();
|
|
158
172
|
},
|
|
159
173
|
status: (text) => {
|
|
160
|
-
|
|
174
|
+
status(text);
|
|
161
175
|
},
|
|
162
176
|
usage: (usage) => recordUsage(session.usage, usage),
|
|
163
177
|
requestInput: (inputTokens) => recordRequestInput(session.usage, inputTokens),
|
|
@@ -181,6 +195,7 @@ export function appWorkflows(options) {
|
|
|
181
195
|
session.conversation = next;
|
|
182
196
|
nodeId = next.activeNodeId;
|
|
183
197
|
state.committedNodeId = next.activeNodeId;
|
|
198
|
+
unpersistedSteering.length = 0;
|
|
184
199
|
};
|
|
185
200
|
const compact = async (checkpoint, projected, request) => {
|
|
186
201
|
if (request.reason === "overflow" &&
|
|
@@ -201,11 +216,11 @@ export function appWorkflows(options) {
|
|
|
201
216
|
force,
|
|
202
217
|
policy: request.policy,
|
|
203
218
|
onBegin: () => {
|
|
204
|
-
|
|
219
|
+
status("Compacting");
|
|
205
220
|
options.render();
|
|
206
221
|
},
|
|
207
222
|
onEnd: () => {
|
|
208
|
-
|
|
223
|
+
status(WAITING);
|
|
209
224
|
options.render();
|
|
210
225
|
},
|
|
211
226
|
});
|
|
@@ -217,6 +232,11 @@ export function appWorkflows(options) {
|
|
|
217
232
|
return result.messages;
|
|
218
233
|
};
|
|
219
234
|
events.onContext = compact;
|
|
235
|
+
events.onSteering = (guidance) => {
|
|
236
|
+
unpersistedSteering.push(guidance);
|
|
237
|
+
options.emit({ kind: "user", text: guidance });
|
|
238
|
+
options.render();
|
|
239
|
+
};
|
|
220
240
|
events.onCheckpoint = async (checkpoint, settlement, projected) => {
|
|
221
241
|
await persist(checkpoint, settlement);
|
|
222
242
|
const compacted = await compact(checkpoint, projected, {
|
|
@@ -231,7 +251,7 @@ export function appWorkflows(options) {
|
|
|
231
251
|
let finishReason;
|
|
232
252
|
let failed;
|
|
233
253
|
try {
|
|
234
|
-
await runTurn(history, controllerOptions(session, policy, permissions.availableTools()), events, activity.control.signal, modelHistory);
|
|
254
|
+
await runTurn(history, controllerOptions(session, policy, permissions.availableTools(), inbox), events, activity.control.signal, modelHistory);
|
|
235
255
|
}
|
|
236
256
|
catch (error) {
|
|
237
257
|
const interrupted = activity.control.signal.aborted;
|
|
@@ -247,6 +267,10 @@ export function appWorkflows(options) {
|
|
|
247
267
|
}
|
|
248
268
|
}
|
|
249
269
|
finally {
|
|
270
|
+
let pendingSteering = inbox.close();
|
|
271
|
+
if (activeSteering === inbox)
|
|
272
|
+
activeSteering = undefined;
|
|
273
|
+
state.steering = undefined;
|
|
250
274
|
try {
|
|
251
275
|
events.finish(finishReason);
|
|
252
276
|
if (failed !== undefined) {
|
|
@@ -265,7 +289,14 @@ export function appWorkflows(options) {
|
|
|
265
289
|
// been saved. Revert to the last durable path and return the input
|
|
266
290
|
// to the composer so the user can retry without losing it.
|
|
267
291
|
options.replaceTranscript();
|
|
268
|
-
state.editor = edit.of(
|
|
292
|
+
state.editor = edit.of([
|
|
293
|
+
...(nodeId === undefined ? [text] : []),
|
|
294
|
+
...unpersistedSteering,
|
|
295
|
+
...pendingSteering,
|
|
296
|
+
state.editor.text,
|
|
297
|
+
].filter((part) => part !== "").join("\n\n"));
|
|
298
|
+
state.completing = undefined;
|
|
299
|
+
pendingSteering = [];
|
|
269
300
|
feedback.show({
|
|
270
301
|
text: error.message,
|
|
271
302
|
tone: "error",
|
|
@@ -275,11 +306,22 @@ export function appWorkflows(options) {
|
|
|
275
306
|
}
|
|
276
307
|
}
|
|
277
308
|
finally {
|
|
309
|
+
restorePendingSteering(state, pendingSteering);
|
|
278
310
|
options.finishActivity(activity);
|
|
279
311
|
}
|
|
280
312
|
}
|
|
281
313
|
}
|
|
282
|
-
|
|
314
|
+
function steer(text) {
|
|
315
|
+
return activeSteering?.offer(text) ?? "unavailable";
|
|
316
|
+
}
|
|
317
|
+
return { command, turn, steer };
|
|
318
|
+
}
|
|
319
|
+
function restorePendingSteering(state, messages) {
|
|
320
|
+
if (messages.length === 0)
|
|
321
|
+
return;
|
|
322
|
+
const pending = messages.join("\n\n");
|
|
323
|
+
state.editor = edit.of(state.editor.text === "" ? pending : `${pending}\n\n${state.editor.text}`);
|
|
324
|
+
state.completing = undefined;
|
|
283
325
|
}
|
|
284
326
|
function closeFailedTurn(history, settlement) {
|
|
285
327
|
const closed = [...history];
|
package/dist/tui/app.js
CHANGED
|
@@ -21,6 +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 MOTION_FRAME_MS = 40;
|
|
24
25
|
const ACTIVITY_REFRESH_MS = 1_000;
|
|
25
26
|
/** How long a lone escape waits to prove it is not the start of a sequence. */
|
|
26
27
|
const ESCAPE_MS = 25;
|
|
@@ -36,6 +37,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
36
37
|
const permissions = sessionPermissions(session.tools, session.config.autoApprove);
|
|
37
38
|
let closed;
|
|
38
39
|
let frameTimer;
|
|
40
|
+
let motionTimer;
|
|
39
41
|
let activityTimer;
|
|
40
42
|
let escapeTimer;
|
|
41
43
|
let stopResize = () => { };
|
|
@@ -67,10 +69,12 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
67
69
|
footer: footerInfo(session, workspace),
|
|
68
70
|
status: state.activity === undefined
|
|
69
71
|
? undefined
|
|
70
|
-
: activityStatus(state.activity,
|
|
72
|
+
: activityStatus(state.activity, now),
|
|
73
|
+
steering: state.steering,
|
|
71
74
|
feedback: state.feedback,
|
|
72
75
|
readiness: turnBlocker(session),
|
|
73
76
|
now,
|
|
77
|
+
reducedMotion: session.config.reducedMotion,
|
|
74
78
|
modal: overlay.shown(state.open),
|
|
75
79
|
menu: completionOptions(state.completing),
|
|
76
80
|
menuIndex: state.completing?.index,
|
|
@@ -98,6 +102,16 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
98
102
|
paint.paint(frame.rows, frame.cursor);
|
|
99
103
|
if (frame.transcriptPending)
|
|
100
104
|
render();
|
|
105
|
+
if (frame.transcriptAnimating && motionTimer === undefined) {
|
|
106
|
+
motionTimer = setTimeout(() => guard(() => {
|
|
107
|
+
motionTimer = undefined;
|
|
108
|
+
render();
|
|
109
|
+
}), Math.max(0, MOTION_FRAME_MS - FRAME_MS));
|
|
110
|
+
}
|
|
111
|
+
else if (!frame.transcriptAnimating && motionTimer !== undefined) {
|
|
112
|
+
clearTimeout(motionTimer);
|
|
113
|
+
motionTimer = undefined;
|
|
114
|
+
}
|
|
101
115
|
};
|
|
102
116
|
// Streaming produces a token at a time; painting at that rate is wasted
|
|
103
117
|
// work, so repaints coalesce onto one frame.
|
|
@@ -148,6 +162,8 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
148
162
|
clearInterval(activityTimer);
|
|
149
163
|
if (frameTimer !== undefined)
|
|
150
164
|
clearTimeout(frameTimer);
|
|
165
|
+
if (motionTimer !== undefined)
|
|
166
|
+
clearTimeout(motionTimer);
|
|
151
167
|
if (escapeTimer !== undefined)
|
|
152
168
|
clearTimeout(escapeTimer);
|
|
153
169
|
safely(() => feedback.close());
|
|
@@ -193,7 +209,6 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
193
209
|
return undefined;
|
|
194
210
|
const activity = begin(kind, label);
|
|
195
211
|
state.activity = activity;
|
|
196
|
-
state.status = label;
|
|
197
212
|
activityTimer = setInterval(() => guard(() => {
|
|
198
213
|
let activeTool;
|
|
199
214
|
for (let index = state.blocks.length - 1; index >= 0; index--) {
|
|
@@ -215,7 +230,6 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
215
230
|
clearInterval(activityTimer);
|
|
216
231
|
activityTimer = undefined;
|
|
217
232
|
state.activity = undefined;
|
|
218
|
-
state.status = undefined;
|
|
219
233
|
state.open = overlay.cancel(state.open);
|
|
220
234
|
if (state.closeWhenIdle)
|
|
221
235
|
quit();
|
|
@@ -248,6 +262,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
248
262
|
actions: {
|
|
249
263
|
command: (text) => track(actions.command(text)),
|
|
250
264
|
turn: (text) => track(actions.turn(text)),
|
|
265
|
+
steer: (text) => actions.steer(text),
|
|
251
266
|
},
|
|
252
267
|
live: () => live,
|
|
253
268
|
quit,
|
package/dist/tui/blocks.js
CHANGED
|
@@ -9,11 +9,17 @@ export function render(block, width, pal, context = {}) {
|
|
|
9
9
|
case "answer":
|
|
10
10
|
return renderAnswer(block, width, pal);
|
|
11
11
|
case "reasoning":
|
|
12
|
-
return renderReasoning(block, width, pal
|
|
12
|
+
return renderReasoning(block, width, pal, {
|
|
13
|
+
continues: context.previous?.kind === "reasoning",
|
|
14
|
+
followsTool: context.previous?.kind === "tool",
|
|
15
|
+
});
|
|
13
16
|
case "tool":
|
|
14
17
|
return renderTool(block, width, pal, {
|
|
15
|
-
continues: context.previous?.kind === "tool",
|
|
18
|
+
continues: context.previous?.kind === "tool" || context.previous?.kind === "reasoning",
|
|
19
|
+
followsReasoning: context.previous?.kind === "reasoning",
|
|
16
20
|
now: context.now,
|
|
21
|
+
motion: context.motion,
|
|
22
|
+
reducedMotion: context.reducedMotion,
|
|
17
23
|
});
|
|
18
24
|
case "notice":
|
|
19
25
|
return renderNotice(block, width, pal);
|
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
import { trailingText } from "../../text-boundary.js";
|
|
2
|
-
import {
|
|
2
|
+
import { row } from "../../ui/render.js";
|
|
3
3
|
import { markdown } from "../../ui/markdown.js";
|
|
4
|
-
|
|
4
|
+
import { transcriptLead, transcriptMark, transcriptWidth } from "../transcript-grammar.js";
|
|
5
5
|
export const REASONING_PREVIEW_ROWS = 3;
|
|
6
6
|
const MIN_REASONING_PREVIEW_CHARS = 4_096;
|
|
7
7
|
const REASONING_PREVIEW_OVERSCAN = 12;
|
|
8
8
|
export function renderUser(block, width, pal) {
|
|
9
|
-
const inner =
|
|
9
|
+
const inner = transcriptWidth(width);
|
|
10
10
|
const content = markdown(block.text, inner, pal, inner);
|
|
11
11
|
return [
|
|
12
12
|
"",
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
...content.map((line, index) => row(width, [
|
|
14
|
+
...transcriptLead(width, index === 0
|
|
15
|
+
? { text: "❯", fg: pal.accent, bold: true }
|
|
16
|
+
: undefined),
|
|
17
|
+
...line.segs,
|
|
18
|
+
])),
|
|
16
19
|
];
|
|
17
20
|
}
|
|
18
21
|
export function renderAnswer(block, width, pal) {
|
|
19
|
-
const inner = Math.max(8, width - PAD * 2);
|
|
20
22
|
return [
|
|
21
23
|
"",
|
|
22
|
-
...markdown(block.text,
|
|
24
|
+
...markdown(block.text, width, pal, width).map((line) => row(width, line.segs)),
|
|
23
25
|
];
|
|
24
26
|
}
|
|
25
|
-
export function renderReasoning(block, width, pal) {
|
|
26
|
-
const inner =
|
|
27
|
+
export function renderReasoning(block, width, pal, context = {}) {
|
|
28
|
+
const inner = transcriptWidth(width);
|
|
27
29
|
// Expanding a live stream is deferred until it is sealed. Re-parsing an
|
|
28
30
|
// ever-growing full thought on every token makes the whole TUI stall.
|
|
29
31
|
const expanded = block.expanded === true && block.live !== true;
|
|
@@ -33,8 +35,13 @@ export function renderReasoning(block, width, pal) {
|
|
|
33
35
|
const content = markdown(source.text, inner, pal, inner);
|
|
34
36
|
const visible = expanded ? content : content.slice(-REASONING_PREVIEW_ROWS);
|
|
35
37
|
return [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
...(context.followsTool === true
|
|
39
|
+
? [row(width, transcriptMark(width, { text: "│", fg: pal.rule }))]
|
|
40
|
+
: context.continues === true ? [] : [""]),
|
|
41
|
+
...visible.map((line) => row(width, [
|
|
42
|
+
...transcriptLead(width, { text: "│", fg: pal.rule }),
|
|
43
|
+
...line.segs.map((seg) => ({ ...seg, fg: pal.ink.dim, italic: true })),
|
|
44
|
+
])),
|
|
38
45
|
];
|
|
39
46
|
}
|
|
40
47
|
export function reasoningPreviewSource(text, width) {
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { row, wrap } from "../../ui/render.js";
|
|
2
|
+
import { transcriptLead, transcriptWidth } from "../transcript-grammar.js";
|
|
2
3
|
export function renderNotice(block, width, pal) {
|
|
3
4
|
const fg = {
|
|
4
5
|
info: pal.ink.muted,
|
|
5
6
|
warn: pal.ink.attention,
|
|
6
7
|
error: pal.ink.removed,
|
|
7
8
|
};
|
|
8
|
-
const mark = block.tone === "error" ? "×
|
|
9
|
+
const mark = block.tone === "error" ? "×" : block.tone === "warn" ? "!" : "·";
|
|
9
10
|
return [
|
|
10
11
|
"",
|
|
11
|
-
...wrap(block.text,
|
|
12
|
-
...(
|
|
13
|
-
? []
|
|
14
|
-
:
|
|
12
|
+
...wrap(block.text, transcriptWidth(width)).map((line, index) => row(width, [
|
|
13
|
+
...transcriptLead(width, index === 0
|
|
14
|
+
? { text: mark, fg: fg[block.tone], bold: block.tone !== "info" }
|
|
15
|
+
: undefined),
|
|
15
16
|
{ text: line, fg: fg[block.tone] },
|
|
16
|
-
]
|
|
17
|
+
])),
|
|
17
18
|
];
|
|
18
19
|
}
|
|
@@ -6,8 +6,19 @@ export function renderStatus(info, pal) {
|
|
|
6
6
|
if (urgent !== undefined)
|
|
7
7
|
return withUnseen(feedbackSegments(urgent, pal), info.unseen, pal);
|
|
8
8
|
if (info.status !== undefined) {
|
|
9
|
+
const active = info.steering !== undefined && info.steering > 0
|
|
10
|
+
? [
|
|
11
|
+
{ text: `${info.steering} queued`, fg: pal.accent, bold: true },
|
|
12
|
+
{ text: ` · ${info.status}`, fg: pal.ink.muted, optional: true },
|
|
13
|
+
]
|
|
14
|
+
: [
|
|
15
|
+
{ text: info.status, fg: pal.ink.muted },
|
|
16
|
+
...(info.steering === 0
|
|
17
|
+
? [{ text: " · enter to steer", fg: pal.ink.dim, optional: true }]
|
|
18
|
+
: []),
|
|
19
|
+
];
|
|
9
20
|
return withUnseen([
|
|
10
|
-
|
|
21
|
+
...active,
|
|
11
22
|
{ text: " · esc to interrupt", fg: pal.ink.dim, optional: true },
|
|
12
23
|
], info.unseen, pal);
|
|
13
24
|
}
|