@giovannijecha/jecode 0.8.6 → 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.
Files changed (67) hide show
  1. package/README.md +18 -8
  2. package/assets/tokenizers/LICENSE +21 -0
  3. package/assets/tokenizers/o200k-base.tiktoken.gz +0 -0
  4. package/dist/cli-info.js +8 -13
  5. package/dist/commands.js +7 -3
  6. package/dist/config.js +33 -47
  7. package/dist/context/automatic.js +16 -1
  8. package/dist/context/budget.js +5 -2
  9. package/dist/context/compactor.js +91 -37
  10. package/dist/context/diagnostics.js +108 -0
  11. package/dist/context/lifetime.js +18 -0
  12. package/dist/context/manager.js +67 -0
  13. package/dist/context/manual.js +11 -10
  14. package/dist/context/measurement.js +75 -0
  15. package/dist/context/policy.js +13 -10
  16. package/dist/context/request-observation.js +46 -0
  17. package/dist/context/request-projection.js +8 -34
  18. package/dist/context/request.js +22 -0
  19. package/dist/context/tokenizer/bpe.js +68 -0
  20. package/dist/context/tokenizer/o200k.js +54 -0
  21. package/dist/context/tokenizer/vocabulary.js +34 -0
  22. package/dist/controller-request.js +47 -45
  23. package/dist/controller.js +13 -2
  24. package/dist/input-boundary.js +0 -62
  25. package/dist/launch.js +13 -9
  26. package/dist/model-command.js +7 -17
  27. package/dist/oauth-result-page.js +68 -0
  28. package/dist/openai-oauth-callback.js +2 -50
  29. package/dist/permission-command.js +9 -24
  30. package/dist/permissions.js +10 -8
  31. package/dist/providers/anthropic-wire.js +1 -1
  32. package/dist/providers/anthropic.js +3 -1
  33. package/dist/providers/input-measurement.js +85 -0
  34. package/dist/providers/ollama-wire.js +1 -1
  35. package/dist/providers/ollama.js +2 -0
  36. package/dist/providers/openai-codex.js +3 -0
  37. package/dist/providers/openai-stream.js +20 -8
  38. package/dist/providers/openai-summary.js +37 -0
  39. package/dist/providers/openai-wire.js +1 -1
  40. package/dist/providers/openai.js +3 -0
  41. package/dist/settings-command.js +5 -10
  42. package/dist/settings.js +1 -1
  43. package/dist/start.js +35 -51
  44. package/dist/timeline.js +2 -0
  45. package/dist/tui/activity.js +1 -1
  46. package/dist/tui/app-input.js +34 -17
  47. package/dist/tui/app-workflows.js +7 -2
  48. package/dist/tui/app.js +55 -28
  49. package/dist/tui/approve.js +3 -4
  50. package/dist/tui/blocks.js +5 -7
  51. package/dist/tui/command-workflow.js +14 -8
  52. package/dist/tui/components/command-menu.js +1 -1
  53. package/dist/tui/components/menu.js +19 -11
  54. package/dist/tui/components/status.js +1 -1
  55. package/dist/tui/frame.js +8 -3
  56. package/dist/tui/help.js +2 -1
  57. package/dist/tui/picker-layout.js +5 -1
  58. package/dist/tui/screen.js +7 -0
  59. package/dist/tui/session-view.js +0 -1
  60. package/dist/tui/transcript-grammar.js +1 -8
  61. package/dist/tui/transcript-view.js +4 -0
  62. package/dist/tui/turn-workflow.js +47 -76
  63. package/dist/tui/view.js +4 -4
  64. package/dist/ui/render.js +0 -4
  65. package/package.json +5 -1
  66. package/dist/batch-view.js +0 -42
  67. package/dist/batch.js +0 -269
package/dist/tui/app.js CHANGED
@@ -34,7 +34,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
34
34
  const state = appState();
35
35
  state.blocks.push(...session.conversation.transcript);
36
36
  state.committedNodeId = session.conversation.activeNodeId;
