@code-yeongyu/senpi-codemode 2026.10.10-9 → 2026.10.11

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 (49) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/package.json +4 -4
  3. package/src/bridge/memory-protocol.ts +8 -1
  4. package/src/bridge/protocol.ts +1 -1
  5. package/src/bridges/eval-environment-schemas.ts +5 -1
  6. package/src/environments/editable-check.ts +1 -1
  7. package/src/extension/session-cwd.ts +25 -9
  8. package/src/extension/session-manager.ts +4 -4
  9. package/src/kernels/jl/globals.jl +128 -0
  10. package/src/kernels/jl/prelude.jl +15 -2
  11. package/src/kernels/jl/runner.jl +35 -143
  12. package/src/kernels/js/group-signal-notice.js +117 -0
  13. package/src/kernels/js/inline-worker.ts +2 -0
  14. package/src/kernels/js/local-module-loader.ts +8 -0
  15. package/src/kernels/js/process-tree-host.ts +4 -8
  16. package/src/kernels/js/process-tree.d.ts +11 -0
  17. package/src/kernels/js/process-tree.js +55 -10
  18. package/src/kernels/js/windows-tree-kill-host.ts +117 -0
  19. package/src/kernels/js/windows-tree-kill.d.ts +19 -0
  20. package/src/kernels/js/windows-tree-kill.js +100 -0
  21. package/src/kernels/js/worker-cwd.js +41 -10
  22. package/src/kernels/js/worker-runtime.js +23 -4
  23. package/src/kernels/js/worker-shell-capture.js +4 -0
  24. package/src/kernels/js/worker-slot.ts +2 -3
  25. package/src/kernels/py/prelude.py +36 -25
  26. package/src/kernels/py/transport.ts +4 -0
  27. package/src/kernels/rb/runner.rb +9 -19
  28. package/src/kernels/sandbox/vendor/pi-codemode/runtime/host.ts +2 -2
  29. package/src/kernels/shared/bun-ffi.ts +24 -0
  30. package/src/kernels/shared/kernel-memory-host.ts +30 -7
  31. package/src/kernels/shared/process-cpu.ts +7 -3
  32. package/src/kernels/shared/process-group-cpu.ts +6 -3
  33. package/src/kernels/shared/subprocess-kernel.ts +29 -3
  34. package/src/kernels/shared/subprocess-memory-globals.ts +101 -0
  35. package/src/kernels/shared/subprocess-process.ts +20 -12
  36. package/src/kernels/shared/subprocess-queue.ts +1 -0
  37. package/src/tool/eval-tool.ts +2 -1
  38. package/src/tool/render-agent.ts +110 -0
  39. package/src/tool/render-blocks.ts +252 -0
  40. package/src/tool/render-call.ts +130 -0
  41. package/src/tool/render-cell.ts +165 -0
  42. package/src/tool/render-detail.ts +152 -0
  43. package/src/tool/render-live.ts +288 -0
  44. package/src/tool/render-result.ts +202 -0
  45. package/src/tool/render-status.ts +166 -0
  46. package/src/tool/render-tail.ts +139 -0
  47. package/src/tool/render.ts +3 -1128
  48. package/src/tool/run-eval-cell.ts +7 -12
  49. package/src/tool/types.ts +11 -2
