@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.
@@ -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 { formatTruncationWarning, stripOutputNotice, type TruncationMeta } from "../output/output-meta.ts";
13
- import { clampEvalSummary } from "./eval-request.ts";
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
- renderPrefixed(style(environment.theme, "muted", cell.summary), environment, {
623
- prefix: "│ ",
624
- continuation: "│ ",
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 = stripOutputNotice(cell.output, environment.meta).trimEnd();
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 now keep the provider's
888
- // original summary (senpi#1472 detached preparation from the message). The advertised display
889
- // limit is enforced here instead, idempotently: an already-clamped value is returned unchanged.
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 clampEvalSummary(summary);
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
- : [{ kind: "text" as const, text: style(theme, "muted", displaySummary(args.summary) ?? "") }]),
903
+ : [summaryBlock(displaySummary(args.summary) ?? "", theme, context.expanded)]),
919
904
  {
920
905
  kind: "text",
921
- text: style(theme, "mdCodeBlock", args.code.trim().length > 0 ? args.code : "..."),
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 = stripOutputNotice(rawOutput, details?.meta).trimEnd();
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: "Cell body, verbatim." })),
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. ONE line in the USER'S conversational language (Korean conversation -> Korean summary) stating WHAT this cell does and FOR WHAT PURPOSE; shown in the TUI while the cell runs. Longer values are force-truncated to 80 chars.",
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: [