37
- const permissions = sessionPermissions(session.tools, session.config.autoApprove);
37
+ const permissions = sessionPermissions(session.tools);
38
38
  let closed;
39
39
  let frameTimer;
40
40
  let motionTimer;
@@ -42,9 +42,10 @@ export async function runApp(session, transcriptRoot, environment = {}) {
42
42
  let escapeTimer;
43
43
  let stopResize = () => { };
44
44
  let stopInput = () => { };
45
+ let stopOutput = () => { };
45
46
  let stopShutdown = () => { };
46
47
  let failure;
47
- let activeWorkflow;
48
+ const activeWorkflows = new Set();
48
49
  // Timers outlive the teardown they were scheduled before. Painting after the
49
50
  // terminal has been handed back would write escapes into the user's shell.
50
51
  let live = true;
@@ -61,22 +62,25 @@ export async function runApp(session, transcriptRoot, environment = {}) {
61
62
  };
62
63
  const view = () => {
63
64
  const now = Date.now();
65
+ const activity = state.command !== undefined && state.open === undefined && state.approval === undefined
66
+ ? state.command : state.activity ?? state.command;
64
67
  return {
65
68
  blocks: state.blocks,
66
69
  editor: state.editor,
67
70
  scroll: state.scroll,
68
71
  unseen: state.unseen,
69
72
  pal: session.palette,
70
- footer: footerInfo(session, workspace),
71
- status: state.activity === undefined
73
+ footer: state.turnFooter === undefined ? footerInfo(session, workspace) : { ...state.turnFooter, workspace },
74
+ status: activity === undefined
72
75
  ? undefined
73
- : activityStatus(state.activity, now),
74
- steering: state.steering,
76
+ : activityStatus(activity, now),
77
+ turnActive: state.activity !== undefined,
78
+ steering: state.command === undefined ? state.steering : undefined,
75
79
  feedback: state.feedback,
76
80
  readiness: turnBlocker(session),
77
81
  now,
78
82
  reducedMotion: session.config.reducedMotion,
79
- modal: overlay.shown(state.open),
83
+ modal: overlay.shown(state.approval ?? state.open),
80
84
  menu: completionOptions(state.completing),
81
85
  menuIndex: state.completing?.index,
82
86
  };
@@ -143,8 +147,8 @@ export async function runApp(session, transcriptRoot, environment = {}) {
143
147
  if (state.follow)
144
148
  state.unseen = 0;
145
149
  };
146
- const replaceTranscript = () => {
147
- state.blocks.splice(0, state.blocks.length, ...session.conversation.transcript);
150
+ const replaceTranscript = (blocks = session.conversation.transcript) => {
151
+ state.blocks.splice(0, state.blocks.length, ...blocks);
148
152
  state.scroll = 0;
149
153
  state.follow = true;
150
154
  state.unseen = 0;
@@ -158,6 +162,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
158
162
  return;
159
163
  live = false;
160
164
  safely(stopInput);
165
+ safely(stopOutput);
161
166
  safely(stopResize);
162
167
  safely(stopShutdown);
163
168
  if (activityTimer !== undefined)
@@ -183,35 +188,42 @@ export async function runApp(session, transcriptRoot, environment = {}) {
183
188
  function fail(error) {
184
189
  failure ??= { error };
185
190
  state.open = overlay.cancel(state.open);
191
+ state.approval = overlay.cancel(state.approval);
186
192
  state.activity?.control.abort(error);
193
+ state.command?.control.abort(error);
187
194
  quit();
188
195
  }
189
196
  function track(work) {
190
197
  const tracked = work
191
198
  .catch((error) => fail(error))
192
199
  .finally(() => {
193
- if (activeWorkflow === tracked)
194
- activeWorkflow = undefined;
200
+ activeWorkflows.delete(tracked);
195
201
  });
196
- activeWorkflow = tracked;
202
+ activeWorkflows.add(tracked);
197
203
  return tracked;
198
204
  }
199
205
  function requestQuit(reason = new Error("interrupted")) {
200
206
  state.open = overlay.cancel(state.open);
201
- const activity = state.activity;
202
- if (activity === undefined) {
207
+ state.approval = overlay.cancel(state.approval);
208
+ if (state.activity === undefined && state.command === undefined) {
203
209
  quit();
204
210
  return;
205
211
  }
206
212
  state.closeWhenIdle = true;
207
- activity.control.abort(reason);
213
+ state.activity?.control.abort(reason);
214
+ state.command?.control.abort(reason);
208
215
  }
209
216
  function startActivity(kind, label) {
210
- if (state.activity !== undefined)
217
+ if (state.closeWhenIdle)
218
+ return undefined;
219
+ if (state.command !== undefined || (kind === "turn" && state.activity !== undefined))
211
220
  return undefined;
212
221
  const activity = begin(kind, label);
213
- state.activity = activity;
214
- activityTimer = setInterval(() => guard(() => {
222
+ if (kind === "turn")
223
+ state.activity = activity;
224
+ else
225
+ state.command = activity;
226
+ activityTimer ??= setInterval(() => guard(() => {
215
227
  let activeTool;
216
228
  for (let index = state.blocks.length - 1; index >= 0; index--) {
217
229
  const block = state.blocks[index];
@@ -226,14 +238,23 @@ export async function runApp(session, transcriptRoot, environment = {}) {
226
238
  return activity;
227
239
  }
228
240
  function finishActivity(activity) {
229
- if (state.activity !== activity)
241
+ if (state.activity === activity) {
242
+ state.activity = undefined;
243
+ state.turnFooter = undefined;
244
+ state.approval = overlay.cancel(state.approval);
245
+ }
246
+ else if (state.command === activity) {
247
+ state.command = undefined;
248
+ state.open = overlay.cancel(state.open);
249
+ }
250
+ else
230
251
  return;
231
- if (activityTimer !== undefined)
252
+ const idle = state.activity === undefined && state.command === undefined;
253
+ if (idle && activityTimer !== undefined) {
232
254
  clearInterval(activityTimer);
233
- activityTimer = undefined;
234
- state.activity = undefined;
235
- state.open = overlay.cancel(state.open);
236
- if (state.closeWhenIdle)
255
+ activityTimer = undefined;
256
+ }
257
+ if (state.closeWhenIdle && idle)
237
258
  quit();
238
259
  else
239
260
  render();
@@ -273,9 +294,6 @@ export async function runApp(session, transcriptRoot, environment = {}) {
273
294
  invalidate: () => paint.invalidate(),
274
295
  transcriptChanged: (block) => transcript.invalidate(block),
275
296
  });
276
- const resumeAtLaunch = session.resume === undefined
277
- ? undefined
278
- : track(openResumedSession(session.resume));
279
297
  async function openResumedSession(launch) {
280
298
  while (live) {
281
299
  const index = await new Promise((resolve) => {
@@ -314,6 +332,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
314
332
  shutdownSignal?.addEventListener("abort", onShutdown, { once: true });
315
333
  stopShutdown = () => shutdownSignal?.removeEventListener("abort", onShutdown);
316
334
  terminal.enter(session.config.reducedMotion);
335
+ stopOutput = paint.onReady?.(() => guard(() => render())) ?? (() => { });
317
336
  stopResize = terminal.onResize(() => guard(() => {
318
337
  paint.invalidate();
319
338
  render();
@@ -339,6 +358,9 @@ export async function runApp(session, transcriptRoot, environment = {}) {
339
358
  }), ESCAPE_MS);
340
359
  render();
341
360
  }));
361
+ const resumeAtLaunch = session.resume === undefined
362
+ ? undefined
363
+ : track(openResumedSession(session.resume));
342
364
  draw();
343
365
  if (resumeAtLaunch !== undefined)
344
366
  await Promise.race([resumeAtLaunch, done]);
@@ -347,10 +369,15 @@ export async function runApp(session, transcriptRoot, environment = {}) {
347
369
  if (failure !== undefined)
348
370
  throw failure.error;
349
371
  }
372
+ catch (error) {
373
+ // Startup failures must settle pending menus before teardown waits for work.
374
+ fail(error);
375
+ throw error;
376
+ }
350
377
  finally {
351
378
  try {
352
379
  quit();
353
- await activeWorkflow;
380
+ await Promise.allSettled([...activeWorkflows]);
354
381
  }
355
382
  finally {
356
383
  await session.persistence?.close();
@@ -16,10 +16,9 @@ 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: "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." },
19
+ { label: "Yes, once", hint: "y", key: "y" },
20
+ { label: `Yes, ${scopeNoun(scope)} for the session`, hint: "a", key: "a" },
21
+ { label: "No, and say why", hint: "n", key: "n" },
23
22
  ];
24
23
  return {
25
24
  title: [{ text: question(call.name), fg: pal.ink.attention, bold: true }],
@@ -2,24 +2,22 @@
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";
6
5
  export function render(block, width, pal, context = {}) {
7
- const inner = modelTranscriptWidth(width);
8
6
  switch (block.kind) {
9
7
  case "user":
10
8
  return renderUser(block, width, pal);
11
9
  case "answer":
12
- return insetTranscript(renderAnswer(block, inner, pal));
10
+ return renderAnswer(block, width, pal);
13
11
  case "reasoning":
14
- return insetTranscript(renderReasoning(block, inner, pal, {
12
+ return renderReasoning(block, width, pal, {
15
13
  continues: context.previous?.kind === "reasoning",
16
- }));
14
+ });
17
15
  case "tool":
18
- return insetTranscript(renderTool(block, inner, pal, {
16
+ return renderTool(block, width, pal, {
19
17
  continues: context.previous?.kind === "reasoning",
20
18
  now: context.now,
21
19
  reducedMotion: context.reducedMotion,
22
- }));
20
+ });
23
21
  case "notice":
24
22
  return renderNotice(block, width, pal);
25
23
  }
@@ -1,5 +1,5 @@
1
1
  // Foreground slash-command interactions and their session updates.
2
- import { handleCommand } from "../commands.js";
2
+ import { commandNeedsIdle, handleCommand } from "../commands.js";
3
3
  import { compactSession } from "../context/manual.js";
4
4
  import { resetRequestIdentity } from "../request-identity.js";
5
5
  import { updateSettings } from "../settings.js";
@@ -7,13 +7,16 @@ import { selectTimeline } from "../timeline.js";
7
7
  import { saveTranscript } from "../transcript-export.js";
8
8
  import { transition } from "./activity.js";
9
9
  import { cancel as cancelOpen } from "./overlay.js";
10
- export function commandWorkflow(options, resetCompaction) {
10
+ export function commandWorkflow(options, resetCompaction, resetInput) {
11
11
  const { session, state, permissions, feedback } = options;
12
12
  const choose = (picker) => new Promise((resolve) => {
13
+ state.command?.control.signal.throwIfAborted();
13
14
  state.open = { picker, settle: resolve };
14
15
  options.render();
15
16
  });
16
17
  async function command(text) {
18
+ if (state.activity !== undefined && commandNeedsIdle(text))
19
+ return;
17
20
  const activity = options.startActivity("command", `Running ${text.split(/\s+/)[0]}`);
18
21
  if (activity === undefined)
19
22
  return;
@@ -23,6 +26,7 @@ export function commandWorkflow(options, resetCompaction) {
23
26
  emit: options.commandNotice,
24
27
  signal: activity.control.signal,
25
28
  showHelp: () => new Promise((resolve) => {
29
+ activity.control.signal.throwIfAborted();
26
30
  state.open = { help: true, settle: resolve };
27
31
  options.render();
28
32
  }),
@@ -32,6 +36,7 @@ export function commandWorkflow(options, resetCompaction) {
32
36
  options.render();
33
37
  },
34
38
  type: (field) => new Promise((resolve) => {
39
+ activity.control.signal.throwIfAborted();
35
40
  state.open = { field, settle: resolve };
36
41
  options.render();
37
42
  }),
@@ -43,17 +48,13 @@ export function commandWorkflow(options, resetCompaction) {
43
48
  await session.persistence?.reset();
44
49
  resetRequestIdentity(session);
45
50
  resetCompaction();
46
- state.blocks.splice(0);
47
51
  state.past.length = 0;
48
52
  permissions.reset();
49
- state.scroll = 0;
50
- state.follow = true;
51
- state.unseen = 0;
52
- state.lastMaxScroll = 0;
53
+ options.replaceTranscript([]);
53
54
  state.committedNodeId = 0;
54
55
  },
55
56
  permissions,
56
- exportTranscript: () => saveTranscript(options.transcriptRoot, state.blocks),
57
+ exportTranscript: () => saveTranscript(options.transcriptRoot, structuredClone(state.blocks)),
57
58
  saveSettings: async (patch) => {
58
59
  await updateSettings(patch);
59
60
  },
@@ -62,6 +63,7 @@ export function commandWorkflow(options, resetCompaction) {
62
63
  const selected = await selectTimeline(session, choose);
63
64
  if (!selected)
64
65
  return "unchanged";
66
+ resetCompaction();
65
67
  options.replaceTranscript();
66
68
  return session.conversation.activeNodeId === state.committedNodeId
67
69
  ? "unchanged"
@@ -99,6 +101,10 @@ export function commandWorkflow(options, resetCompaction) {
99
101
  }
100
102
  }
101
103
  finally {
104
+ // Access can change while a turn is running. Discard even an observation
105
+ // from an in-flight response that used the previous connection.
106
+ if (text.trim().split(/\s+/)[0] === "/providers")
107
+ resetInput();
102
108
  options.finishActivity(activity);
103
109
  }
104
110
  }
@@ -5,7 +5,7 @@ export function renderCommandMenu(commands, selected, width, pal) {
5
5
  if (commands.length === 0)
6
6
  return { rows: [], right: "" };
7
7
  const menu = renderMenu(commands.map((command, index) => ({
8
- label: `/${command.name}`, description: command.blurb, selected: selectedIndex === index,
8
+ label: `/${command.name}`, selected: selectedIndex === index,
9
9
  })), width, pal, { maxRows: MAX_ROWS + 2, visible: MAX_ROWS });
10
10
  return {
11
11
  rows: menu.rows,
@@ -1,4 +1,4 @@
1
- // Ribbon rows and a stable detail area shared by completion and selectors.
1
+ // Ribbon rows and bounded overflow recovery shared by completion and selectors.
2
2
  import { row } from "../../ui/render.js";
3
3
  import { terminalText } from "../../ui/terminal-text.js";
4
4
  import { elide, textWidth, wrapText } from "../../ui/width.js";
@@ -16,10 +16,10 @@ export function renderMenu(entries, width, pal, options) {
16
16
  const room = Math.max(0, options.maxRows);
17
17
  if (room === 0)
18
18
  return { rows: [], first: 0, last: 0 };
19
- const detailLimit = Math.min(2, room - 1);
19
+ const detailLimit = options.overflow === "truncate" ? 0 : Math.min(2, room - 1);
20
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.
21
+ // Measure every possible selection so revealing clipped identity and values
22
+ // does not move the dock when the selected row changes.
23
23
  for (const entry of entries) {
24
24
  detailRows = Math.max(detailRows, detailLines(entry, width, detailLimit).length);
25
25
  if (detailRows === detailLimit)
@@ -47,11 +47,8 @@ export function renderMenu(entries, width, pal, options) {
47
47
  function detailLines(entry, width, maxRows) {
48
48
  if (entry === undefined || maxRows <= 0)
49
49
  return [];
50
- // Complete identity and values take priority over explanatory copy.
51
50
  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;
51
+ return menuText(clippedParts(entry, width).join(" · "), room, maxRows);
55
52
  }
56
53
  export function menuWindow(length, selected, visible) {
57
54
  const count = Math.max(0, length);
@@ -68,11 +65,22 @@ export function menuText(text, width, maxRows) {
68
65
  return lines.slice(0, maxRows).map((line, index) => index === maxRows - 1 && lines.length > maxRows ? elide(line + " …", width) : line);
69
66
  }
70
67
  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(" · ");
68
+ return [entry.hint, displayedValue(entry)].filter(Boolean).join(" · ");
73
69
  }
74
70
  function summary(entry, width) {
75
- return elide(terminalText(fullSummary(entry)), Math.max(1, Math.floor(width * 0.42)));
71
+ const room = Math.max(1, Math.floor(width * 0.42));
72
+ const value = terminalText(displayedValue(entry));
73
+ const hint = terminalText(entry.hint ?? "");
74
+ if (value === "" || hint === "")
75
+ return elide(value || hint, room);
76
+ // Keep the current policy visible even when its remembered-scope hint is long.
77
+ const hintRoom = room - textWidth(value) - 3;
78
+ return hintRoom <= 0 ? elide(value, room) : `${elide(hint, hintRoom)} · ${value}`;
79
+ }
80
+ function displayedValue(entry) {
81
+ if (entry.value === undefined)
82
+ return "";
83
+ return entry.selected && entry.adjustable ? `‹ ${entry.value} ›` : entry.value;
76
84
  }
77
85
  function clippedParts(entry, width) {
78
86
  // Reserve the stepper's width for every peer, so moving selection cannot
@@ -19,7 +19,7 @@ export function renderStatus(info, pal) {
19
19
  ];
20
20
  return withUnseen([
21
21
  ...active,
22
- { text: " · esc to interrupt", fg: pal.ink.dim, optional: true },
22
+ ...(info.interruptible === false ? [] : [{ text: " · esc to interrupt", fg: pal.ink.dim, optional: true }]),
23
23
  ], info.unseen, pal);
24
24
  }
25
25
  if (info.feedback !== undefined) {
package/dist/tui/frame.js CHANGED
@@ -5,11 +5,15 @@
5
5
  // writing it: a full repaint at streaming speed flickers and floods the pipe.
6
6
  // So the painter keeps the last frame and emits the difference.
7
7
  import { CSI } from "../ui/render.js";
8
- import { CURSOR, SYNC, write } from "./screen.js";
9
- export function painter() {
8
+ import { CURSOR, SYNC, write, outputReady, onDrain } from "./screen.js";
9
+ export function painter(output = { write, ready: outputReady, onReady: onDrain }) {
10
10
  let previous = [];
11
11
  return {
12
12
  paint(rows, cursor) {
13
+ // The last accepted frame remains the diff base. The host redraws current
14
+ // state on drain, so stale intermediate frames never form another queue.
15
+ if (!output.ready())
16
+ return;
13
17
  let out = SYNC.begin + CURSOR.hide;
14
18
  const height = Math.max(rows.length, previous.length);
15
19
  for (let i = 0; i < height; i++) {
@@ -24,10 +28,11 @@ export function painter() {
24
28
  previous = rows.slice();
25
29
  // One write, so a terminal that ignores the synchronization hint still
26
30
  // gets the frame as a single arrival rather than a row at a time.
27
- write(out + SYNC.end);
31
+ output.write(out + SYNC.end);
28
32
  },
29
33
  invalidate() {
30
34
  previous = [];
31
35
  },
36
+ onReady: (handler) => output.onReady(handler),
32
37
  };
33
38
  }
package/dist/tui/help.js CHANGED
@@ -3,6 +3,7 @@ import { row } from "../ui/render.js";
3
3
  import { textWidth } from "../ui/width.js";
4
4
  const CONTROL_WIDTH = 18;
5
5
  const CONTROLS = [
6
+ { key: "/", description: "open commands, also while working" },
6
7
  { key: "up / down", description: "move through menus or history" },
7
8
  { key: "left / right", description: "move cursor or change a value" },
8
9
  { key: "ctrl+left / right", description: "move cursor by word" },
@@ -10,7 +11,7 @@ const CONTROLS = [
10
11
  { key: "enter / tab", description: "select, send/steer · complete" },
11
12
  { key: "alt+enter", description: "insert a new line" },
12
13
  { key: "esc", description: "close UI or interrupt work" },
13
- { key: "ctrl+c", description: "interrupt work or exit" },
14
+ { key: "ctrl+c", description: "cancel menu, interrupt work or exit" },
14
15
  { key: "ctrl+o", description: "expand/collapse reasoning or evidence" },
15
16
  { key: "wheel / pgup/dn", description: "scroll the transcript" },
16
17
  { key: "ctrl+l", description: "redraw the screen" },
@@ -12,7 +12,11 @@ export function layoutPicker(picker, found, width, pal, maxRows) {
12
12
  contextRoom -= target.length;
13
13
  const note = menuText(picker.description ?? picker.footer ?? "", width, Math.min(2, contextRoom));
14
14
  const prefix = titleRows + target.length + note.length + queryRows;
15
- const menu = renderMenu(found.map(({ option, index }) => ({ ...option, selected: index === picker.index })), width, pal, { maxRows: budget - prefix - controlRows, visible: picker.visible ?? 6 });
15
+ const menu = renderMenu(found.map(({ option, index }) => ({ ...option, selected: index === picker.index })), width, pal, {
16
+ maxRows: budget - prefix - controlRows,
17
+ visible: picker.visible ?? 6,
18
+ overflow: picker.overflow,
19
+ });
16
20
  const progress = found.length > menu.last - menu.first || picker.searchable
17
21
  ? `${found.length === 0 ? "0" : `${menu.first + 1}–${menu.last}`} / ${found.length}` +
18
22
  (found.length === picker.options.length ? "" : ` · ${picker.options.length} total`)
@@ -50,6 +50,13 @@ export function size() {
50
50
  export function write(text) {
51
51
  process.stdout.write(text);
52
52
  }
53
+ export function outputReady() {
54
+ return !process.stdout.writableNeedDrain;
55
+ }
56
+ export function onDrain(handler) {
57
+ process.stdout.on("drain", handler);
58
+ return () => { process.stdout.off("drain", handler); };
59
+ }
53
60
  export function enter(reducedMotion = false) {
54
61
  if (active)
55
62
  return;
@@ -14,7 +14,6 @@ export function controllerOptions(session, contextPolicy, tools = session.tools,
14
14
  contextPolicy,
15
15
  effort: session.config.effort,
16
16
  requestIdentity: requestIdentityForSession(session),
17
- maxModelRequests: session.config.maxModelRequests,
18
17
  toolContext: { root: session.config.root },
19
18
  ...(steering === undefined ? {} : { steering }),
20
19
  };
@@ -1,12 +1,5 @@
1
- // Shared transcript inset and a bounded measure for model prose and tool records.
1
+ // Content padding for user surfaces, notices, and tool evidence markers.
2
2
  const GUTTER = 2;
3
- const MODEL_MEASURE = 86;
4
- export function modelTranscriptWidth(width) {
5
- return Math.max(8, Math.min(width - GUTTER * 2, MODEL_MEASURE));
6
- }
7
- export function insetTranscript(rows) {
8
- return rows.map((line) => line === "" ? "" : `${" ".repeat(GUTTER)}${line}`);
9
- }
10
3
  export function transcriptGutter(_width) {
11
4
  return GUTTER;
12
5
  }
@@ -17,6 +17,10 @@ export function transcriptRenderer(draw = render, clock = Date.now) {
17
17
  const activeTools = new Set();
18
18
  return {
19
19
  viewport(blocks, width, height, scroll, palette, state = {}) {
20
+ if (blocks.length === 0 && layouts.some((layout) => layout.entries.length > 0)) {
21
+ layouts = [];
22
+ activeTools.clear();
23
+ }
20
24
  const layout = layoutFor(blocks, width, palette);
21
25
  state = { ...state, now: state.now ?? clock() };
22
26
  if (scroll === 0)