@code-yeongyu/senpi-codemode 2026.10.10-10 → 2026.10.10-12

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 (47) hide show
  1. package/CHANGELOG.md +46 -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/runner.jl +11 -22
  10. package/src/kernels/js/group-signal-notice.js +117 -0
  11. package/src/kernels/js/inline-worker.ts +2 -0
  12. package/src/kernels/js/local-module-loader.ts +8 -0
  13. package/src/kernels/js/process-tree-host.ts +4 -8
  14. package/src/kernels/js/process-tree.d.ts +11 -0
  15. package/src/kernels/js/process-tree.js +55 -10
  16. package/src/kernels/js/windows-tree-kill-host.ts +117 -0
  17. package/src/kernels/js/windows-tree-kill.d.ts +19 -0
  18. package/src/kernels/js/windows-tree-kill.js +100 -0
  19. package/src/kernels/js/worker-cwd.js +41 -10
  20. package/src/kernels/js/worker-runtime.js +23 -4
  21. package/src/kernels/js/worker-shell-capture.js +4 -0
  22. package/src/kernels/js/worker-slot.ts +2 -3
  23. package/src/kernels/py/prelude.py +36 -25
  24. package/src/kernels/py/transport.ts +4 -0
  25. package/src/kernels/rb/runner.rb +9 -19
  26. package/src/kernels/sandbox/vendor/pi-codemode/runtime/host.ts +2 -2
  27. package/src/kernels/shared/bun-ffi.ts +24 -0
  28. package/src/kernels/shared/kernel-memory-host.ts +30 -7
  29. package/src/kernels/shared/process-cpu.ts +7 -3
  30. package/src/kernels/shared/process-group-cpu.ts +6 -3
  31. package/src/kernels/shared/subprocess-kernel.ts +29 -3
  32. package/src/kernels/shared/subprocess-memory-globals.ts +101 -0
  33. package/src/kernels/shared/subprocess-process.ts +20 -12
  34. package/src/kernels/shared/subprocess-queue.ts +1 -0
  35. package/src/tool/eval-tool.ts +2 -1
  36. package/src/tool/render-agent.ts +110 -0
  37. package/src/tool/render-blocks.ts +252 -0
  38. package/src/tool/render-call.ts +130 -0
  39. package/src/tool/render-cell.ts +165 -0
  40. package/src/tool/render-detail.ts +152 -0
  41. package/src/tool/render-live.ts +288 -0
  42. package/src/tool/render-result.ts +202 -0
  43. package/src/tool/render-status.ts +166 -0
  44. package/src/tool/render-tail.ts +139 -0
  45. package/src/tool/render.ts +3 -1128
  46. package/src/tool/run-eval-cell.ts +7 -12
  47. package/src/tool/types.ts +11 -2
