@giovannijecha/jecode 0.8.5 → 0.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +36 -9
  2. package/assets/tokenizers/LICENSE +21 -0
  3. package/assets/tokenizers/o200k-base.tiktoken.gz +0 -0
  4. package/dist/cli-info.js +8 -14
  5. package/dist/commands.js +7 -3
  6. package/dist/config.js +47 -53
  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/credential-commands.js +3 -3
  25. package/dist/input-boundary.js +0 -62
  26. package/dist/launch.js +13 -9
  27. package/dist/model-command.js +7 -17
  28. package/dist/oauth-result-page.js +68 -0
  29. package/dist/openai-account-command.js +14 -12
  30. package/dist/openai-account.js +7 -5
  31. package/dist/openai-oauth-callback.js +15 -54
  32. package/dist/openai-oauth-tokens.js +9 -7
  33. package/dist/openai-oauth.js +8 -6
  34. package/dist/permission-command.js +9 -24
  35. package/dist/permissions.js +10 -8
  36. package/dist/provider-commands.js +1 -33
  37. package/dist/provider-errors.js +1 -10
  38. package/dist/provider-label.js +3 -10
  39. package/dist/providers/anthropic-wire.js +1 -1
  40. package/dist/providers/anthropic.js +3 -2
  41. package/dist/providers/index.js +1 -5
  42. package/dist/providers/input-measurement.js +85 -0
  43. package/dist/providers/ollama-context.js +42 -0
  44. package/dist/providers/ollama-endpoint.js +7 -34
  45. package/dist/providers/ollama-wire.js +1 -1
  46. package/dist/providers/ollama.js +15 -147
  47. package/dist/providers/openai-codex.js +7 -4
  48. package/dist/providers/openai-stream.js +20 -8
  49. package/dist/providers/openai-summary.js +37 -0
  50. package/dist/providers/openai-wire.js +1 -1
  51. package/dist/providers/openai.js +3 -1
  52. package/dist/sessions/bucket.js +55 -0
  53. package/dist/sessions/catalog-io.js +162 -0
  54. package/dist/sessions/catalog.js +3 -1
  55. package/dist/sessions/codec-messages.js +122 -0
  56. package/dist/sessions/codec-transcript.js +93 -0
  57. package/dist/sessions/codec-values.js +52 -0
  58. package/dist/sessions/codec.js +4 -257
  59. package/dist/sessions/files.js +158 -0
  60. package/dist/sessions/load.js +90 -0
  61. package/dist/sessions/snapshot.js +33 -0
  62. package/dist/sessions/store.js +42 -461
  63. package/dist/settings.js +17 -16
  64. package/dist/start.js +36 -53
  65. package/dist/timeline.js +2 -0
  66. package/dist/tools/file-read.js +192 -0
  67. package/dist/tools/file-summary.js +9 -0
  68. package/dist/tools/{fs.js → file-write.js} +5 -193
  69. package/dist/tools/glob.js +107 -0
  70. package/dist/tools/index.js +2 -1
  71. package/dist/tools/search.js +1 -105
  72. package/dist/tui/activity.js +1 -1
  73. package/dist/tui/app-input.js +34 -17
  74. package/dist/tui/app-workflows.js +13 -361
  75. package/dist/tui/app.js +55 -28
  76. package/dist/tui/approve.js +3 -2
  77. package/dist/tui/blocks.js +1 -2
  78. package/dist/tui/command-workflow.js +112 -0
  79. package/dist/tui/components/command-menu.js +7 -10
  80. package/dist/tui/components/menu.js +82 -43
  81. package/dist/tui/components/messages.js +16 -9
  82. package/dist/tui/components/status.js +1 -1
  83. package/dist/tui/components/tool-evidence.js +107 -0
  84. package/dist/tui/components/tool-motion.js +32 -0
  85. package/dist/tui/components/tool.js +48 -202
  86. package/dist/tui/frame.js +8 -3
  87. package/dist/tui/help.js +3 -2
  88. package/dist/tui/picker-layout.js +44 -0
  89. package/dist/tui/picker.js +7 -71
  90. package/dist/tui/screen.js +7 -0
  91. package/dist/tui/session-view.js +0 -1
  92. package/dist/tui/tool-details.js +135 -0
  93. package/dist/tui/transcript-grammar.js +1 -1
  94. package/dist/tui/transcript-view.js +30 -112
  95. package/dist/tui/turn-workflow.js +235 -0
  96. package/dist/tui/turn.js +7 -140
  97. package/dist/tui/view.js +4 -4
  98. package/dist/tui/workflow-types.js +2 -0
  99. package/dist/ui/render.js +0 -4
  100. package/package.json +16 -12
  101. package/dist/batch-view.js +0 -17
  102. package/dist/batch.js +0 -267
  103. package/dist/ollama-settings-command.js +0 -74
  104. package/dist/tui/motion.js +0 -32
