@code-yeongyu/senpi-codemode 2026.8.16 → 2026.8.17

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 CHANGED
@@ -12,6 +12,20 @@
12
12
 
13
13
  ### Removed
14
14
 
15
+ ## [2026.8.17] - 2026-08-17
16
+
17
+ ### Breaking Changes
18
+
19
+ ### Added
20
+
21
+ - Show exact nested tool-call count and calls-per-second in completed eval TUI headers, using true wall-clock elapsed time for both the visible duration and throughput denominator while preserving kernel-reported timing separately ([#916](https://github.com/code-yeongyu/senpi/pull/916)).
22
+
23
+ ### Changed
24
+
25
+ ### Fixed
26
+
27
+ ### Removed
28
+
15
29
  ## [2026.8.16] - 2026-08-16
16
30
 
17
31
  ### Breaking Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-codemode",
3
- "version": "2026.8.16",
3
+ "version": "2026.8.17",
4
4
  "description": "Source-only senpi extension package for codemode evaluation tools",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -30,14 +30,14 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@babel/parser": "8.0.4",
33
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.8.16",
33
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.8.17",
34
34
  "typebox": "1.3.8"
35
35
  },
36
36
  "peerDependencies": {
37
- "@code-yeongyu/senpi": "2026.8.16"
37
+ "@code-yeongyu/senpi": "2026.8.17"
38
38
  },
39
39
  "devDependencies": {
40
- "@code-yeongyu/senpi": "2026.8.16"
40
+ "@code-yeongyu/senpi": "2026.8.17"
41
41
  },
42
42
  "keywords": [
43
43
  "senpi",
@@ -127,6 +127,8 @@ export class CellResultBuilder {
127
127
  languages: [this.#state.input.language],
128
128
  ...(this.#state.input.summary === undefined ? {} : { summary: this.#state.input.summary }),
129
129
  durationMs: this.#state.durationMs,
130
+ wallDurationMs: Math.max(0, Date.now() - this.#state.startedAt),
131
+ toolCallCount: this.#state.toolCallMetrics.length,
130
132
  toolCalls: [...this.#state.toolCalls],
131
133
  truncated: output?.truncated ?? false,
132
134
  ...(isError ? { isError: true } : {}),
@@ -188,12 +188,18 @@ type RenderEnvironment = {
188
188
  readonly width: number;
189
189
  readonly meta: TruncationMeta | undefined;
190
190
  };
191
- type CellBadges = { readonly reset: boolean; readonly timeout: number | undefined };
191
+ type CellBadges = {
192
+ readonly reset: boolean;
193
+ readonly timeout: number | undefined;
194
+ readonly throughput: CellThroughput | undefined;
195
+ };
196
+ type CellThroughput = { readonly calls: number; readonly wallDurationMs: number | undefined };
192
197
  type PrefixStyle = { readonly prefix: string; readonly continuation: string; readonly color: ThemeColor };
193
198
  type DetailedRenderContext = {
194
199
  readonly environment: RenderEnvironment;
195
200
  readonly args: EvalToolRequest;
196
201
  readonly showImageFallback: boolean;
202
+ readonly isFinal: boolean;
197
203
  };
198
204
 
199
205
  function assertNever(value: never): never {
@@ -256,7 +262,9 @@ function renderPrefixed(text: string, environment: RenderEnvironment, prefixStyl
256
262
  function cellHeader(cell: EvalCellResult, environment: RenderEnvironment, badges: CellBadges): string {
257
263
  const presentation = cellPresentation(cell.status, environment.spinnerFrame);
258
264
  let header = `eval ${cell.language} ${presentation.label} ${presentation.icon}`;
259
- if (cell.durationMs !== undefined) header += ` · ${formatDuration(cell.durationMs)}`;
265
+ if (badges.throughput !== undefined) header += ` · ${formatThroughputBadge(badges.throughput)}`;
266
+ const elapsedMs = badges.throughput?.wallDurationMs ?? cell.durationMs;
267
+ if (elapsedMs !== undefined) header += ` · ${formatDuration(elapsedMs)}`;
260
268
  if (badges.reset) header += " · reset";
261
269
  if (badges.timeout !== undefined) header += ` · timeout ${badges.timeout}s`;
262
270
  return style(environment.theme, presentation.color, header);
@@ -283,6 +291,19 @@ function plural(count: number, singular: string, pluralNoun: string): string {
283
291
  return `${count} ${count === 1 ? singular : pluralNoun}`;
284
292
  }
285
293
 
294
+ function formatCallsPerSecond(calls: number, wallDurationMs: number | undefined): string {
295
+ const seconds = typeof wallDurationMs === "number" && Number.isFinite(wallDurationMs) ? wallDurationMs / 1_000 : 0;
296
+ if (seconds <= 0) return calls === 0 ? "0.00 calls/s" : "n/a calls/s";
297
+ return `${(calls / seconds).toFixed(2)} calls/s`;
298
+ }
299
+
300
+ function formatThroughputBadge(throughput: CellThroughput): string {
301
+ return `${plural(throughput.calls, "call", "calls")} · ${formatCallsPerSecond(
302
+ throughput.calls,
303
+ throughput.wallDurationMs,
304
+ )}`;
305
+ }
306
+
286
307
  function statusIcon(op: string): string {
287
308
  if (op.startsWith("git_")) return "⌁";
288
309
  switch (op) {
@@ -613,9 +634,17 @@ function renderDetailedLines(
613
634
  const cells = details.cells ?? [];
614
635
  for (const [index, cell] of cells.entries()) {
615
636
  const run = isEvalRunInput(context.args) ? context.args : undefined;
637
+ const throughput =
638
+ context.isFinal &&
639
+ cells.length === 1 &&
640
+ cell.status === "complete" &&
641
+ typeof details.toolCallCount === "number"
642
+ ? { calls: details.toolCallCount, wallDurationMs: details.wallDurationMs }
643
+ : undefined;
616
644
  const badges = {
617
645
  reset: index === 0 && run?.reset === true,
618
646
  timeout: index === 0 ? run?.timeout : undefined,
647
+ throughput,
619
648
  };
620
649
  appendLines(lines, renderCell(cell, context.environment, badges));
621
650
  if (index < cells.length - 1) lines.push("");
@@ -754,11 +783,14 @@ function resultMetadata(
754
783
  details: EvalToolDetails | undefined,
755
784
  options: ToolRenderResultOptions,
756
785
  theme: Theme | undefined,
786
+ status: "running" | "done" | "error",
757
787
  ): RenderBlock[] {
758
788
  const metadata: string[] = [];
759
789
  if (details?.phase) metadata.push(`phase ${details.phase}`);
760
- if (!options.isPartial && typeof details?.durationMs === "number")
761
- metadata.push(`took ${formatDuration(details.durationMs)}`);
790
+ const elapsedMs = details?.wallDurationMs ?? details?.durationMs;
791
+ if (!options.isPartial && typeof elapsedMs === "number") metadata.push(`took ${formatDuration(elapsedMs)}`);
792
+ if (status === "done" && typeof details?.toolCallCount === "number")
793
+ metadata.push(formatThroughputBadge({ calls: details.toolCallCount, wallDurationMs: details.wallDurationMs }));
762
794
  if (metadata.length === 0) return [];
763
795
  return [{ kind: "text", text: style(theme, "muted", metadata.join(" | ")) }];
764
796
  }
@@ -814,7 +846,11 @@ export function renderEvalCall(
814
846
  output: "",
815
847
  status: context.spinnerFrame === undefined ? "pending" : "running",
816
848
  };
817
- return renderCell(cell, environment, { reset: args.reset === true, timeout: args.timeout });
849
+ return renderCell(cell, environment, {
850
+ reset: args.reset === true,
851
+ timeout: args.timeout,
852
+ throughput: undefined,
853
+ });
818
854
  },
819
855
  },
820
856
  ]);
@@ -846,6 +882,7 @@ export function renderEvalResult(
846
882
  },
847
883
  args: context.args,
848
884
  showImageFallback: context.showImages && imageProtocol === null,
885
+ isFinal: !options.isPartial,
849
886
  }),
850
887
  },
851
888
  ];
@@ -862,7 +899,7 @@ export function renderEvalResult(
862
899
  ...(details?.summary === undefined
863
900
  ? []
864
901
  : [{ kind: "text" as const, text: style(theme, "muted", details.summary) }]),
865
- ...resultMetadata(details, options, theme),
902
+ ...resultMetadata(details, options, theme, status),
866
903
  { kind: "blank" },
867
904
  ];
868
905
  const rawOutput = textOutput(result, context.showImages && imageProtocol === null);
package/src/tool/types.ts CHANGED
@@ -164,6 +164,10 @@ export interface EvalToolDetails {
164
164
  readonly languages?: readonly EvalLanguage[];
165
165
  readonly summary?: string;
166
166
  readonly durationMs: number;
167
+ /** True wall-clock elapsed time since the cell started; `durationMs` stays kernel-reported. */
168
+ readonly wallDurationMs?: number;
169
+ /** Exact count of initiated nested tool calls, including calls still pending at settlement. */
170
+ readonly toolCallCount?: number;
167
171
  readonly toolCalls: readonly EvalToolCallSummary[];
168
172
  readonly truncated: boolean;
169
173
  readonly isError?: boolean;