@@ -0,0 +1,165 @@
1
+ import { highlightedCode } from "./code-preview.ts";
2
+ import { leadsWithHeadline } from "./live-headline.ts";
3
+ import { renderAgentProgressEvents } from "./render-agent.ts";
4
+ import {
5
+ appendLines,
6
+ assertNever,
7
+ CODE_PREVIEW_LINES,
8
+ OUTPUT_PREVIEW_LINES,
9
+ previewText,
10
+ type RenderBlock,
11
+ type RenderEnvironment,
12
+ renderAllVisualLines,
13
+ renderPrefixed,
14
+ SUMMARY_PREVIEW_LINES,
15
+ style,
16
+ } from "./render-blocks.ts";
17
+ import {
18
+ type CellBadges,
19
+ cellElapsedMs,
20
+ cellHeader,
21
+ cellPresentation,
22
+ FRAME_HEADER_PREFIX,
23
+ FRAME_INNER_PREFIX,
24
+ headlined,
25
+ LIVE_LINE_PREFIX,
26
+ renderLiveCellFrame,
27
+ } from "./render-live.ts";
28
+ import { formatThroughputBadge } from "./render-status.ts";
29
+ import { cellOutputSection, cellStatusSection } from "./render-tail.ts";
30
+ import { formatRuntimeBadge } from "./runtime-label.ts";
31
+ import { formatDuration } from "./tool-widgets.ts";
32
+ import type { EvalCellResult } from "./types.ts";
33
+
34
+ export type { CellBadges } from "./render-live.ts";
35
+ export { LIVE_CODE_WINDOW_LINES } from "./render-live.ts";
36
+
37
+ // A summary has no length limit, so a collapsed block shows its first lines and marks the cut.
38
+ function summaryVisualLines(summary: string, width: number, expanded: boolean): string[] {
39
+ const lines = renderAllVisualLines(summary, width);
40
+ if (expanded || lines.length <= SUMMARY_PREVIEW_LINES) return lines;
41
+ const kept = renderAllVisualLines(summary, Math.max(1, width - 1)).slice(0, SUMMARY_PREVIEW_LINES);
42
+ kept[SUMMARY_PREVIEW_LINES - 1] = `${kept[SUMMARY_PREVIEW_LINES - 1] ?? ""}…`;
43
+ return kept;
44
+ }
45
+
46
+ export function summaryBlock(summary: string, theme: RenderEnvironment["theme"], expanded: boolean): RenderBlock {
47
+ return {
48
+ kind: "dynamic",
49
+ render: (width) => summaryVisualLines(summary, width, expanded).map((line) => style(theme, "muted", line)),
50
+ };
51
+ }
52
+
53
+ export function renderCell(cell: EvalCellResult, environment: RenderEnvironment, badges: CellBadges): string[] {
54
+ const agentEvents = (cell.statusEvents ?? []).filter((event) => event.op === "agent");
55
+ const lines = renderCellFrame(cell, environment, badges);
56
+ if (agentEvents.length > 0) appendLines(lines, renderAgentProgressEvents(agentEvents, environment));
57
+ return lines;
58
+ }
59
+
60
+ function renderCellFrame(cell: EvalCellResult, environment: RenderEnvironment, badges: CellBadges): string[] {
61
+ switch (cell.status) {
62
+ case "pending":
63
+ case "queued":
64
+ case "running":
65
+ return environment.expanded
66
+ ? renderExpandedCellFrame(cell, environment, badges)
67
+ : renderLiveCellFrame(cell, environment, badges);
68
+ case "complete":
69
+ case "error":
70
+ case "cancelled":
71
+ return environment.expanded
72
+ ? renderExpandedCellFrame(cell, environment, badges)
73
+ : renderTerminalCellLine(cell, environment, badges);
74
+ case "detached":
75
+ return environment.expanded
76
+ ? renderExpandedCellFrame(cell, environment, badges)
77
+ : renderDetachedCellFrame(cell, environment, badges);
78
+ default:
79
+ return assertNever(cell.status);
80
+ }
81
+ }
82
+
83
+ // A terminal row collapses to one line: icon, the cell's headline (summary or first code line),
84
+ // status and duration. The full frame stays reachable through the expand action.
85
+ function renderTerminalCellLine(cell: EvalCellResult, environment: RenderEnvironment, badges: CellBadges): string[] {
86
+ const presentation = cellPresentation(cell.status, environment.spinnerFrame);
87
+ const runtimeBadge = cell.runtime === undefined ? "" : ` (${formatRuntimeBadge(cell.language, cell.runtime)})`;
88
+ // The runtime badge rides the base label when it fits (review MEDIUM-1); when it would wrap
89
+ // the row it drops like any badge, so the done row stays one line at any width.
90
+ const segments: string[] = [];
91
+ if (badges.throughput !== undefined) {
92
+ const badge = formatThroughputBadge(badges.throughput);
93
+ if (badge !== undefined) segments.push(badge);
94
+ }
95
+ const elapsedMs = badges.throughput?.wallDurationMs ?? cellElapsedMs(cell, environment);
96
+ if (elapsedMs !== undefined) segments.push(formatDuration(elapsedMs));
97
+ if (badges.reset) segments.push("reset");
98
+ if (badges.timeout !== undefined) segments.push(`timeout ${badges.timeout}s`);
99
+ const tail = segments.length === 0 ? "" : ` · ${segments.join(" · ")}`;
100
+ const rest = `eval ${cell.language}${runtimeBadge} ${presentation.label}${tail}`;
101
+ const badgelessRest = runtimeBadge === "" ? rest : `eval ${cell.language} ${presentation.label}${tail}`;
102
+ const line = style(
103
+ environment.theme,
104
+ presentation.color,
105
+ headlined(presentation.icon, cell.summary, cell.code, rest, environment, { badgelessRest }),
106
+ );
107
+ return renderPrefixed(line, environment, LIVE_LINE_PREFIX);
108
+ }
109
+
110
+ // A detached row keeps its collapsed frame: the cell is no longer live, but its output and status
111
+ // history are the reason it is inspected, so they stay visible without expand.
112
+ function renderDetachedCellFrame(cell: EvalCellResult, environment: RenderEnvironment, badges: CellBadges): string[] {
113
+ if (cell.output.trim().length === 0) {
114
+ const statusEvents = (cell.statusEvents ?? []).filter((event) => event.op !== "agent");
115
+ if (statusEvents.length === 0)
116
+ return renderPrefixed(cellHeader(cell, environment, badges), environment, LIVE_LINE_PREFIX);
117
+ }
118
+ return renderExpandedCellFrame(cell, environment, badges, { collapsedPreviews: true });
119
+ }
120
+
121
+ function renderExpandedCellFrame(
122
+ cell: EvalCellResult,
123
+ environment: RenderEnvironment,
124
+ badges: CellBadges,
125
+ options: { readonly collapsedPreviews?: boolean } = {},
126
+ ): string[] {
127
+ const previewsCollapsed = options.collapsedPreviews === true && !environment.expanded;
128
+ const lines = renderPrefixed(cellHeader(cell, environment, badges), environment, FRAME_HEADER_PREFIX);
129
+ if (cell.summary !== undefined && !leadsWithHeadline(cell.status)) {
130
+ appendLines(
131
+ lines,
132
+ summaryVisualLines(cell.summary, Math.max(1, environment.width - 2), environment.expanded).map(
133
+ (line) => `${style(environment.theme, "muted", "│ ")}${style(environment.theme, "muted", line)}`,
134
+ ),
135
+ );
136
+ }
137
+ const innerWidth = Math.max(1, environment.width - 2);
138
+ const codePreview = previewText(
139
+ highlightedCodeForFrame(cell, environment),
140
+ previewsCollapsed ? CODE_PREVIEW_LINES : Number.POSITIVE_INFINITY,
141
+ innerWidth,
142
+ );
143
+ if (codePreview.skipped > 0) {
144
+ appendLines(
145
+ lines,
146
+ renderPrefixed(`${codePreview.skipped} earlier code lines`, environment, {
147
+ prefix: "│ ",
148
+ continuation: "│ ",
149
+ color: "muted",
150
+ }),
151
+ );
152
+ }
153
+ for (const line of codePreview.lines) appendLines(lines, renderPrefixed(line, environment, FRAME_INNER_PREFIX));
154
+ appendLines(
155
+ lines,
156
+ cellOutputSection(cell, environment, previewsCollapsed ? OUTPUT_PREVIEW_LINES : Number.POSITIVE_INFINITY),
157
+ );
158
+ appendLines(lines, cellStatusSection(cell, environment));
159
+ lines.push(style(environment.theme, "borderMuted", "╰─"));
160
+ return lines;
161
+ }
162
+
163
+ function highlightedCodeForFrame(cell: EvalCellResult, environment: RenderEnvironment): string {
164
+ return highlightedCode(cell.code, cell.language, environment.theme, environment.repaint);
165
+ }
@@ -0,0 +1,152 @@
1
+ import { type AgentToolResult, sanitizeTerminalLabel, type Theme } from "@code-yeongyu/senpi";
2
+ import {
3
+ JSON_TREE_MAX_DEPTH_COLLAPSED,
4
+ JSON_TREE_MAX_DEPTH_EXPANDED,
5
+ JSON_TREE_MAX_LINES_COLLAPSED,
6
+ JSON_TREE_MAX_LINES_EXPANDED,
7
+ JSON_TREE_SCALAR_LEN_COLLAPSED,
8
+ JSON_TREE_SCALAR_LEN_EXPANDED,
9
+ renderJsonTreeLines,
10
+ } from "./json-tree.ts";
11
+ import {
12
+ appendLines,
13
+ isEvalRunInput,
14
+ type RenderBlock,
15
+ type RenderEnvironment,
16
+ renderAllVisualLines,
17
+ renderToolCall,
18
+ style,
19
+ TOOL_CALL_PREVIEW_COUNT,
20
+ } from "./render-blocks.ts";
21
+ import { renderCell } from "./render-cell.ts";
22
+ import { renderToolCallWidget } from "./tool-widgets.ts";
23
+ import type { EvalResultDetails, EvalToolDetails, EvalToolRequest } from "./types.ts";
24
+
25
+ type ToolCallRow = {
26
+ readonly summary: string;
27
+ readonly error?: string;
28
+ readonly color: "success" | "error";
29
+ };
30
+ type DetailedRenderContext = {
31
+ readonly environment: RenderEnvironment;
32
+ readonly args: EvalToolRequest;
33
+ readonly showImageFallback: boolean;
34
+ readonly isFinal: boolean;
35
+ };
36
+
37
+ export function renderJsonOutputs(values: readonly unknown[], environment: RenderEnvironment): string[] {
38
+ const lines: string[] = [];
39
+ const depth = environment.expanded ? JSON_TREE_MAX_DEPTH_EXPANDED : JSON_TREE_MAX_DEPTH_COLLAPSED;
40
+ const lineCap = environment.expanded ? JSON_TREE_MAX_LINES_EXPANDED : JSON_TREE_MAX_LINES_COLLAPSED;
41
+ const scalarLen = environment.expanded ? JSON_TREE_SCALAR_LEN_EXPANDED : JSON_TREE_SCALAR_LEN_COLLAPSED;
42
+ for (const [index, value] of values.entries()) {
43
+ appendLines(
44
+ lines,
45
+ renderAllVisualLines(style(environment.theme, "dim", `display[${index + 1}]`), environment.width),
46
+ );
47
+ const tree = renderJsonTreeLines(value, environment.theme, depth, lineCap, scalarLen);
48
+ for (const line of tree.lines) appendLines(lines, renderAllVisualLines(line, environment.width));
49
+ if (tree.truncated)
50
+ appendLines(lines, renderAllVisualLines(style(environment.theme, "dim", "…"), environment.width));
51
+ }
52
+ return lines;
53
+ }
54
+
55
+ export function renderDetailedLines(
56
+ details: EvalToolDetails,
57
+ result: AgentToolResult<EvalResultDetails>,
58
+ context: DetailedRenderContext,
59
+ ): string[] {
60
+ const lines: string[] = [];
61
+ const cells = details.cells ?? [];
62
+ for (const [index, cell] of cells.entries()) {
63
+ const run = isEvalRunInput(context.args) ? context.args : undefined;
64
+ const throughput =
65
+ context.isFinal &&
66
+ cells.length === 1 &&
67
+ cell.status === "complete" &&
68
+ typeof details.toolCallCount === "number"
69
+ ? { calls: details.toolCallCount, wallDurationMs: details.wallDurationMs }
70
+ : undefined;
71
+ const badges = {
72
+ reset: index === 0 && run?.reset === true,
73
+ timeout: index === 0 ? run?.timeout : undefined,
74
+ throughput,
75
+ };
76
+ appendLines(lines, renderCell(cell, context.environment, badges));
77
+ if (index < cells.length - 1) lines.push("");
78
+ }
79
+ const jsonOutputs = details.jsonOutputs ?? [];
80
+ if (jsonOutputs.length > 0) {
81
+ if (lines.length > 0) lines.push("");
82
+ appendLines(lines, renderJsonOutputs(jsonOutputs, context.environment));
83
+ }
84
+ if (context.showImageFallback) {
85
+ for (const part of result.content) {
86
+ if (part.type !== "image") continue;
87
+ if (lines.length > 0) lines.push("");
88
+ appendLines(
89
+ lines,
90
+ renderAllVisualLines(`[image: ${sanitizeTerminalLabel(part.mimeType)}]`, context.environment.width),
91
+ );
92
+ }
93
+ }
94
+ if (details.phase !== undefined)
95
+ appendLines(
96
+ lines,
97
+ renderAllVisualLines(
98
+ style(context.environment.theme, "muted", `phase ${details.phase}`),
99
+ context.environment.width,
100
+ ),
101
+ );
102
+ return lines;
103
+ }
104
+
105
+ export function toolCallRows(details: EvalToolDetails | undefined): ToolCallRow[] {
106
+ if (!details?.toolCalls || details.toolCalls.length === 0) return [];
107
+ return details.toolCalls.map((call) => {
108
+ const status = call.ok ? "ok" : "error";
109
+ const row = { summary: `- tool.${call.name}: ${status}`, color: call.ok ? "success" : "error" } as const;
110
+ return call.error === undefined ? row : { ...row, error: call.error };
111
+ });
112
+ }
113
+
114
+ export function nestedToolCallBlock(
115
+ details: EvalToolDetails | undefined,
116
+ theme: Theme | undefined,
117
+ cwd: string,
118
+ expanded: boolean,
119
+ ): RenderBlock | undefined {
120
+ const toolCalls = details?.toolCalls;
121
+ if (theme === undefined || toolCalls === undefined || !toolCalls.some((call) => call.args !== undefined)) {
122
+ return undefined;
123
+ }
124
+ const legacyRows = toolCallRows(details);
125
+ return {
126
+ kind: "dynamic",
127
+ render: (width) => {
128
+ const retainedCalls = expanded ? toolCalls : toolCalls.slice(-TOOL_CALL_PREVIEW_COUNT);
129
+ const retainedRows = expanded ? legacyRows : legacyRows.slice(-TOOL_CALL_PREVIEW_COUNT);
130
+ const skippedCount = toolCalls.length - retainedCalls.length;
131
+ const toolCallNoun = skippedCount === 1 ? "call" : "calls";
132
+ const lines =
133
+ expanded || skippedCount === 0
134
+ ? []
135
+ : renderAllVisualLines(style(theme, "muted", `${skippedCount} earlier tool ${toolCallNoun}`), width);
136
+ const legacyBlock: Extract<RenderBlock, { kind: "toolCalls" }> = {
137
+ kind: "toolCalls",
138
+ calls: retainedRows,
139
+ expanded,
140
+ theme,
141
+ };
142
+ for (const [index, call] of retainedCalls.entries()) {
143
+ if (call.args !== undefined) {
144
+ appendLines(lines, renderToolCallWidget(call, { cwd, theme, expanded, width }));
145
+ } else {
146
+ appendLines(lines, renderToolCall(retainedRows[index], legacyBlock, width));
147
+ }
148
+ }
149
+ return lines;
150
+ },
151
+ };
152
+ }
@@ -0,0 +1,288 @@
1
+ import { sanitizeTerminalLabel, truncateToVisualLines, visibleWidth } from "@code-yeongyu/senpi";
2
+ import { highlightedCode } from "./code-preview.ts";
3
+ import { leadsWithHeadline, liveHeadline } from "./live-headline.ts";
4
+ import {
5
+ appendLines,
6
+ assertNever,
7
+ LIVE_RENDER_TICK_MS,
8
+ type PrefixStyle,
9
+ type RenderEnvironment,
10
+ renderPrefixed,
11
+ type StatusPresentation,
12
+ spinner,
13
+ style,
14
+ } from "./render-blocks.ts";
15
+ import { formatThroughputBadge } from "./render-status.ts";
16
+ import { cellOutputSection, cellStatusSection } from "./render-tail.ts";
17
+ import { formatRuntimeBadge } from "./runtime-label.ts";
18
+ import { formatDuration } from "./tool-widgets.ts";
19
+ import type { EvalCellResult, EvalToolDetails } from "./types.ts";
20
+
21
+ type CellStatus = EvalCellResult["status"];
22
+ export type CellThroughput = { readonly calls: number; readonly wallDurationMs: number | undefined };
23
+ export type CellBadges = {
24
+ readonly reset: boolean;
25
+ readonly timeout: number | undefined;
26
+ readonly throughput: CellThroughput | undefined;
27
+ /** The call lane still streaming its arguments: the header names the state instead of "running". */
28
+ readonly streaming?: boolean;
29
+ };
30
+
31
+ export const LIVE_LINE_PREFIX: PrefixStyle = { prefix: "╶─ ", continuation: " ", color: "borderAccent" };
32
+ const FRAME_HEADER_PREFIX: PrefixStyle = { prefix: "╭─ ", continuation: "│ ", color: "borderAccent" };
33
+ const FRAME_INNER_PREFIX: PrefixStyle = { prefix: "│ ", continuation: "│ ", color: "borderMuted" };
34
+ const FRAME_SECTION_PREFIX: PrefixStyle = { prefix: "├─ ", continuation: "│ ", color: "dim" };
35
+
36
+ export const LIVE_CODE_WINDOW_LINES = 6;
37
+ // The live block's total height never changes: header + 6 body rows + border. When output or
38
+ // status events exist, they take a fixed tail section and the code window shrinks inside the
39
+ // same total (review MEDIUM-2): 6 code rows alone, or 3 code + 3 tail rows.
40
+ const LIVE_BODY_ROWS = 6;
41
+ const LIVE_TAIL_ROWS = 3;
42
+
43
+ export function isLiveCellStatus(status: CellStatus): boolean {
44
+ return status === "pending" || status === "running";
45
+ }
46
+
47
+ export function hasLiveCell(details: EvalToolDetails | undefined): boolean {
48
+ return (details?.cells ?? []).some((cell) => isLiveCellStatus(cell.status) && cell.startedAt !== undefined);
49
+ }
50
+
51
+ export function cellPresentation(status: CellStatus, spinnerFrame: number | undefined): StatusPresentation {
52
+ switch (status) {
53
+ case "pending":
54
+ return { label: "pending", icon: "○", color: "muted" };
55
+ case "queued":
56
+ return { label: "queued", icon: "○", color: "muted" };
57
+ case "running":
58
+ return { label: "running", icon: spinner(spinnerFrame), color: "warning" };
59
+ case "detached":
60
+ return { label: "detached", icon: "↗", color: "warning" };
61
+ case "complete":
62
+ return { label: "done", icon: "✓", color: "success" };
63
+ case "error":
64
+ return { label: "error", icon: "✗", color: "error" };
65
+ case "cancelled":
66
+ return { label: "cancelled", icon: "×", color: "error" };
67
+ default:
68
+ return assertNever(status);
69
+ }
70
+ }
71
+
72
+ // A running cell only receives updates on output/status events, so a stored duration
73
+ // freezes between them. Non-terminal cells therefore derive elapsed time from the
74
+ // render-time clock; terminal cells keep their settled duration verbatim.
75
+ export function cellElapsedMs(cell: EvalCellResult, environment: RenderEnvironment): number | undefined {
76
+ if (!isLiveCellStatus(cell.status) || cell.startedAt === undefined) return cell.durationMs;
77
+ return Math.max(0, environment.now - cell.startedAt);
78
+ }
79
+
80
+ // An in-progress row leads with what the cell is doing (senpi#2802). Collapsed, the whole row
81
+ // (icon, headline and every badge) always fits one visual line (senpi#2933 review HIGH-1).
82
+ // Drop order: reset/timeout, then queued-behind/throughput, then the runtime badge; the
83
+ // headline is cut to its floor (never emptied) before elapsed is dropped, and only the final
84
+ // pass may drop the base label itself (review MEDIUM-2/3).
85
+ export function headlined(
86
+ icon: string,
87
+ summary: string | undefined,
88
+ code: string | undefined,
89
+ rest: string,
90
+ environment: RenderEnvironment,
91
+ options: { readonly badgelessRest?: string; readonly protectedTail?: string } = {},
92
+ ): string {
93
+ if (environment.expanded)
94
+ return `${icon} ${liveHeadline(summary, code, undefined)} · ${rest}${
95
+ options.protectedTail === undefined ? "" : ` · ${options.protectedTail}`
96
+ }`;
97
+ const headline = liveHeadline(summary, code, undefined);
98
+ const width = Math.max(1, environment.width - 3);
99
+ // When a badge rides the base label and the full rest cannot fit on one line, the badge
100
+ // drops before anything else is cut, and the badge-less form is fitted instead (review
101
+ // MEDIUM-1).
102
+ const fullTail = options.protectedTail === undefined ? rest : `${rest} · ${options.protectedTail}`;
103
+ if (
104
+ options.badgelessRest !== undefined &&
105
+ options.badgelessRest !== rest &&
106
+ visibleWidth(`${icon} ${headline} · ${fullTail}`) > width
107
+ )
108
+ return fitOneLine(icon, headline, options.badgelessRest, width, options.protectedTail);
109
+ return fitOneLine(icon, headline, rest, width, options.protectedTail);
110
+ }
111
+
112
+ const HEADLINE_FLOOR_CELLS = 12;
113
+
114
+ function fitOneLine(icon: string, headline: string, rest: string, width: number, protectedTail?: string): string {
115
+ const lead = `${icon} `;
116
+ const middle = " · ";
117
+ const tail = protectedTail === undefined ? "" : `${middle}${protectedTail}`;
118
+ const full = `${lead}${headline}${middle}${rest}${tail}`;
119
+ if (visibleWidth(full) <= width) return full;
120
+ // 1) Drop the lowest-priority tail segments (reset/timeout are last in rest), keeping
121
+ // elapsed appended, until one fits with the full headline.
122
+ let keptRest = rest;
123
+ for (;;) {
124
+ const cut = keptRest.lastIndexOf(middle);
125
+ if (cut <= 0) break;
126
+ const candidate = keptRest.slice(0, cut);
127
+ if (visibleWidth(`${lead}${headline}${middle}${candidate}${tail}`) <= width)
128
+ return `${lead}${headline}${middle}${candidate}${tail}`;
129
+ keptRest = candidate;
130
+ }
131
+ // 2) Cut the headline to its floor before dropping elapsed (review MEDIUM-3).
132
+ const floorBudget = Math.max(4, width - visibleWidth(`${lead}${middle}${keptRest}${tail}`) - 1);
133
+ const floor = Math.max(HEADLINE_FLOOR_CELLS, floorBudget);
134
+ let cutHeadline = headline;
135
+ if (visibleWidth(headline) > floor) {
136
+ const candidate = `${cellPrefixText(headline, floor - 1)}…`;
137
+ if (visibleWidth(`${lead}${candidate}${middle}${keptRest}${tail}`) <= width) cutHeadline = candidate;
138
+ }
139
+ // Step 1 already left only the base label in keptRest, so nothing else can drop before elapsed.
140
+ const finalRest = keptRest;
141
+ if (visibleWidth(`${lead}${cutHeadline}${middle}${finalRest}${tail}`) <= width)
142
+ return `${lead}${cutHeadline}${middle}${finalRest}${tail}`;
143
+ // 3) Drop elapsed, then cut the headline to whatever the base label leaves (review MEDIUM-3).
144
+ if (visibleWidth(`${lead}${cutHeadline}${middle}${finalRest}`) <= width)
145
+ return `${lead}${cutHeadline}${middle}${finalRest}`;
146
+ const budget = Math.max(4, width - visibleWidth(`${lead}${middle}${finalRest}`) - 1);
147
+ const shortened = visibleWidth(cutHeadline) <= budget ? cutHeadline : `${cellPrefixText(cutHeadline, budget - 1)}…`;
148
+ return `${lead}${shortened}${middle}${finalRest}`;
149
+ }
150
+
151
+ function cellPrefixText(text: string, cells: number): string {
152
+ let kept = "";
153
+ let used = 0;
154
+ for (const { segment } of new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(text)) {
155
+ const width = visibleWidth(segment);
156
+ if (used + width > cells) break;
157
+ kept += segment;
158
+ used += width;
159
+ }
160
+ return kept;
161
+ }
162
+
163
+ export function cellHeader(cell: EvalCellResult, environment: RenderEnvironment, badges: CellBadges): string {
164
+ const spinnerFrame =
165
+ environment.spinnerFrame ?? Math.floor((cellElapsedMs(cell, environment) ?? 0) / LIVE_RENDER_TICK_MS);
166
+ const presentation =
167
+ badges.streaming === true
168
+ ? { label: "streaming", icon: spinner(spinnerFrame), color: "warning" as const }
169
+ : cellPresentation(cell.status, spinnerFrame);
170
+ const runtimeBadge = cell.runtime === undefined ? "" : ` (${formatRuntimeBadge(cell.language, cell.runtime)})`;
171
+ const base = leadsWithHeadline(cell.status)
172
+ ? `eval ${cell.language} ${presentation.label}`
173
+ : `eval ${cell.language} ${presentation.label} ${presentation.icon}`;
174
+ const segments: string[] = [];
175
+ // The runtime badge is droppable like any badge (review MEDIUM-2): it sits after
176
+ // queued-behind/throughput so those drop first. Elapsed is passed separately as the
177
+ // protected tail, so it is dropped only after the headline is cut (review MEDIUM-3).
178
+ if (cell.queuedBehind !== undefined && cell.queuedBehind.length > 0)
179
+ segments.push(`queued behind ${cell.queuedBehind.map(sanitizeTerminalLabel).join(", ")}`);
180
+ else if (cell.queuedBehind !== undefined && cell.status === "queued")
181
+ segments.push(`waiting for the ${cell.language} kernel to be ready`);
182
+ const throughputBadge = badges.throughput === undefined ? undefined : formatThroughputBadge(badges.throughput);
183
+ if (throughputBadge !== undefined) segments.push(throughputBadge);
184
+ if (runtimeBadge !== "") segments.push(runtimeBadge.trimStart());
185
+ if (badges.reset) segments.push("reset");
186
+ if (badges.timeout !== undefined) segments.push(`timeout ${badges.timeout}s`);
187
+ const elapsedMs = badges.throughput?.wallDurationMs ?? cellElapsedMs(cell, environment);
188
+ const protectedTail = elapsedMs === undefined ? undefined : formatDuration(elapsedMs);
189
+ const header = leadsWithHeadline(cell.status)
190
+ ? headlined(presentation.icon, cell.summary, cell.code, joinSegments(base, segments), environment, {
191
+ protectedTail,
192
+ })
193
+ : joinSegments(base, protectedTail === undefined ? segments : [...segments, protectedTail]);
194
+ return style(environment.theme, presentation.color, header);
195
+ }
196
+
197
+ function joinSegments(base: string, segments: readonly string[]): string {
198
+ return segments.length === 0 ? base : `${base} · ${segments.join(" · ")}`;
199
+ }
200
+
201
+ // A live row is a framed block of constant total height: header + LIVE_BODY_ROWS body rows +
202
+ // border, whether or not output or status events have arrived. New code lines scroll the code
203
+ // window upward inside its share; the hidden prefix folds into one "N earlier code lines" row
204
+ // counted inside the share, so the block never grows the transcript.
205
+ export function renderLiveCellFrame(
206
+ cell: EvalCellResult,
207
+ environment: RenderEnvironment,
208
+ badges: CellBadges,
209
+ ): string[] {
210
+ const lines = renderPrefixed(cellHeader(cell, environment, badges), environment, FRAME_HEADER_PREFIX);
211
+ const hasOutput = cell.output.trimEnd().length > 0;
212
+ const hasStatus = (cell.statusEvents ?? []).some((event) => event.op !== "agent");
213
+ // The tail always keeps its full row count (review HIGH-C): a short section is padded, and
214
+ // each section fits itself inside the budget in visual rows with exact omission markers.
215
+ const tail: string[] = [];
216
+ if (hasOutput) appendLines(tail, cellOutputSection(cell, environment, LIVE_TAIL_ROWS, LIVE_TAIL_ROWS));
217
+ else if (hasStatus) appendLines(tail, cellStatusSection(cell, environment, LIVE_TAIL_ROWS));
218
+ const codeRows = tail.length === 0 ? LIVE_BODY_ROWS : LIVE_BODY_ROWS - LIVE_TAIL_ROWS;
219
+ appendLines(lines, liveCodeWindow(cell, environment, codeRows));
220
+ appendLines(lines, tail);
221
+ lines.push(style(environment.theme, "borderMuted", "╰─"));
222
+ return lines;
223
+ }
224
+
225
+ function liveCodeWindow(cell: EvalCellResult, environment: RenderEnvironment, windowRows: number): string[] {
226
+ const innerWidth = Math.max(1, environment.width - 2);
227
+ // Streamed code is not yet trusted input: a hostile or half-arrived chunk can carry escape and
228
+ // control characters, and the collapsed row must stay inert in the terminal (senpi#2839).
229
+ const code = highlightedCode(sanitizeCellCode(cell.code), cell.language, environment.theme, environment.repaint);
230
+ // Fill from the bottom by VISUAL rows so the newest source line is always fully visible
231
+ // (review HIGH-B); the marker counts hidden SOURCE lines. When the source overflows, the
232
+ // marker takes one of the window's rows.
233
+ const allRows = visualLines(code, innerWidth);
234
+ const totalRows = allRows.length;
235
+ const fits = totalRows <= windowRows;
236
+ const budget = fits ? windowRows : windowRows - 1;
237
+ const kept = allRows.slice(Math.max(0, totalRows - budget));
238
+ const skippedVisual = totalRows - kept.length;
239
+ const hiddenSourceLines = countHiddenSourceLines(code, innerWidth, skippedVisual);
240
+ const windowLines: string[] = [];
241
+ if (!fits)
242
+ appendLines(
243
+ windowLines,
244
+ renderPrefixed(`${hiddenSourceLines} earlier code lines`, environment, {
245
+ prefix: "│ ",
246
+ continuation: "│ ",
247
+ color: "muted",
248
+ }),
249
+ );
250
+ for (const line of kept) appendLines(windowLines, renderPrefixed(line, environment, FRAME_INNER_PREFIX));
251
+ while (windowLines.length < windowRows) windowLines.push(style(environment.theme, "borderMuted", "│ "));
252
+ return windowLines;
253
+ }
254
+
255
+ // The source lines whose first visual row lies above the first shown row (review HIGH-B):
256
+ // walk the code's own lines, wrap each, and count a line as hidden as soon as the cut reaches
257
+ // its first row — a line the cut slices through is hidden even though its tail still shows.
258
+ function countHiddenSourceLines(code: string, innerWidth: number, skippedVisual: number): number {
259
+ if (skippedVisual <= 0) return 0;
260
+ let consumed = 0;
261
+ let hidden = 0;
262
+ for (const sourceLine of code.split("\n")) {
263
+ if (consumed >= skippedVisual) break;
264
+ hidden += 1;
265
+ consumed += Math.max(1, visualLines(sourceLine, innerWidth).length);
266
+ }
267
+ return hidden;
268
+ }
269
+
270
+ const TERMINAL_ESCAPE_SEQUENCE =
271
+ /(?:\u001B\][\s\S]*?(?:\u0007|\u001B\\|\u009C))|[\u001B\u009B][[\]()#;?]*(?:\d{1,4}(?:[;:]\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]/g;
272
+ const TERMINAL_CONTROL_RUN = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]+/g;
273
+
274
+ // The window strips escape sequences and control characters (senpi#2839) but never collapses
275
+ // whitespace: indentation and inner spacing carry meaning in every language (review HIGH-A),
276
+ // and tabs expand to two spaces.
277
+ function visualLines(text: string, width: number): string[] {
278
+ return truncateToVisualLines(text, Number.POSITIVE_INFINITY, width).visualLines.map((line) => line.trimEnd());
279
+ }
280
+
281
+ function sanitizeCellCode(code: string): string {
282
+ return code
283
+ .split("\n")
284
+ .map((line) => line.replace(TERMINAL_ESCAPE_SEQUENCE, "").replace(TERMINAL_CONTROL_RUN, "").replace(/\t/g, " "))
285
+ .join("\n");
286
+ }
287
+
288
+ export { FRAME_HEADER_PREFIX, FRAME_INNER_PREFIX, FRAME_SECTION_PREFIX };