@giovannijecha/jecode 0.7.4 → 0.8.0

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.
@@ -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
  }
@@ -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
+ }
@@ -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 step, so the messages are written for that reader.
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 === "")) {
@@ -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
- out.push(`- **${safeInline(block.name)}**${block.target === "" ? "" : ` \`${inlineCode(block.target)}\``}${block.right === "" ? "" : ` — ${safeInline(block.right)}`}`);
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
  }
@@ -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 { kind, label, control: new AbortController(), startedAt: now };
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
- const seconds = Math.max(0, Math.floor((now - activity.startedAt) / 1_000));
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
- }
@@ -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 === "" || state.activity !== undefined)
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
- if (isCommand)
153
- void actions.command(text);
154
- else
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
- state.status = said ?? activity.label;
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: (status) => {
82
- state.status = status ?? activity.label;
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
- state.status = "Checking context";
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: (status) => {
147
+ onStatus: (said) => {
134
148
  visible = true;
135
- state.status = status;
149
+ status(said);
136
150
  options.render();
137
151
  },
138
152
  }).finally(() => {
139
153
  if (visible) {
140
- state.status = WAITING;
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
- state.status = text;
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
- state.status = "Compacting";
219
+ status("Compacting");
205
220
  options.render();
206
221
  },
207
222
  onEnd: () => {
208
- state.status = WAITING;
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(text);
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
- return { command, turn };
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, state.status ?? state.activity.label, now),
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,
@@ -9,11 +9,16 @@ 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 === "tool" || context.previous?.kind === "reasoning",
14
+ });
13
15
  case "tool":
14
16
  return renderTool(block, width, pal, {
15
- continues: context.previous?.kind === "tool",
17
+ continues: context.previous?.kind === "tool" || context.previous?.kind === "reasoning",
18
+ followsReasoning: context.previous?.kind === "reasoning",
16
19
  now: context.now,
20
+ motion: context.motion,
21
+ reducedMotion: context.reducedMotion,
17
22
  });
18
23
  case "notice":
19
24
  return renderNotice(block, width, pal);
@@ -1,29 +1,32 @@
1
1
  import { trailingText } from "../../text-boundary.js";
2
- import { blank, row } from "../../ui/render.js";
2
+ import { row } from "../../ui/render.js";
3
3
  import { markdown } from "../../ui/markdown.js";
4
- const PAD = 1;
4
+ import { transcriptLead, 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 = Math.max(8, width - PAD * 2);
9
+ const inner = transcriptWidth(width);
10
10
  const content = markdown(block.text, inner, pal, inner);
11
11
  return [
12
12
  "",
13
- blank(width, pal.surface.subtle),
14
- ...content.map((line) => row(width, line.segs, [], pal.surface.subtle, PAD)),
15
- blank(width, pal.surface.subtle),
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);
22
+ const inner = transcriptWidth(width);
20
23
  return [
21
24
  "",
22
- ...markdown(block.text, inner, pal, inner).map((line) => row(width, line.segs, [], undefined, PAD)),
25
+ ...markdown(block.text, inner, pal, inner).map((line) => row(width, [...transcriptLead(width), ...line.segs])),
23
26
  ];
24
27
  }
25
- export function renderReasoning(block, width, pal) {
26
- const inner = Math.max(8, width - PAD * 2);
28
+ export function renderReasoning(block, width, pal, context = {}) {
29
+ const inner = transcriptWidth(width);
27
30
  // Expanding a live stream is deferred until it is sealed. Re-parsing an
28
31
  // ever-growing full thought on every token makes the whole TUI stall.
29
32
  const expanded = block.expanded === true && block.live !== true;
@@ -33,8 +36,11 @@ export function renderReasoning(block, width, pal) {
33
36
  const content = markdown(source.text, inner, pal, inner);
34
37
  const visible = expanded ? content : content.slice(-REASONING_PREVIEW_ROWS);
35
38
  return [
36
- "",
37
- ...visible.map((line) => row(width, line.segs.map((seg) => ({ ...seg, fg: pal.ink.dim, italic: true })), [], undefined, PAD)),
39
+ ...(context.continues === true ? [] : [""]),
40
+ ...visible.map((line) => row(width, [
41
+ ...transcriptLead(width, { text: "│", fg: pal.rule }),
42
+ ...line.segs.map((seg) => ({ ...seg, fg: pal.ink.dim, italic: true })),
43
+ ])),
38
44
  ];
39
45
  }
40
46
  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" ? "× " : block.tone === "warn" ? "! " : undefined;
9
+ const mark = block.tone === "error" ? "×" : block.tone === "warn" ? "!" : "·";
9
10
  return [
10
11
  "",
11
- ...wrap(block.text, Math.max(1, width - 3)).map((line, index) => row(width, [
12
- ...(mark === undefined
13
- ? []
14
- : [{ text: index === 0 ? mark : " ", fg: fg[block.tone], bold: index === 0 }]),
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
- ], [], undefined, 1)),
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
- { text: info.status, fg: pal.ink.muted },
21
+ ...active,
11
22
  { text: " · esc to interrupt", fg: pal.ink.dim, optional: true },
12
23
  ], info.unseen, pal);
13
24
  }