@code-yeongyu/senpi-codemode 2026.9.23 → 2026.9.24-3
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.
- package/CHANGELOG.md +98 -0
- package/README.md +16 -10
- package/package.json +4 -4
- package/src/tool/AGENTS.md +1 -0
- package/src/tool/code-preview.ts +31 -0
- package/src/tool/display-code.ts +48 -0
- package/src/tool/display-js-ast.ts +65 -0
- package/src/tool/display-js-layout.ts +90 -0
- package/src/tool/display-js-mask.ts +178 -0
- package/src/tool/display-js.ts +100 -0
- package/src/tool/display-python-script.ts +127 -0
- package/src/tool/display-python.ts +150 -0
- package/src/tool/eval-request.ts +24 -15
- package/src/tool/eval-tool.ts +3 -3
- package/src/tool/render.ts +45 -58
- package/src/tool/types.ts +17 -13
package/src/tool/render.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// allow: SIZE_OK — one eval render pipeline avoids the runtime/render TDZ that motivated this module boundary.
|
|
2
2
|
import {
|
|
3
3
|
type AgentToolResult,
|
|
4
|
-
highlightCode,
|
|
5
4
|
sanitizeTerminalLabel,
|
|
6
5
|
type Theme,
|
|
7
6
|
type ThemeColor,
|
|
@@ -9,8 +8,10 @@ import {
|
|
|
9
8
|
type ToolRenderResultOptions,
|
|
10
9
|
truncateToVisualLines,
|
|
11
10
|
} from "@code-yeongyu/senpi";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
11
|
+
import type { TruncationMeta } from "../output/output-meta.ts";
|
|
12
|
+
import { highlightedCode } from "./code-preview.ts";
|
|
13
|
+
import { displayCode } from "./display-code.ts";
|
|
14
|
+
import { normalizeEvalSummary } from "./eval-request.ts";
|
|
14
15
|
import {
|
|
15
16
|
JSON_TREE_MAX_DEPTH_COLLAPSED,
|
|
16
17
|
JSON_TREE_MAX_DEPTH_EXPANDED,
|
|
@@ -25,7 +26,6 @@ import { codePointPrefix, formatDuration, renderToolCallWidget } from "./tool-wi
|
|
|
25
26
|
import type {
|
|
26
27
|
EvalCellResult,
|
|
27
28
|
EvalInputSchema,
|
|
28
|
-
EvalLanguage,
|
|
29
29
|
EvalResultDetails,
|
|
30
30
|
EvalStatusEvent,
|
|
31
31
|
EvalToolDetails,
|
|
@@ -65,6 +65,7 @@ type RenderBlock =
|
|
|
65
65
|
| { readonly kind: "dynamic"; readonly render: (width: number) => readonly string[] };
|
|
66
66
|
|
|
67
67
|
const CODE_PREVIEW_LINES = 4;
|
|
68
|
+
const SUMMARY_PREVIEW_LINES = 3;
|
|
68
69
|
const OUTPUT_PREVIEW_LINES = 8;
|
|
69
70
|
const STATUS_PREVIEW_COUNT = 3;
|
|
70
71
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
|
|
@@ -183,6 +184,22 @@ function renderAllVisualLines(text: string, width: number): string[] {
|
|
|
183
184
|
return truncateToVisualLines(text, Number.POSITIVE_INFINITY, width).visualLines.map((line) => line.trimEnd());
|
|
184
185
|
}
|
|
185
186
|
|
|
187
|
+
// A summary has no length limit, so a collapsed block shows its first lines and marks the cut.
|
|
188
|
+
function summaryVisualLines(summary: string, width: number, expanded: boolean): string[] {
|
|
189
|
+
const lines = renderAllVisualLines(summary, width);
|
|
190
|
+
if (expanded || lines.length <= SUMMARY_PREVIEW_LINES) return lines;
|
|
191
|
+
const kept = renderAllVisualLines(summary, Math.max(1, width - 1)).slice(0, SUMMARY_PREVIEW_LINES);
|
|
192
|
+
kept[SUMMARY_PREVIEW_LINES - 1] = `${kept[SUMMARY_PREVIEW_LINES - 1] ?? ""}\u2026`;
|
|
193
|
+
return kept;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function summaryBlock(summary: string, theme: Theme | undefined, expanded: boolean): RenderBlock {
|
|
197
|
+
return {
|
|
198
|
+
kind: "dynamic",
|
|
199
|
+
render: (width) => summaryVisualLines(summary, width, expanded).map((line) => style(theme, "muted", line)),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
186
203
|
function renderTextBlock(block: Extract<RenderBlock, { kind: "text" }>, width: number): string[] {
|
|
187
204
|
if (block.maxVisualLines === undefined) return renderAllVisualLines(block.text, width);
|
|
188
205
|
const result = truncateToVisualLines(block.text, block.maxVisualLines, width);
|
|
@@ -251,6 +268,8 @@ type RenderEnvironment = {
|
|
|
251
268
|
readonly meta: TruncationMeta | undefined;
|
|
252
269
|
/** Render-time clock, injected so elapsed time is deterministic under test. */
|
|
253
270
|
readonly now: number;
|
|
271
|
+
/** Repaints the row once a background-formatted code preview is ready; absent while args stream. */
|
|
272
|
+
readonly repaint?: () => void;
|
|
254
273
|
};
|
|
255
274
|
type CellBadges = {
|
|
256
275
|
readonly reset: boolean;
|
|
@@ -270,27 +289,6 @@ function assertNever(value: never): never {
|
|
|
270
289
|
throw new TypeError(`Unhandled eval render variant: ${String(value)}`);
|
|
271
290
|
}
|
|
272
291
|
|
|
273
|
-
function languageForHighlighter(language: EvalLanguage): "python" | "javascript" | "ruby" | "julia" {
|
|
274
|
-
switch (language) {
|
|
275
|
-
case "py":
|
|
276
|
-
return "python";
|
|
277
|
-
case "js":
|
|
278
|
-
return "javascript";
|
|
279
|
-
case "rb":
|
|
280
|
-
return "ruby";
|
|
281
|
-
case "jl":
|
|
282
|
-
return "julia";
|
|
283
|
-
default:
|
|
284
|
-
return assertNever(language);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function highlightedCode(code: string, language: EvalLanguage, theme: Theme | undefined): string {
|
|
289
|
-
const normalizedCode = code.trim().length > 0 ? code : "...";
|
|
290
|
-
const lines = highlightCode(normalizedCode, languageForHighlighter(language));
|
|
291
|
-
return (theme === undefined ? lines.map((line) => line.replace(/\u001b\[[0-9;]*m/gu, "")) : lines).join("\n");
|
|
292
|
-
}
|
|
293
|
-
|
|
294
292
|
function spinner(frame: number | undefined): string {
|
|
295
293
|
return SPINNER_FRAMES.at((frame ?? 0) % SPINNER_FRAMES.length) ?? SPINNER_FRAMES[0];
|
|
296
294
|
}
|
|
@@ -619,16 +617,14 @@ function renderCell(cell: EvalCellResult, environment: RenderEnvironment, badges
|
|
|
619
617
|
if (cell.summary !== undefined) {
|
|
620
618
|
appendLines(
|
|
621
619
|
lines,
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
color: "muted",
|
|
626
|
-
}),
|
|
620
|
+
summaryVisualLines(cell.summary, Math.max(1, environment.width - 2), environment.expanded).map(
|
|
621
|
+
(line) => `${style(environment.theme, "muted", "│ ")}${style(environment.theme, "muted", line)}`,
|
|
622
|
+
),
|
|
627
623
|
);
|
|
628
624
|
}
|
|
629
625
|
const innerWidth = Math.max(1, environment.width - 2);
|
|
630
626
|
const codePreview = previewText(
|
|
631
|
-
highlightedCode(cell.code, cell.language, environment.theme),
|
|
627
|
+
highlightedCode(cell.code, cell.language, environment.theme, environment.repaint),
|
|
632
628
|
environment.expanded ? Number.POSITIVE_INFINITY : CODE_PREVIEW_LINES,
|
|
633
629
|
innerWidth,
|
|
634
630
|
);
|
|
@@ -645,7 +641,7 @@ function renderCell(cell: EvalCellResult, environment: RenderEnvironment, badges
|
|
|
645
641
|
for (const line of codePreview.lines) {
|
|
646
642
|
appendLines(lines, renderPrefixed(line, environment, { prefix: "│ ", continuation: "│ ", color: "borderMuted" }));
|
|
647
643
|
}
|
|
648
|
-
const output =
|
|
644
|
+
const output = cell.output.trimEnd();
|
|
649
645
|
if (output.length > 0) {
|
|
650
646
|
appendLines(lines, renderPrefixed("output", environment, { prefix: "├─ ", continuation: "│ ", color: "dim" }));
|
|
651
647
|
const outputColor: ThemeColor = cell.status === "error" ? "error" : "toolOutput";
|
|
@@ -757,24 +753,13 @@ function renderDetailedLines(
|
|
|
757
753
|
context.environment.width,
|
|
758
754
|
),
|
|
759
755
|
);
|
|
760
|
-
if (details.notice !== undefined)
|
|
761
|
-
appendLines(
|
|
762
|
-
lines,
|
|
763
|
-
renderAllVisualLines(style(context.environment.theme, "dim", details.notice), context.environment.width),
|
|
764
|
-
);
|
|
765
|
-
const warning = formatTruncationWarning(details.meta) ?? (details.truncated ? "[eval output truncated]" : null);
|
|
766
|
-
if (warning !== null)
|
|
767
|
-
appendLines(
|
|
768
|
-
lines,
|
|
769
|
-
renderAllVisualLines(style(context.environment.theme, "warning", warning), context.environment.width),
|
|
770
|
-
);
|
|
771
756
|
return lines;
|
|
772
757
|
}
|
|
773
758
|
|
|
774
759
|
function textOutput(result: AgentToolResult<EvalResultDetails>, showImageFallback: boolean): string {
|
|
775
760
|
const lines: string[] = [];
|
|
776
761
|
for (const part of result.content) {
|
|
777
|
-
if (part.type === "text") lines.push(part.text);
|
|
762
|
+
if (part.type === "text" && part.audience !== "model") lines.push(part.text);
|
|
778
763
|
else if (showImageFallback && part.type === "image") {
|
|
779
764
|
lines.push(`[image: ${sanitizeTerminalLabel(part.mimeType)}]`);
|
|
780
765
|
}
|
|
@@ -884,11 +869,11 @@ function resultMetadata(
|
|
|
884
869
|
return [{ kind: "text", text: style(theme, "muted", metadata.join(" | ")) }];
|
|
885
870
|
}
|
|
886
871
|
|
|
887
|
-
// The call renderer reads the assistant message's raw arguments, which
|
|
888
|
-
// original summary (senpi#1472 detached preparation from the message)
|
|
889
|
-
//
|
|
872
|
+
// The call renderer reads the assistant message's raw arguments, which keep the provider's
|
|
873
|
+
// original summary (senpi#1472 detached preparation from the message), so it normalizes here,
|
|
874
|
+
// idempotently: an already-normalized value is returned unchanged.
|
|
890
875
|
function displaySummary(summary: string | undefined): string | undefined {
|
|
891
|
-
return
|
|
876
|
+
return normalizeEvalSummary(summary);
|
|
892
877
|
}
|
|
893
878
|
|
|
894
879
|
export function renderEvalCall(
|
|
@@ -915,10 +900,16 @@ export function renderEvalCall(
|
|
|
915
900
|
{ kind: "text", text: style(theme, "toolTitle", `eval ${args.language}${reset}${timeout}`) },
|
|
916
901
|
...(displaySummary(args.summary) === undefined
|
|
917
902
|
? []
|
|
918
|
-
: [
|
|
903
|
+
: [summaryBlock(displaySummary(args.summary) ?? "", theme, context.expanded)]),
|
|
919
904
|
{
|
|
920
905
|
kind: "text",
|
|
921
|
-
text: style(
|
|
906
|
+
text: style(
|
|
907
|
+
theme,
|
|
908
|
+
"mdCodeBlock",
|
|
909
|
+
args.code.trim().length > 0
|
|
910
|
+
? displayCode(args.code, args.language, context.argsComplete ? context.invalidate : undefined)
|
|
911
|
+
: "...",
|
|
912
|
+
),
|
|
922
913
|
maxVisualLines: context.expanded ? undefined : CODE_PREVIEW_LINES,
|
|
923
914
|
collapseKind: "code",
|
|
924
915
|
theme,
|
|
@@ -937,6 +928,7 @@ export function renderEvalCall(
|
|
|
937
928
|
width,
|
|
938
929
|
meta: undefined,
|
|
939
930
|
now: renderNow(context),
|
|
931
|
+
...(context.argsComplete ? { repaint: context.invalidate } : {}),
|
|
940
932
|
};
|
|
941
933
|
const cell: EvalCellResult = {
|
|
942
934
|
index: 0,
|
|
@@ -989,6 +981,7 @@ export function renderEvalResult(
|
|
|
989
981
|
width,
|
|
990
982
|
meta: details.meta,
|
|
991
983
|
now: renderNow(context),
|
|
984
|
+
repaint: context.invalidate,
|
|
992
985
|
},
|
|
993
986
|
args: context.args,
|
|
994
987
|
showImageFallback: context.showImages && imageProtocol === null,
|
|
@@ -1006,14 +999,12 @@ export function renderEvalResult(
|
|
|
1006
999
|
const status = resultStatus(details, options, context.isError);
|
|
1007
1000
|
const blocks: RenderBlock[] = [
|
|
1008
1001
|
{ kind: "text", text: resultHeader(details, status, theme) },
|
|
1009
|
-
...(details?.summary === undefined
|
|
1010
|
-
? []
|
|
1011
|
-
: [{ kind: "text" as const, text: style(theme, "muted", details.summary) }]),
|
|
1002
|
+
...(details?.summary === undefined ? [] : [summaryBlock(details.summary, theme, expanded)]),
|
|
1012
1003
|
...resultMetadata(details, options, theme, status),
|
|
1013
1004
|
{ kind: "blank" },
|
|
1014
1005
|
];
|
|
1015
1006
|
const rawOutput = textOutput(result, context.showImages && imageProtocol === null);
|
|
1016
|
-
const output =
|
|
1007
|
+
const output = rawOutput.trimEnd();
|
|
1017
1008
|
const hasRenderedImage =
|
|
1018
1009
|
context.showImages && imageProtocol !== null && result.content.some((part) => part.type === "image");
|
|
1019
1010
|
if (output.length > 0) {
|
|
@@ -1070,10 +1061,6 @@ export function renderEvalResult(
|
|
|
1070
1061
|
const calls = toolCallRows(details);
|
|
1071
1062
|
const nestedCalls = nestedToolCallBlock(details, theme, context.cwd, expanded);
|
|
1072
1063
|
if (calls.length > 0) blocks.push({ kind: "blank" }, nestedCalls ?? { kind: "toolCalls", calls, expanded, theme });
|
|
1073
|
-
if (details?.notice !== undefined)
|
|
1074
|
-
blocks.push({ kind: "blank" }, { kind: "text", text: style(theme, "dim", details.notice) });
|
|
1075
|
-
const warning = formatTruncationWarning(details?.meta) ?? (details?.truncated ? "[eval output truncated]" : null);
|
|
1076
|
-
if (warning !== null) blocks.push({ kind: "blank" }, { kind: "text", text: style(theme, "warning", warning) });
|
|
1077
1064
|
component.setBlocks(blocks);
|
|
1078
1065
|
return component;
|
|
1079
1066
|
}
|
package/src/tool/types.ts
CHANGED
|
@@ -17,8 +17,6 @@ export function enabledLanguageList(enabled: EnabledEvalLanguages): EvalLanguage
|
|
|
17
17
|
return evalLanguageOrder.filter((language) => enabled[language]);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export const EVAL_SUMMARY_MAX_LENGTH = 80;
|
|
21
|
-
|
|
22
20
|
/** The deadlines the schema teaches the model; every number comes from the resolved settings. */
|
|
23
21
|
export interface EvalDeadlineSeconds {
|
|
24
22
|
readonly runBudgetSeconds: number;
|
|
@@ -67,6 +65,12 @@ export type EvalControlInput =
|
|
|
67
65
|
|
|
68
66
|
export type EvalToolRequest = EvalToolInput | EvalControlInput;
|
|
69
67
|
|
|
68
|
+
// Like `summary`, `language` and `code` stay optional in the wire schema because control
|
|
69
|
+
// actions share it; the description teaches the requirement and parseEvalRequest enforces it.
|
|
70
|
+
const LANGUAGE_FIELD_DESCRIPTION =
|
|
71
|
+
"REQUIRED for run. Kernel that runs the cell; each language keeps its own persistent state across eval calls.";
|
|
72
|
+
const CODE_FIELD_DESCRIPTION = "REQUIRED for run. Cell body, verbatim.";
|
|
73
|
+
|
|
70
74
|
function evalInputProperties<Language extends TSchema>(languageSchema: Language, deadlines: EvalDeadlineSeconds) {
|
|
71
75
|
return {
|
|
72
76
|
action: Type.Optional(
|
|
@@ -76,12 +80,11 @@ function evalInputProperties<Language extends TSchema>(languageSchema: Language,
|
|
|
76
80
|
}),
|
|
77
81
|
),
|
|
78
82
|
language: Type.Optional(languageSchema),
|
|
79
|
-
code: Type.Optional(Type.String({ description:
|
|
83
|
+
code: Type.Optional(Type.String({ description: CODE_FIELD_DESCRIPTION })),
|
|
80
84
|
summary: Type.Optional(
|
|
81
85
|
Type.String({
|
|
82
|
-
maxLength: EVAL_SUMMARY_MAX_LENGTH,
|
|
83
86
|
description:
|
|
84
|
-
"REQUIRED for run.
|
|
87
|
+
"REQUIRED for run. One line in the language the user writes in: a progress update saying what you are doing and why, not a label for the code; shown in the TUI while the cell runs.",
|
|
85
88
|
}),
|
|
86
89
|
),
|
|
87
90
|
timeout: Type.Optional(Type.Number({ minimum: 1, description: timeoutFieldDescription(deadlines) })),
|
|
@@ -99,11 +102,15 @@ function evalInputProperties<Language extends TSchema>(languageSchema: Language,
|
|
|
99
102
|
};
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
function evalLanguageUnion(languages: readonly EvalLanguage[]) {
|
|
106
|
+
return Type.Union(
|
|
107
|
+
languages.map((item) => Type.Literal(item)),
|
|
108
|
+
{ description: LANGUAGE_FIELD_DESCRIPTION },
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
102
112
|
const fullEvalInputSchema = Type.Object(
|
|
103
|
-
evalInputProperties(
|
|
104
|
-
Type.Union([Type.Literal("js"), Type.Literal("py"), Type.Literal("rb"), Type.Literal("jl")]),
|
|
105
|
-
defaultEvalDeadlineSeconds,
|
|
106
|
-
),
|
|
113
|
+
evalInputProperties(evalLanguageUnion(evalLanguageOrder), defaultEvalDeadlineSeconds),
|
|
107
114
|
);
|
|
108
115
|
|
|
109
116
|
/** Runtime accepts a discriminated run/control union. */
|
|
@@ -115,10 +122,7 @@ export function createEvalInputSchema(
|
|
|
115
122
|
): EvalInputSchema {
|
|
116
123
|
const languages = enabledLanguageList(enabled);
|
|
117
124
|
if (languages.length === 0) throw new Error("eval requires at least one enabled language");
|
|
118
|
-
const languageSchema =
|
|
119
|
-
languages.length === 1
|
|
120
|
-
? Type.Union([Type.Literal(languages[0])])
|
|
121
|
-
: Type.Union(languages.map((item) => Type.Literal(item)));
|
|
125
|
+
const languageSchema = evalLanguageUnion(languages);
|
|
122
126
|
return Type.Unsafe<EvalToolRequest>(
|
|
123
127
|
Type.Object(evalInputProperties(languageSchema, deadlines), {
|
|
124
128
|
anyOf: [
|