@@ -0,0 +1,166 @@
1
+ import type { Theme } from "@code-yeongyu/senpi";
2
+ import { type RenderEnvironment, STATUS_PREVIEW_COUNT, style } from "./render-blocks.ts";
3
+ import type { EvalStatusEvent } from "./types.ts";
4
+
5
+ export function eventString(value: unknown): string | undefined {
6
+ return typeof value === "string" && value.length > 0 ? value : undefined;
7
+ }
8
+
9
+ function eventNumber(value: unknown): number {
10
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
11
+ }
12
+
13
+ function plural(count: number, singular: string, pluralNoun: string): string {
14
+ return `${count} ${count === 1 ? singular : pluralNoun}`;
15
+ }
16
+
17
+ function formatCallsPerSecond(calls: number, wallDurationMs: number | undefined): string {
18
+ const seconds = typeof wallDurationMs === "number" && Number.isFinite(wallDurationMs) ? wallDurationMs / 1_000 : 0;
19
+ if (seconds <= 0) return "n/a calls/s";
20
+ return `${(calls / seconds).toFixed(2)} calls/s`;
21
+ }
22
+
23
+ // A cell that issued no tool calls has no throughput to report: emitting
24
+ // "0 calls · 0.00 calls/s" is noise, so the whole badge is dropped instead.
25
+ export function formatThroughputBadge(throughput: {
26
+ readonly calls: number;
27
+ readonly wallDurationMs: number | undefined;
28
+ }): string | undefined {
29
+ if (throughput.calls <= 0) return undefined;
30
+ return `${plural(throughput.calls, "call", "calls")} · ${formatCallsPerSecond(
31
+ throughput.calls,
32
+ throughput.wallDurationMs,
33
+ )}`;
34
+ }
35
+
36
+ function statusIcon(op: string): string {
37
+ if (op.startsWith("git_")) return "⌁";
38
+ switch (op) {
39
+ case "read":
40
+ case "write":
41
+ case "cat":
42
+ case "touch":
43
+ return "▣";
44
+ case "ls":
45
+ case "cd":
46
+ case "pwd":
47
+ case "mkdir":
48
+ return "▤";
49
+ case "run":
50
+ case "sh":
51
+ return "▶";
52
+ case "completion":
53
+ return "◇";
54
+ case "phase":
55
+ return "◆";
56
+ default:
57
+ return "•";
58
+ }
59
+ }
60
+
61
+ function formatStatusEvent(event: EvalStatusEvent, theme: Theme | undefined): string {
62
+ const op = event.op;
63
+ const icon = style(theme, "muted", statusIcon(op));
64
+ const error = eventString(event.error);
65
+ if (error !== undefined) return `${icon} ${style(theme, "warning", op)}: ${style(theme, "dim", error)}`;
66
+ const parts: string[] = [];
67
+ switch (op) {
68
+ case "read": {
69
+ parts.push(`${eventNumber(event.chars ?? event.bytes)} chars`);
70
+ const path = eventString(event.path);
71
+ if (path !== undefined) parts.push(`from ${path}`);
72
+ break;
73
+ }
74
+ case "write": {
75
+ parts.push(`${eventNumber(event.chars ?? event.bytes)} chars`);
76
+ const path = eventString(event.path);
77
+ if (path !== undefined) parts.push(`to ${path}`);
78
+ break;
79
+ }
80
+ case "cat":
81
+ parts.push(plural(eventNumber(event.files), "file", "files"));
82
+ parts.push(`${eventNumber(event.chars)} chars`);
83
+ break;
84
+ case "ls":
85
+ parts.push(plural(eventNumber(event.count), "entry", "entries"));
86
+ break;
87
+ case "env": {
88
+ const action = eventString(event.action);
89
+ const key = eventString(event.key);
90
+ const value = eventString(event.value) ?? "";
91
+ if (action === "set" && key !== undefined) parts.push(`set ${key}=${value.slice(0, 30)}`);
92
+ else if (action === "get" && key !== undefined) parts.push(`${key}=${value.slice(0, 30)}`);
93
+ else parts.push(plural(eventNumber(event.count), "variable", "variables"));
94
+ break;
95
+ }
96
+ case "git_status": {
97
+ if (event.clean === true) parts.push("clean");
98
+ else {
99
+ const changes: string[] = [];
100
+ for (const key of ["staged", "modified", "untracked"] as const) {
101
+ const count = eventNumber(event[key]);
102
+ if (count > 0) changes.push(`${count} ${key}`);
103
+ }
104
+ parts.push(changes.join(", ") || "unknown");
105
+ }
106
+ const branch = eventString(event.branch);
107
+ if (branch !== undefined) parts.push(`on ${branch}`);
108
+ break;
109
+ }
110
+ case "git_diff":
111
+ parts.push(plural(eventNumber(event.lines), "line", "lines"));
112
+ if (event.staged === true) parts.push("staged");
113
+ break;
114
+ case "git_log":
115
+ parts.push(plural(eventNumber(event.commits), "commit", "commits"));
116
+ break;
117
+ case "run":
118
+ case "sh": {
119
+ const command = eventString(event.command ?? event.cmd);
120
+ if (command !== undefined) parts.push(command);
121
+ if (typeof event.exitCode === "number") parts.push(`exit ${event.exitCode}`);
122
+ break;
123
+ }
124
+ case "completion": {
125
+ const model = eventString(event.model);
126
+ const tier = eventString(event.tier);
127
+ if (model !== undefined) parts.push(model);
128
+ if (tier !== undefined && tier !== model) parts.push(tier);
129
+ parts.push(`${eventNumber(event.chars)} chars`);
130
+ break;
131
+ }
132
+ case "log":
133
+ parts.push(eventString(event.message) ?? "");
134
+ break;
135
+ case "phase":
136
+ parts.push(eventString(event.title) ?? "");
137
+ break;
138
+ case "status-events-omitted":
139
+ parts.push(`${eventNumber(event.count)} earlier events omitted`);
140
+ break;
141
+ default: {
142
+ if (event.count !== undefined) parts.push(String(event.count));
143
+ const path = eventString(event.path);
144
+ if (path !== undefined) parts.push(path);
145
+ }
146
+ }
147
+ const description = parts.filter((part) => part.length > 0).join(" · ");
148
+ return `${icon} ${style(theme, "muted", op)}${description.length > 0 ? ` ${style(theme, "dim", description)}` : ""}`;
149
+ }
150
+
151
+ export function renderStatusEvents(events: readonly EvalStatusEvent[], environment: RenderEnvironment): string[] {
152
+ // A bounded history stores its exact omission count in a leading marker event; fold that
153
+ // count into the summary line so collapsing the preview can never understate omissions.
154
+ const first = events[0];
155
+ const omittedByBound = first?.op === "status-events-omitted" && typeof first.count === "number" ? first.count : 0;
156
+ const visible = omittedByBound > 0 ? events.slice(1) : events;
157
+ const retained = environment.expanded ? visible : visible.slice(-STATUS_PREVIEW_COUNT);
158
+ const skipped = visible.length - retained.length + omittedByBound;
159
+ const lines: string[] = [];
160
+ if (skipped > 0) lines.push(style(environment.theme, "dim", `├ … ${skipped} earlier status events`));
161
+ for (const [index, event] of retained.entries()) {
162
+ const branch = index === retained.length - 1 ? "└" : "├";
163
+ lines.push(`${style(environment.theme, "dim", branch)} ${formatStatusEvent(event, environment.theme)}`);
164
+ }
165
+ return lines;
166
+ }
@@ -0,0 +1,139 @@
1
+ import { visibleWidth } from "@code-yeongyu/senpi";
2
+ import { appendLines, previewText, type RenderEnvironment, renderPrefixed, style } from "./render-blocks.ts";
3
+ import { FRAME_INNER_PREFIX, FRAME_SECTION_PREFIX } from "./render-live.ts";
4
+ import { renderStatusEvents } from "./render-status.ts";
5
+ import type { EvalCellResult } from "./types.ts";
6
+
7
+ export function cellOutputSection(
8
+ cell: EvalCellResult,
9
+ environment: RenderEnvironment,
10
+ maxLinesOrBudget: number,
11
+ rowBudget?: number,
12
+ ): string[] {
13
+ const output = cell.output.trimEnd();
14
+ if (output.length === 0) return [];
15
+ const lines = renderPrefixed("output", environment, FRAME_SECTION_PREFIX);
16
+ const outputColor = cell.status === "error" ? "error" : "toolOutput";
17
+ const styledOutput = output
18
+ .split("\n")
19
+ .map((line) => style(environment.theme, outputColor, line))
20
+ .join("\n");
21
+ const innerWidth = Math.max(1, environment.width - 2);
22
+ if (rowBudget !== undefined) {
23
+ // Fit and pad inside the row budget (review HIGH-C): header + body never exceed the
24
+ // budget, the marker consumes one body row when anything is hidden, and a short section
25
+ // is padded so the block's total height never changes. The marker counts every hidden
26
+ // row (review NEW-1): the kept tail plus the marker together are the whole output.
27
+ const bodyRows = rowBudget - 1;
28
+ const preview = previewText(styledOutput, bodyRows, innerWidth);
29
+ const body: string[] = [];
30
+ const kept = preview.skipped > 0 ? preview.lines.slice(-(bodyRows - 1)) : preview.lines;
31
+ const omitted = preview.skipped + preview.lines.length - kept.length;
32
+ if (omitted > 0) {
33
+ appendLines(
34
+ body,
35
+ renderPrefixed(`${omitted} earlier output lines`, environment, {
36
+ prefix: "│ ",
37
+ continuation: "│ ",
38
+ color: "muted",
39
+ }),
40
+ );
41
+ }
42
+ for (const line of kept) appendLines(body, renderPrefixed(line, environment, FRAME_INNER_PREFIX));
43
+ const rows = [...lines, ...body];
44
+ while (rows.length < rowBudget) rows.push(style(environment.theme, "borderMuted", "│ "));
45
+ return rows.slice(0, rowBudget);
46
+ }
47
+ const preview = previewText(styledOutput, maxLinesOrBudget, innerWidth);
48
+ if (preview.skipped > 0)
49
+ appendLines(
50
+ lines,
51
+ renderPrefixed(`${preview.skipped} earlier output lines`, environment, {
52
+ prefix: "│ ",
53
+ continuation: "│ ",
54
+ color: "muted",
55
+ }),
56
+ );
57
+ for (const line of preview.lines) appendLines(lines, renderPrefixed(line, environment, FRAME_INNER_PREFIX));
58
+ return lines;
59
+ }
60
+
61
+ export function cellStatusSection(cell: EvalCellResult, environment: RenderEnvironment, rowBudget?: number): string[] {
62
+ const statusEvents = (cell.statusEvents ?? []).filter((event) => event.op !== "agent");
63
+ if (statusEvents.length === 0) return [];
64
+ if (rowBudget === undefined) {
65
+ const lines = renderPrefixed("status", environment, FRAME_SECTION_PREFIX);
66
+ for (const line of renderStatusEvents(statusEvents, environment))
67
+ appendLines(lines, renderPrefixed(line, environment, FRAME_INNER_PREFIX));
68
+ return lines;
69
+ }
70
+ // Budget in visual ROWS, not events (review HIGH-C). The tail keeps the NEWEST rows (review
71
+ // NEW-2): whole events fold from the front into the exact marker when their rows overflow,
72
+ // and a single event that alone overflows keeps its newest rows while the marker counts the
73
+ // rows cut from its head. Rows are kept or dropped whole, never re-styled (review r4 HIGH-1).
74
+ const first = statusEvents[0];
75
+ const omittedByBound = first?.op === "status-events-omitted" && typeof first.count === "number" ? first.count : 0;
76
+ const visible = omittedByBound > 0 ? statusEvents.slice(1) : statusEvents;
77
+ const bodyRows = rowBudget - 1;
78
+ // Render each event to its own rows so whole events fold cleanly. The fold marker counts
79
+ // exactly the folded events plus the stored bound (review HIGH-2, HIGH-C).
80
+ const perEvent = visible.map((event) => {
81
+ const rendered = renderStatusEvents([event], { ...environment, expanded: true });
82
+ const rows: string[] = [];
83
+ for (const line of rendered) appendLines(rows, renderPrefixed(line, environment, FRAME_INNER_PREFIX));
84
+ return rows;
85
+ });
86
+ // Keep events from the newest until the next-older one would overflow the body; the newest
87
+ // event is always kept, even when it alone overflows (review NEW-2). The fold marker counts
88
+ // exactly the folded events plus the stored bound (review HIGH-2, HIGH-C).
89
+ const keptEvents: string[][] = [];
90
+ let used = 0;
91
+ let firstShown = perEvent.length;
92
+ for (let i = perEvent.length - 1; i >= 0; i--) {
93
+ const rows = perEvent[i] ?? [];
94
+ // The fold marker appears whenever anything older than event i (an earlier event or the
95
+ // stored bound) is hidden, so it reserves one body row of the budget for keeping event i.
96
+ const marker = i > 0 || omittedByBound > 0 ? 1 : 0;
97
+ if (used + rows.length <= bodyRows - marker || keptEvents.length === 0) {
98
+ keptEvents.unshift(rows);
99
+ used += rows.length;
100
+ firstShown = i;
101
+ } else {
102
+ break;
103
+ }
104
+ }
105
+ const skipped = firstShown + omittedByBound;
106
+ const eventRows: string[] = [];
107
+ for (const rows of keptEvents) appendLines(eventRows, rows);
108
+ // A still-overflowing tail is a single event taller than its share: keep its newest rows and
109
+ // count the rows cut from its head in the marker, which then always takes one body row.
110
+ const needsMarker = skipped > 0 || eventRows.length > bodyRows;
111
+ const shownBudget = Math.max(0, bodyRows - (needsMarker ? 1 : 0));
112
+ const cutRows = Math.max(0, eventRows.length - shownBudget);
113
+ const body: string[] = [];
114
+ if (needsMarker) body.push(statusMarker(skipped, cutRows, environment));
115
+ appendLines(body, eventRows.slice(cutRows));
116
+ const rows = [...renderPrefixed("status", environment, FRAME_SECTION_PREFIX), ...body.slice(0, bodyRows)];
117
+ while (rows.length < rowBudget) rows.push(style(environment.theme, "borderMuted", "│ "));
118
+ return rows;
119
+ }
120
+
121
+ // The fold marker is always exactly one row: the longest wording that fits the width wins, so
122
+ // both counts survive narrow terminals (review r4 HIGH-1, MEDIUM-1).
123
+ function statusMarker(skippedEvents: number, cutRows: number, environment: RenderEnvironment): string {
124
+ const candidates =
125
+ cutRows === 0
126
+ ? [`├ … ${skippedEvents} earlier status events`, `├ … ${skippedEvents} earlier`, `├ … ${skippedEvents}`]
127
+ : skippedEvents === 0
128
+ ? [`├ … ${cutRows} earlier rows of this event`, `├ … ${cutRows} earlier rows`, `├ … ${cutRows} rows`]
129
+ : [
130
+ `├ … ${skippedEvents} earlier status events, ${cutRows} rows`,
131
+ `├ … ${skippedEvents} events, ${cutRows} rows`,
132
+ `├ … ${skippedEvents} ev, ${cutRows} rows`,
133
+ `├ … ${skippedEvents}ev ${cutRows}r`,
134
+ ];
135
+ const innerWidth = Math.max(1, environment.width - visibleWidth(FRAME_INNER_PREFIX.prefix));
136
+ const text = candidates.find((candidate) => visibleWidth(candidate) <= innerWidth) ?? candidates.at(-1) ?? "";
137
+ const [row] = renderPrefixed(text, environment, { prefix: "│ ", continuation: "│ ", color: "dim" });
138
+ return row ?? "";
139
+ }