@@ -1,210 +1,56 @@
1
- // A compact execution trace: state and identity on one semantic rail,
2
- // bounded evidence below, and motion that never enters durable state.
3
- import { fitSegs, hasColor, plainLen, row } from "../../ui/render.js";
4
- import { graphemeCeiling, graphemeFloor } from "../../text-boundary.js";
1
+ // Target-first execution records, connected to bounded, expandable evidence.
2
+ import { plainLen, row } from "../../ui/render.js";
5
3
  import { toolDuration } from "../../duration.js";
6
- import { breathe, easeInOut, easeOut, interval, mix, TOOL_BIRTH_MS, TOOL_LEADER_MAX_MS, TOOL_ROW_ARRIVAL_MS, } from "../motion.js";
7
- import { transcriptLead } from "../transcript-grammar.js";
8
- const OUTPUT_ROWS = 8;
9
- const LIVE_OUTPUT_ROWS = 6;
10
- const DIFF_ROWS = 15;
11
- const TOOL_NAME_COLS = 12;
12
- const TOOL_COLUMNS_AT = 64;
13
- const SPINNER_MS = 80;
14
- const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
4
+ import { toolEvidence, renderDetail } from "./tool-evidence.js";
5
+ import { connector, connectorInk, runningTool, stateInk, stateMark } from "./tool-motion.js";
15
6
  export function renderTool(block, width, pal, context = {}) {
16
7
  const now = context.now ?? Date.now();
17
- const shown = visibleDetails(block);
18
- const ink = stateInk(block, pal, context, now);
19
- const nameInk = birthInk(pal.ink.bright, pal.ink.dim, context, now);
8
+ const clock = { ...context, now };
9
+ const evidence = toolEvidence(block);
10
+ const active = runningTool(block);
11
+ const elapsed = active ? toolDuration(Math.max(0, now - block.startedAt), true)
12
+ : block.durationMs === undefined ? "" : toolDuration(block.durationMs);
13
+ const mark = stateMark(block);
14
+ // The separate change summary already carries these exact counts.
15
+ const repeatedCount = evidence.counts !== "" && block.right.startsWith(`${evidence.counts} · `);
16
+ const outcome = repeatedCount ? block.right.slice(evidence.counts.length + 3) : block.right;
17
+ const result = [`${mark}${outcome === "" ? "" : ` ${outcome}`}`, elapsed].filter(Boolean).join(" · ");
18
+ const hasEvidence = evidence.details.length > 0;
19
+ const name = { text: block.name, fg: pal.ink.dim };
20
+ const right = [{ text: result, fg: stateInk(block, pal) }];
21
+ const separateStatus = plainLen([name, ...right]) + (hasEvidence ? 2 : 3) + 3 > width;
22
+ const summaryAt = separateStatus ? 3 : 2;
23
+ const detailStart = summaryAt + (evidence.summary === "" ? 0 : 1);
24
+ const length = hasEvidence ? detailStart + evidence.details.length + 1 : summaryAt;
20
25
  const left = [
21
- ...transcriptLead(width, { text: stateGlyph(block, context, now), fg: ink, bold: true }),
22
- { text: toolName(block, width), fg: nameInk, bold: true },
23
- ...(block.target === "" ? [] : [{ text: ` ${block.target}`, fg: pal.technical }]),
26
+ ...connector(hasEvidence || separateStatus ? "│ " : "└─ ", block, pal, clock, 1, length),
27
+ name,
24
28
  ];
25
- const right = resultSegments(block, pal, context, now);
26
- const leader = movingLeader(width, left, right, pal, context, now);
27
- return [
29
+ const rows = [
28
30
  ...(context.continues === true ? [] : [""]),
29
- row(width, leader === undefined ? left : [...left, leader], right),
30
- ...shown.map(({ detail, sourceIndex }) => renderDetail(detail, block.tone, width, pal, context.reducedMotion === true ? undefined : context.motion?.rowsAt[sourceIndex ?? -1], now)),
31
+ row(width, [
32
+ ...connector("┌ ", block, pal, clock, 0, length),
33
+ { text: block.target || block.name, fg: pal.technical },
34
+ ]),
35
+ separateStatus ? row(width, left)
36
+ : row(Math.min(width, plainLen([...left, ...right]) + 3), left, right),
31
37
  ];
32
- }
33
- function visibleDetails(block) {
34
- const all = block.body ?? [];
35
- if (block.expanded === true || all.length === 0) {
36
- return all.map((detail, sourceIndex) => ({ detail, sourceIndex }));
37
- }
38
- if (all.every((detail) => detail.kind === "out")) {
39
- const limit = block.tone === "pending" ? LIVE_OUTPUT_ROWS : OUTPUT_ROWS;
40
- if (all.length <= limit)
41
- return all.map((detail, sourceIndex) => ({ detail, sourceIndex }));
42
- const hidden = all.length - limit;
43
- const note = block.tone === "pending"
44
- ? `… ${all.length} lines so far`
45
- : `… ${hidden} earlier lines · ctrl+o expand`;
46
- return [
47
- { detail: { kind: "gap", text: note } },
48
- ...all.slice(-limit).map((detail, index) => ({
49
- detail,
50
- sourceIndex: all.length - limit + index,
51
- })),
52
- ];
53
- }
54
- // A compact transcript audits what changed rather than repeating unchanged
55
- // source. One budget covers writes and edits, with both ends retained.
56
- const changed = all
57
- .map((detail, sourceIndex) => ({ detail, sourceIndex }))
58
- .filter(({ detail }) => detail.kind === "add" || detail.kind === "del");
59
- if (changed.length <= DIFF_ROWS)
60
- return changed;
61
- const leading = Math.ceil(DIFF_ROWS / 2);
62
- const trailing = DIFF_ROWS - leading;
63
- const hidden = changed.length - DIFF_ROWS;
64
- return [
65
- ...changed.slice(0, leading),
66
- {
67
- detail: {
68
- kind: "gap",
69
- text: `… ${hidden} more changed ${hidden === 1 ? "line" : "lines"} · ctrl+o expand`,
70
- },
71
- },
72
- ...changed.slice(-trailing),
73
- ];
74
- }
75
- function renderDetail(detail, tone, width, pal, arrivedAt, now) {
76
- const rail = transcriptLead(width, { text: "│", fg: pal.rule });
77
- if (detail.kind === "out") {
78
- const base = tone === "fail" || failureLine(detail.text) ? pal.ink.removed : pal.ink.muted;
79
- return row(width, [
80
- ...rail,
81
- { text: detail.text === "" ? " " : detail.text, fg: arrivalInk(base, pal, arrivedAt, now) },
82
- ]);
83
- }
84
- if (detail.kind === "gap") {
85
- return row(width, [
86
- ...rail,
87
- { text: detail.text, fg: arrivalInk(pal.ink.dim, pal, arrivedAt, now), italic: true },
88
- ]);
89
- }
90
- const number = detail.kind === "add" ? detail.newLine : detail.oldLine;
91
- const sign = detail.kind === "add" ? "+" : detail.kind === "del" ? "-" : " ";
92
- const base = detail.kind === "add"
93
- ? pal.ink.added
94
- : detail.kind === "del"
95
- ? pal.ink.removed
96
- : pal.ink.dim;
97
- const fg = arrivalInk(base, pal, arrivedAt, now);
98
- const tint = detail.kind === "add"
99
- ? pal.surface.added
100
- : detail.kind === "del"
101
- ? pal.surface.removed
102
- : undefined;
103
- return row(width, [
104
- ...rail,
105
- { text: sign, fg, bold: detail.kind !== "keep" },
106
- { text: `${String(number ?? "").padStart(4)} `, fg: pal.ink.dim },
107
- ...emphasized(detail.text, detail.emphasis, fg, tint),
108
- ]);
109
- }
110
- function emphasized(text, emphasis, fg, bg) {
111
- if (emphasis === undefined || emphasis.length <= 0)
112
- return [{ text, fg }];
113
- const requestedStart = Math.max(0, Math.min(text.length, emphasis.start));
114
- const requestedEnd = Math.max(requestedStart, Math.min(text.length, requestedStart + emphasis.length));
115
- const start = graphemeFloor(text, requestedStart);
116
- const end = graphemeCeiling(text, requestedEnd);
117
- if (start === end)
118
- return [{ text, fg }];
119
- return [
120
- ...(start === 0 ? [] : [{ text: text.slice(0, start), fg }]),
121
- { text: text.slice(start, end), fg, bg, bold: true },
122
- ...(end === text.length ? [] : [{ text: text.slice(end), fg }]),
123
- ];
124
- }
125
- function toolName(block, width) {
126
- if (width < TOOL_COLUMNS_AT || block.target === "")
127
- return block.name;
128
- return block.name.padEnd(TOOL_NAME_COLS);
129
- }
130
- function stateGlyph(block, context, now) {
131
- if (block.tone === "pending") {
132
- if (block.startedAt === undefined || block.right !== "running")
133
- return "○";
134
- if (context.reducedMotion === true)
135
- return "○";
136
- return SPINNER[Math.floor(now / SPINNER_MS) % SPINNER.length] ?? "○";
137
- }
138
- if (block.tone === "fail")
139
- return hasColor() ? "●" : "×";
140
- if (block.tone === "deny")
141
- return "○";
142
- return hasColor() ? "●" : "✓";
143
- }
144
- function resultSegments(block, pal, context, now) {
145
- const status = block.right;
146
- const duration = liveDuration(block, now);
147
- if (status === "" && duration === "")
148
- return [];
149
- const ink = resultInk(block, pal, context, now);
150
- return [
151
- ...(status === "" ? [] : [{ text: status, fg: ink }]),
152
- ...(duration === "" ? [] : [{ text: `${status === "" ? "" : " · "}${duration}`, fg: pal.ink.dim }]),
153
- ];
154
- }
155
- function liveDuration(block, now) {
156
- if (block.tone === "pending" && block.startedAt !== undefined && block.right === "running") {
157
- return toolDuration(Math.max(0, now - block.startedAt), true);
158
- }
159
- return block.durationMs === undefined ? "" : toolDuration(block.durationMs);
160
- }
161
- function stateInk(block, pal, context, now) {
162
- if (block.tone === "fail")
163
- return pal.ink.removed;
164
- if (block.tone === "deny")
165
- return pal.ink.attention;
166
- if (block.tone === "pending") {
167
- if (block.startedAt === undefined || context.reducedMotion === true)
168
- return pal.ink.attention;
169
- return mix(pal.ink.attention, pal.accent, breathe(now));
170
- }
171
- return pal.ink.added;
172
- }
173
- function resultInk(block, pal, context, now) {
174
- return block.tone === "ok" ? pal.ink.muted : stateInk(block, pal, context, now);
175
- }
176
- function birthInk(final, initial, context, now) {
177
- if (context.reducedMotion === true || context.motion === undefined)
178
- return final;
179
- return mix(initial, final, easeOut(interval(now, context.motion.bornAt, TOOL_BIRTH_MS)));
180
- }
181
- function arrivalInk(base, pal, arrivedAt, now) {
182
- if (arrivedAt === undefined)
183
- return base;
184
- return mix(pal.ink.bright, base, easeOut(interval(now, arrivedAt, TOOL_ROW_ARRIVAL_MS)));
185
- }
186
- function movingLeader(width, left, right, pal, context, now) {
187
- if (context.reducedMotion === true || context.motion === undefined || right.length === 0)
188
- return undefined;
189
- const progress = interval(now, context.motion.bornAt, TOOL_LEADER_MAX_MS);
190
- if (progress >= 1)
191
- return undefined;
192
- const fittedRight = fitSegs(right, width);
193
- const gap = width - plainLen(left) - plainLen(fittedRight) - 1;
194
- if (gap < 9)
195
- return undefined;
196
- const cells = Array.from({ length: gap }, () => " ");
197
- const travelCells = gap - 2;
198
- const trail = Math.min(7, travelCells);
199
- const travel = travelCells + trail;
200
- const head = Math.floor(easeInOut(progress) * travel) - trail;
201
- for (let offset = 0; offset < trail; offset++) {
202
- const at = head + offset;
203
- if (at >= 0 && at < travelCells)
204
- cells[at + 1] = "·";
205
- }
206
- return { text: cells.join(""), fg: pal.rule };
207
- }
208
- function failureLine(line) {
209
- return line.startsWith("✖") || line.startsWith("×") || line.includes("AssertionError") || /\bfailed\b/i.test(line);
38
+ if (separateStatus)
39
+ rows.push(row(width, [
40
+ ...connector(hasEvidence ? "│ " : "└─ ", block, pal, clock, 2, length),
41
+ ...right,
42
+ ]));
43
+ if (!hasEvidence)
44
+ return rows;
45
+ if (evidence.summary !== "")
46
+ rows.push(row(width, [
47
+ ...connector("│ ", block, pal, clock, summaryAt, length),
48
+ { text: evidence.summary, fg: pal.ink.muted },
49
+ ]));
50
+ rows.push(...evidence.details.map((detail, index) => renderDetail(detail, block.tone, width, pal, block.expanded ? pal.rule : connectorInk(block, pal, clock, detailStart + index, length))));
51
+ rows.push(row(width, [
52
+ ...connector("└─", block, pal, clock, length - 1, length),
53
+ { text: evidence.note === "" ? "" : ` ${evidence.note}`, fg: pal.ink.dim },
54
+ ]));
55
+ return rows;
210
56
  }
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,8 +11,8 @@ 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+o", description: "toggle reasoning or tool details" },
14
+ { key: "ctrl+c", description: "cancel menu, interrupt work or exit" },
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" },
17
18
  ];
