@code-yeongyu/senpi-codemode 2026.10.1 → 2026.10.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/src/tool/image.ts CHANGED
@@ -30,6 +30,28 @@ const MAX_DISPLAY_IMAGES_PER_CELL = 8;
30
30
  const MAX_DISPLAY_IMAGE_BYTES_PER_CELL = 24 * 1024 * 1024;
31
31
  const MAX_JSON_OUTPUTS_PER_CELL = 64;
32
32
 
33
+ // Base64 of the signatures of the formats providers accept inline (PNG, JPEG except JPEG-LS, GIF,
34
+ // "RIFF....WEBP"). Signatures start at byte 0, so their encodings are prefixes.
35
+ const IMAGE_SIGNATURES: ReadonlyArray<readonly [string, RegExp]> = [
36
+ ["image/png", /^iVBORw0KGg/],
37
+ ["image/jpeg", /^[/]9j[/](?!9)/],
38
+ ["image/gif", /^R0lGOD[dl]h/],
39
+ ["image/webp", /^UklG.{8}RUJQ/],
40
+ ];
41
+
42
+ // Providers reject the whole request on a bad image, and a kept image block is resent on every later turn,
43
+ // so invalid data is dropped with a reason instead. The detected type wins over the declared one.
44
+ function validateDisplayImage(dataBase64: string): { data: string; mimeType: string } | { reason: string } {
45
+ const data = dataBase64.replace(/\s+/g, "");
46
+ if (data.length === 0 || data.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(data)) {
47
+ return { reason: "the image data is not valid base64 (truncated or corrupted?)" };
48
+ }
49
+ const head = data.slice(0, 16);
50
+ const signature = IMAGE_SIGNATURES.find(([, pattern]) => pattern.test(head));
51
+ if (signature === undefined) return { reason: "the image data is not a PNG, JPEG, GIF, or WebP image" };
52
+ return { data, mimeType: signature[0] };
53
+ }
54
+
33
55
  export interface EvalOutputOptions {
34
56
  readonly artifactPath?: string;
35
57
  readonly headBytes: number;
@@ -98,6 +120,11 @@ export class EvalOutputCollector {
98
120
 
99
121
  display(message: DisplayMessage): void {
100
122
  if (message.mimeType.startsWith("image/")) {
123
+ const image = validateDisplayImage(message.dataBase64);
124
+ if ("reason" in image) {
125
+ this.#sink.push(`[display: image dropped \u2014 ${image.reason}]\n`);
126
+ return;
127
+ }
101
128
  if (
102
129
  this.#displayImages.length >= MAX_DISPLAY_IMAGES_PER_CELL ||
103
130
  this.#displayImageBytes + message.dataBase64.length > MAX_DISPLAY_IMAGE_BYTES_PER_CELL
@@ -105,8 +132,8 @@ export class EvalOutputCollector {
105
132
  this.#displayImagesElided++;
106
133
  return;
107
134
  }
108
- this.#displayImages.push({ type: "image", mimeType: message.mimeType, data: message.dataBase64 });
109
- this.#displayImageBytes += message.dataBase64.length;
135
+ this.#displayImages.push({ type: "image", mimeType: image.mimeType, data: image.data });
136
+ this.#displayImageBytes += image.data.length;
110
137
  return;
111
138
  }
112
139
  const text = Buffer.from(message.dataBase64, "base64").toString("utf8");
@@ -73,9 +73,9 @@ const TOOL_CALL_PREVIEW_COUNT = 5;
73
73
  const TOOL_CALL_COLLAPSED_VISUAL_LINES = 4;
74
74
  const TOOL_CALL_COLLAPSED_ERROR_CODE_POINTS = 512;
75
75
  const TOOL_ERROR_OMISSION_MARKER = "[tool error omitted]";
76
- const LIVE_ELAPSED_TICK_MS = 1_000;
76
+ const LIVE_RENDER_TICK_MS = 100;
77
77
  // A live row repaints on every tick, so this many ticks without a render means the row is gone.
78
- const LIVE_TICKER_MAX_IDLE_TICKS = 60;
78
+ const LIVE_TICKER_MAX_IDLE_TICKS = 600;
79
79
 
80
80
  class PlainTextComponent implements EvalRenderComponent {
81
81
  #blocks: readonly RenderBlock[] = [];
@@ -92,7 +92,7 @@ class PlainTextComponent implements EvalRenderComponent {
92
92
  * The host only animates tool rows for streaming args, `task`, and results carrying
93
93
  * `details.progress`; an eval row matches none of them, so nothing repaints it between
94
94
  * update events. While a cell is non-terminal this drives the repaint itself so the
95
- * header's elapsed time advances, and it emits no tool updates or RPC traffic. Detached
95
+ * header's spinner and elapsed time advance, with no tool updates or RPC traffic. Detached
96
96
  * and terminal cards never arm it, and a ticker whose row stopped rendering (transcript
97
97
  * rebuild, session switch) stops itself after LIVE_TICKER_MAX_IDLE_TICKS and rearms on
98
98
  * the next render, so dropped rows cannot accumulate intervals.
@@ -115,7 +115,7 @@ class PlainTextComponent implements EvalRenderComponent {
115
115
 
116
116
  #armTicker(): void {
117
117
  if (this.#ticker !== undefined || this.#invalidate === undefined) return;
118
- this.#ticker = setInterval(() => this.#tick(), LIVE_ELAPSED_TICK_MS);
118
+ this.#ticker = setInterval(() => this.#tick(), LIVE_RENDER_TICK_MS);
119
119
  this.#ticker.unref?.();
120
120
  }
121
121
 
@@ -336,7 +336,10 @@ function cellElapsedMs(cell: EvalCellResult, environment: RenderEnvironment): nu
336
336
  }
337
337
 
338
338
  function cellHeader(cell: EvalCellResult, environment: RenderEnvironment, badges: CellBadges): string {
339
- const presentation = cellPresentation(cell.status, environment.spinnerFrame);
339
+ const presentation = cellPresentation(
340
+ cell.status,
341
+ environment.spinnerFrame ?? Math.floor((cellElapsedMs(cell, environment) ?? 0) / LIVE_RENDER_TICK_MS),
342
+ );
340
343
  const runtimeBadge = cell.runtime === undefined ? "" : ` (${formatRuntimeBadge(cell.language, cell.runtime)})`;
341
344
  let header = `eval ${cell.language}${runtimeBadge} ${presentation.label} ${presentation.icon}`;
342
345
  if (cell.queuedBehind !== undefined && cell.queuedBehind.length > 0)
@@ -207,8 +207,8 @@ async function executeCell(
207
207
  };
208
208
  try {
209
209
  const kernel = await execution.wait(options.kernelManager.getKernel(invocation.input.language, onMessage));
210
- // Computed before the handler so the cell's capability can be entered per host tool call from the
211
- // worker's message loop, which runs outside the `kernelToolsStorage.run` context below (#1754).
210
+ // Computed before the handler so its construction-time snapshot captures this cell's capability.
211
+ // Worker messages later restore that snapshot before calling host tools (#1754, #2512).
212
212
  const kernelTools = jsKernelTools(kernel, invocation.input.language);
213
213
  const runBound = async (): Promise<AgentToolResult<EvalToolDetails>> => {
214
214
  const queue = kernel.queueSnapshot();
package/src/tool/types.ts CHANGED
@@ -66,8 +66,7 @@ export type EvalControlInput =
66
66
 
67
67
  export type EvalToolRequest = EvalToolInput | EvalControlInput;
68
68
 
69
- // Like `summary`, `language` and `code` stay optional in the wire schema because control
70
- // actions share it; the description teaches the requirement and parseEvalRequest enforces it.
69
+ // Run fields are optional at the root for control calls, but required in the run branch.
71
70
  const LANGUAGE_FIELD_DESCRIPTION =
72
71
  "REQUIRED for run. Kernel that runs the cell; each language keeps its own persistent state across eval calls.";
73
72
  const CODE_FIELD_DESCRIPTION = "REQUIRED for run. Cell body, verbatim.";
@@ -124,11 +123,20 @@ export function createEvalInputSchema(
124
123
  const languages = enabledLanguageList(enabled);
125
124
  if (languages.length === 0) throw new Error("eval requires at least one enabled language");
126
125
  const languageSchema = evalLanguageUnion(languages);
126
+ const properties = evalInputProperties(languageSchema, deadlines);
127
127
  return Type.Unsafe<EvalToolRequest>(
128
- Type.Object(evalInputProperties(languageSchema, deadlines), {
128
+ Type.Object(properties, {
129
+ // Keep branches self-contained for Mistral-hosted GLM (#2240).
129
130
  anyOf: [
130
- { properties: { action: { enum: ["run", "list"] } } },
131
- { properties: { action: { enum: ["peek", "stop"] } }, required: ["action", "cell_id"] },
131
+ Type.Object(
132
+ { ...properties, action: Type.Optional(Type.Literal("run")) },
133
+ { required: ["language", "code", "summary"] },
134
+ ),
135
+ Type.Object({ action: Type.Literal("list") }),
136
+ Type.Object(
137
+ { action: Type.Union([Type.Literal("peek"), Type.Literal("stop")]), cell_id: properties.cell_id },
138
+ { required: ["action", "cell_id"] },
139
+ ),
132
140
  ],
133
141
  }),
134
142
  ) as EvalInputSchema;