@@ -0,0 +1,44 @@
1
+ // Bounded Ribbon composition; filtering and selection remain in picker.ts.
2
+ import { row } from "../ui/render.js";
3
+ import { menuText, renderMenu } from "./components/menu.js";
4
+ import { promptCursor, promptLine } from "./components/prompt.js";
5
+ export function layoutPicker(picker, found, width, pal, maxRows) {
6
+ const budget = Math.max(0, maxRows);
7
+ const queryRows = picker.searchable && budget >= 2 ? 1 : 0;
8
+ const titleRows = picker.title.length > 0 && budget - queryRows >= 2 ? 1 : 0;
9
+ const controlRows = budget - queryRows - titleRows >= 2 ? 1 : 0;
10
+ let contextRoom = Math.max(0, budget - queryRows - titleRows - controlRows - 1);
11
+ const target = menuText(picker.right ?? "", width, Math.min(2, contextRoom));
12
+ contextRoom -= target.length;
13
+ const note = menuText(picker.description ?? picker.footer ?? "", width, Math.min(2, contextRoom));
14
+ const prefix = titleRows + target.length + note.length + queryRows;
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
+ });
20
+ const progress = found.length > menu.last - menu.first || picker.searchable
21
+ ? `${found.length === 0 ? "0" : `${menu.first + 1}–${menu.last}`} / ${found.length}` +
22
+ (found.length === picker.options.length ? "" : ` · ${picker.options.length} total`)
23
+ : "";
24
+ const query = picker.query ?? "";
25
+ const cursor = queryRows === 0 ? undefined : {
26
+ row: prefix - 1,
27
+ col: promptCursor(query, query.length, width, { right: progress }).col,
28
+ };
29
+ if (pal === undefined)
30
+ return { rows: [], cursor };
31
+ const controls = picker.controls ?? (picker.adjust
32
+ ? "↑↓ move · ←→ change · esc close" : "↑↓ move · enter select · esc close");
33
+ return {
34
+ rows: [
35
+ ...(titleRows === 0 ? [] : [row(width, picker.title, queryRows > 0 || progress === "" ? [] : [{ text: progress, fg: pal.ink.dim }])]),
36
+ ...[...target, ...note].map((text) => row(width, [{ text, fg: pal.ink.muted }])),
37
+ ...(queryRows === 0 ? [] : [promptLine(query, query.length, width, pal, {
38
+ placeholder: "type to filter", right: progress,
39
+ }).row]),
40
+ ...menu.rows,
41
+ ...(controlRows === 0 ? [] : [row(width, [{ text: controls, fg: pal.ink.dim }])]),
42
+ ], cursor,
43
+ };
44
+ }
@@ -1,9 +1,7 @@
1
1
  // One interaction model for every terminal selector.
2
- import { row } from "../ui/render.js";
3
- import { elide, graphemes } from "../ui/width.js";
2
+ import { graphemes } from "../ui/width.js";
4
3
  import { assertPromptAppend } from "../input-boundary.js";
5
- import { menuWindow, renderMenuRows } from "./components/menu.js";
6
- import { promptCursor, promptLine } from "./components/prompt.js";
4
+ import { layoutPicker } from "./picker-layout.js";
7
5
  const WINDOW = 6;
8
6
  export function move(picker, step) {
9
7
  const shown = matches(picker);
@@ -53,74 +51,12 @@ export function byKey(picker, text) {
53
51
  const found = picker.options.findIndex((option) => option.key === typed);
54
52
  return found === -1 ? undefined : found;
55
53
  }
56
- export function panel(picker, width, pal, maxRows = (picker.visible ?? WINDOW) + 3) {
57
- return layout(picker, width, pal, maxRows).rows;
54
+ export function panel(picker, width, pal, maxRows = (picker.visible ?? WINDOW) + 6) {
55
+ return layoutPicker(picker, matches(picker), width, pal, maxRows).rows;
58
56
  }
59
57
  /** Caret for the shared query row, relative to the unframed picker body. */
60
- export function caret(picker, width, maxRows = (picker.visible ?? WINDOW) + 3) {
61
- return layout(picker, width, undefined, maxRows).cursor;
62
- }
63
- function layout(picker, width, pal, maxRows) {
64
- const found = matches(picker);
65
- const note = picker.description ?? picker.footer;
66
- const hasTitle = picker.title.length > 0;
67
- const fixed = (hasTitle ? 1 : 0) +
68
- (note === undefined ? 0 : 1) +
69
- (picker.searchable === true ? 1 : 0);
70
- const optionRoom = Math.max(1, Math.min(picker.visible ?? WINDOW, maxRows - fixed));
71
- const selectedAt = Math.max(0, found.findIndex((entry) => entry.index === picker.index));
72
- const { first, last } = menuWindow(found.length, selectedAt, optionRoom);
73
- const shown = found.slice(first, last);
74
- const colors = pal;
75
- const options = colors === undefined
76
- ? []
77
- : shown.length === 0
78
- ? [row(width, [{ text: "no matches", fg: colors.ink.muted }])]
79
- : renderMenuRows(shown.map(({ option, index }) => ({
80
- label: option.label,
81
- description: option.description,
82
- hint: option.hint,
83
- value: option.value,
84
- adjustable: option.adjustable,
85
- selected: index === picker.index,
86
- })), width, colors);
87
- const progress = found.length > shown.length || picker.searchable === true
88
- ? `${found.length === 0 ? "0" : `${first + 1}–${first + shown.length}`} / ${found.length}` +
89
- (found.length === picker.options.length ? "" : ` · ${picker.options.length} total`)
90
- : "";
91
- const titleRight = hasTitle
92
- ? picker.right ?? (picker.searchable === true ? undefined : progress)
93
- : undefined;
94
- const visibleTitleRight = titleRight === undefined
95
- ? undefined
96
- : elide(titleRight, Math.max(1, Math.floor(width * 0.55)));
97
- const queryRow = picker.searchable === true && colors !== undefined
98
- ? promptLine(picker.query ?? "", (picker.query ?? "").length, width, colors, {
99
- placeholder: "type to filter",
100
- right: progress,
101
- })
102
- : undefined;
103
- const queryCursor = picker.searchable === true
104
- ? promptCursor(picker.query ?? "", (picker.query ?? "").length, width, { right: progress })
105
- : undefined;
106
- const queryOffset = (hasTitle ? 1 : 0) + (note === undefined ? 0 : 1);
107
- return {
108
- rows: colors === undefined
109
- ? []
110
- : [
111
- ...(hasTitle
112
- ? [row(width, picker.title, visibleTitleRight === undefined || visibleTitleRight === ""
113
- ? []
114
- : [{ text: visibleTitleRight, fg: colors.ink.dim }])]
115
- : []),
116
- ...(note === undefined ? [] : [row(width, [{ text: note, fg: colors.ink.muted }])]),
117
- ...(queryRow === undefined ? [] : [queryRow.row]),
118
- ...options,
119
- ],
120
- cursor: picker.searchable === true
121
- ? { row: queryOffset, col: queryCursor?.col ?? 0 }
122
- : undefined,
123
- };
58
+ export function caret(picker, width, maxRows = (picker.visible ?? WINDOW) + 6) {
59
+ return layoutPicker(picker, matches(picker), width, undefined, maxRows).cursor;
124
60
  }
125
61
  export function heading(label, about, pal) {
126
62
  return [
@@ -128,7 +64,7 @@ export function heading(label, about, pal) {
128
64
  { text: about, fg: pal.ink.fg },
129
65
  ];
130
66
  }
131
- function matches(picker) {
67
+ export function matches(picker) {
132
68
  const query = (picker.query ?? "").trim().toLocaleLowerCase();
133
69
  return picker.options.flatMap((option, index) => {
134
70
  const haystack = `${option.label} ${option.hint ?? ""} ${option.value ?? ""}`.toLocaleLowerCase();
@@ -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
  };
@@ -0,0 +1,135 @@
1
+ // Semantic tool evidence built from arguments, previews, and results.
2
+ import { graphemes } from "../text-boundary.js";
3
+ import { condense, diff } from "../ui/diff.js";
4
+ /** Unchanged rows kept either side of a change. */
5
+ const CONTEXT = 2;
6
+ /** The argument worth showing: the thing the call acts on. */
7
+ export function toolTarget(input) {
8
+ const { path, command } = input;
9
+ if (typeof command === "string")
10
+ return command;
11
+ if (typeof path === "string")
12
+ return path;
13
+ const rest = JSON.stringify(input);
14
+ return rest === "{}" ? "" : rest;
15
+ }
16
+ /**
17
+ * What the call is about to do, drawn before it is allowed to do it.
18
+ *
19
+ * The tool is asked first, because only it knows what is already on disk: a
20
+ * write against an existing file is a replacement, and showing it as a page of
21
+ * additions hides exactly the part worth approving. The fallback diffs what is
22
+ * in the arguments, which is all there is when a tool has nothing to say.
23
+ */
24
+ export function previewDetails(call, look) {
25
+ if (look !== undefined)
26
+ return changes(look.before, look.after);
27
+ const input = call.input;
28
+ if (call.name === "write_file" && typeof input.content === "string") {
29
+ return changes("", input.content);
30
+ }
31
+ if (call.name === "edit_file" && typeof input.old_text === "string") {
32
+ return changes(input.old_text, typeof input.new_text === "string" ? input.new_text : "");
33
+ }
34
+ return undefined;
35
+ }
36
+ /** Two texts as the rows of their difference, unchanged runs summed up. */
37
+ function changes(before, after) {
38
+ let oldLine = 1;
39
+ let newLine = 1;
40
+ const rows = [];
41
+ for (const changed of condense(diff(before, after), CONTEXT)) {
42
+ if (changed.kind === "gap") {
43
+ rows.push({ kind: "gap", text: `… ${changed.skipped} unchanged` });
44
+ oldLine += changed.skipped;
45
+ newLine += changed.skipped;
46
+ continue;
47
+ }
48
+ if (changed.kind === "keep") {
49
+ rows.push({ kind: "keep", text: tabs(changed.text), oldLine, newLine });
50
+ oldLine++;
51
+ newLine++;
52
+ continue;
53
+ }
54
+ if (changed.kind === "del") {
55
+ rows.push({ kind: "del", text: tabs(changed.text), oldLine });
56
+ oldLine++;
57
+ continue;
58
+ }
59
+ rows.push({ kind: "add", text: tabs(changed.text), newLine });
60
+ newLine++;
61
+ }
62
+ emphasizePairs(rows);
63
+ return rows.length === 0 ? undefined : rows;
64
+ }
65
+ function emphasizePairs(rows) {
66
+ for (let index = 0; index < rows.length - 1; index++) {
67
+ const removed = rows[index];
68
+ const added = rows[index + 1];
69
+ if (removed?.kind !== "del" || added?.kind !== "add")
70
+ continue;
71
+ if (rows[index - 1]?.kind === "del" || rows[index + 2]?.kind === "add")
72
+ continue;
73
+ const removedClusters = graphemes(removed.text);
74
+ const addedClusters = graphemes(added.text);
75
+ let prefix = 0;
76
+ let start = 0;
77
+ while (prefix < removedClusters.length &&
78
+ prefix < addedClusters.length &&
79
+ removedClusters[prefix] === addedClusters[prefix]) {
80
+ start += removedClusters[prefix].length;
81
+ prefix++;
82
+ }
83
+ let suffix = 0;
84
+ let removedSuffix = 0;
85
+ let addedSuffix = 0;
86
+ while (suffix < removedClusters.length - prefix &&
87
+ suffix < addedClusters.length - prefix &&
88
+ removedClusters[removedClusters.length - 1 - suffix] ===
89
+ addedClusters[addedClusters.length - 1 - suffix]) {
90
+ removedSuffix += removedClusters[removedClusters.length - 1 - suffix].length;
91
+ addedSuffix += addedClusters[addedClusters.length - 1 - suffix].length;
92
+ suffix++;
93
+ }
94
+ while (suffix > 0 &&
95
+ (!wordBoundary(removed.text, removedSuffix) || !wordBoundary(added.text, addedSuffix))) {
96
+ removedSuffix -= removedClusters[removedClusters.length - suffix].length;
97
+ addedSuffix -= addedClusters[addedClusters.length - suffix].length;
98
+ suffix--;
99
+ }
100
+ const removedLength = removed.text.length - start - removedSuffix;
101
+ const addedLength = added.text.length - start - addedSuffix;
102
+ if (removedLength > 0)
103
+ removed.emphasis = { start, length: removedLength };
104
+ if (addedLength > 0)
105
+ added.emphasis = { start, length: addedLength };
106
+ }
107
+ }
108
+ function wordBoundary(text, suffix) {
109
+ const at = text.length - suffix;
110
+ if (at <= 0 || at >= text.length)
111
+ return true;
112
+ return /\w/.test(text[at - 1]) !== /\w/.test(text[at]);
113
+ }
114
+ /** What the call left behind, for the calls whose output is worth a look. */
115
+ export function producedDetails(call, output) {
116
+ if (call.name === "run_command")
117
+ return outputDetails(output, "out");
118
+ // A read or a listing is already summed up on the right of its own row, and
119
+ // the model is about to say what was in it. Printing it twice helps nobody.
120
+ return undefined;
121
+ }
122
+ export function outputDetails(text, kind) {
123
+ const rows = split(text).map((line) => ({ kind, text: tabs(line) }));
124
+ return rows.length === 0 ? undefined : rows;
125
+ }
126
+ function split(text) {
127
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
128
+ while (lines.length > 0 && lines[lines.length - 1] === "")
129
+ lines.pop();
130
+ return lines.map(tabs);
131
+ }
132
+ // A tab is a width the terminal decides and the row measurement cannot see.
133
+ function tabs(line) {
134
+ return line.replace(/\t/g, " ");
135
+ }
@@ -1,4 +1,4 @@
1
- // One semantic margin shared by the user cue and executable tool evidence.
1
+ // Content padding for user surfaces, notices, and tool evidence markers.
2
2
  const GUTTER = 2;
3
3
  export function transcriptGutter(_width) {
4
4
  return GUTTER;