@code-yeongyu/senpi-codemode 2026.7.28-3 → 2026.7.29

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,22 @@
12
12
 
13
13
  ### Removed
14
14
 
15
+ ## [2026.7.29] - 2026-07-29
16
+
17
+ ### Breaking Changes
18
+
19
+ ### Added
20
+
21
+ - Show every live detached eval cell in the interactive footer, using a highlighted `↗ <language> · <title>` status for one cell and a bounded packed summary for multiple cells; clear the status immediately when the final detached cell settles ([#483](https://github.com/code-yeongyu/senpi/pull/483)).
22
+
23
+ ### Changed
24
+
25
+ ### Fixed
26
+
27
+ - Route reserved `agent()`, `output()`, and `tool_schema()` bridge calls from Python and other subprocess kernels through the reserved HTTP handler instead of attempting to execute nonexistent `__agent__`, `__output__`, and `__schema__` tools; ordinary bridge tool calls remain unchanged ([#462](https://github.com/code-yeongyu/senpi/pull/462)).
28
+
29
+ ### Removed
30
+
15
31
  ## [2026.7.28-3] - 2026-07-28
16
32
 
17
33
  ### Breaking Changes
package/README.md CHANGED
@@ -133,6 +133,10 @@ cell keeps only its own language kernel busy. A new same-language call returns
133
133
  a busy error with its cell id and output tail; calls in other languages continue
134
134
  normally. Do not re-run the cell.
135
135
 
136
+ While any cell is detached, the interactive footer shows a highlighted
137
+ `↗ <language> · <title>` status on the extension status line (the cell id when
138
+ the call had no title), clearing as soon as the last detached cell settles.
139
+
136
140
  Use `eval({ action: "peek", cell_id })` for its state and buffered output, or
137
141
  `eval({ action: "stop", cell_id })` to cancel it. Python stop interrupts the
138
142
  existing kernel and preserves variables. JavaScript stop kills and restarts its
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-codemode",
3
- "version": "2026.7.28-3",
3
+ "version": "2026.7.29",
4
4
  "description": "Source-only senpi extension package for codemode evaluation tools",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -29,15 +29,15 @@
29
29
  "access": "public"
30
30
  },
31
31
  "dependencies": {
32
- "@babel/parser": "7.29.7",
33
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@^2026.7.28-3",
34
- "typebox": "1.1.38"
32
+ "@babel/parser": "8.0.4",
33
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@^2026.7.29",
34
+ "typebox": "1.3.8"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "@code-yeongyu/senpi": "*"
38
38
  },
39
39
  "devDependencies": {
40
- "@code-yeongyu/senpi": "2026.7.28-3"
40
+ "@code-yeongyu/senpi": "2026.7.29"
41
41
  },
42
42
  "keywords": [
43
43
  "senpi",
@@ -0,0 +1,44 @@
1
+ import type { EvalDetachedCellStatusEntry } from "../tool/detached-cell-manager.ts";
2
+
3
+ export const EVAL_CELLS_STATUS_KEY = "eval-cells";
4
+
5
+ /** The footer shares one status line with other extensions; keep this brief. */
6
+ const MAX_STATUS_LENGTH = 48;
7
+ /** Same glyph the transcript uses for a detached cell, so the two surfaces read as one state. */
8
+ const DETACHED_GLYPH = "↗";
9
+
10
+ function truncateEnd(text: string, max: number): string {
11
+ if (text.length <= max) return text;
12
+ return `${text.slice(0, Math.max(0, max - 1))}…`;
13
+ }
14
+
15
+ /**
16
+ * Fits as many whole labels as possible into the budget, folding the rest into
17
+ * a `+N more` counter so the detached-cell count is never truncated away.
18
+ */
19
+ function packLabels(labels: readonly string[], budget: number): string {
20
+ for (let kept = labels.length; kept >= 1; kept--) {
21
+ const hiddenCount = labels.length - kept;
22
+ const tail = hiddenCount > 0 ? ` +${hiddenCount} more` : "";
23
+ const joined = labels.slice(0, kept).join(", ");
24
+ if (joined.length + tail.length <= budget) return joined + tail;
25
+ }
26
+ const tail = labels.length > 1 ? ` +${labels.length - 1} more` : "";
27
+ return truncateEnd(labels[0] ?? "", Math.max(1, budget - tail.length)) + tail;
28
+ }
29
+
30
+ function labelOf(entry: EvalDetachedCellStatusEntry): string {
31
+ return entry.title === undefined || entry.title.length === 0 ? entry.cellId : entry.title;
32
+ }
33
+
34
+ /** Brief footer text for the cells still running detached; undefined clears the status. */
35
+ export function formatEvalCellStatus(entries: readonly EvalDetachedCellStatusEntry[]): string | undefined {
36
+ const first = entries[0];
37
+ if (first === undefined) return undefined;
38
+ if (entries.length === 1) {
39
+ const head = `${DETACHED_GLYPH} ${first.language} · `;
40
+ return head + truncateEnd(labelOf(first), MAX_STATUS_LENGTH - head.length);
41
+ }
42
+ const head = `${DETACHED_GLYPH} eval ${entries.length}: `;
43
+ return head + packLabels(entries.map(labelOf), MAX_STATUS_LENGTH - head.length);
44
+ }
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionContext } from "@code-yeongyu/senpi";
2
2
  import type { AgentExecuteTool } from "../bridges/agent-bridge.ts";
3
+ import type { EvalSchemaToolInfo } from "../bridges/schema-bridge.ts";
3
4
  import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
4
5
  import {
5
6
  type CodemodeSettings,
@@ -23,6 +24,7 @@ import {
23
24
  export interface CodemodeRuntimeAPI {
24
25
  readonly executeTool: AgentExecuteTool;
25
26
  getActiveTools(): string[];
27
+ getAllTools(): readonly EvalSchemaToolInfo[];
26
28
  }
27
29
 
28
30
  export interface RuntimeFactoryOptions {
@@ -71,6 +73,7 @@ export async function createRuntime(
71
73
  availability,
72
74
  artifactsDir: artifacts.dir,
73
75
  executeTool,
76
+ listTools: () => pi.getAllTools(),
74
77
  complete,
75
78
  });
76
79
  return {
@@ -2,19 +2,28 @@ import { join } from "node:path";
2
2
  import type { ExtensionContext } from "@code-yeongyu/senpi";
3
3
  import { type BridgeServerHandle, startBridgeServer } from "../bridge/http-server.ts";
4
4
  import type { KernelToHostMessage } from "../bridge/protocol.ts";
5
+ import { isReservedToolName, runReservedTool } from "../bridges/reserved-dispatch.ts";
6
+ import type { EvalSchemaToolInfo } from "../bridges/schema-bridge.ts";
5
7
  import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
6
- import type { CodemodeSettings } from "../config/settings.ts";
8
+ import { type CodemodeSettings, defaultCodemodeSettings } from "../config/settings.ts";
7
9
  import type { InterpreterAvailability } from "../interpreters/detect.ts";
8
10
  import { JuliaKernel } from "../kernels/jl/kernel.ts";
9
11
  import { JavaScriptKernel } from "../kernels/js/context-manager.ts";
10
12
  import { PythonKernel } from "../kernels/py/kernel.ts";
11
13
  import { RubyKernel } from "../kernels/rb/kernel.ts";
14
+ import { marshalToolResult } from "../tool/image.ts";
12
15
  import type { EvalKernel, EvalKernelManager, EvalLanguage, ExecuteTool } from "../tool/types.ts";
13
16
 
14
17
  export interface CodemodeSessionManager extends EvalKernelManager {
15
18
  dispose(): Promise<void>;
16
19
  complete(request: CompletionRequest, ctx: ExtensionContext): Promise<CompletionResult>;
17
20
  setContext?(ctx: ExtensionContext): void;
21
+ bridgeEndpoint?(): BridgeEndpoint;
22
+ }
23
+
24
+ export interface BridgeEndpoint {
25
+ readonly port: number;
26
+ readonly token: string;
18
27
  }
19
28
 
20
29
  export interface EvalExecutionTracker {
@@ -32,6 +41,7 @@ export interface CreateCodemodeSessionManagerOptions {
32
41
  /** Session-adjacent directory used for persisted eval artifacts. */
33
42
  readonly artifactsDir?: string;
34
43
  readonly executeTool: ExecuteTool;
44
+ readonly listTools?: () => readonly EvalSchemaToolInfo[];
35
45
  readonly complete: (request: CompletionRequest, ctx: ExtensionContext) => Promise<CompletionResult>;
36
46
  }
37
47
 
@@ -75,14 +85,34 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
75
85
 
76
86
  async start(): Promise<void> {
77
87
  this.#bridge = await startBridgeServer({
78
- onCall: async (request) =>
79
- this.#options.executeTool(request.toolName, request.args, { signal: request.signal }),
88
+ onCall: async (request) => await this.#call(request),
80
89
  onEmit: async () => undefined,
81
90
  onCompletion: async (request) =>
82
91
  this.#options.complete({ prompt: request.prompt, opts: request.opts }, this.#contextFor(request.signal)),
83
92
  });
84
93
  }
85
94
 
95
+ // Subprocess kernels (py/rb/jl) reach the host only through this route, so reserved
96
+ // helper names must dispatch exactly as the in-process JS path does in tool/cell-handler.ts.
97
+ // Forwarding them to executeTool made agent() fail with "Unknown tool __agent__".
98
+ async #call(request: { toolName: string; args: unknown; callId: string; signal: AbortSignal }): Promise<unknown> {
99
+ if (!isReservedToolName(request.toolName)) {
100
+ return await this.#options.executeTool(request.toolName, request.args, { signal: request.signal });
101
+ }
102
+ const taskTools = this.#options.settings.taskTools ?? defaultCodemodeSettings.taskTools;
103
+ return await runReservedTool(request.toolName, {
104
+ callId: request.callId,
105
+ args: request.args,
106
+ executeTool: this.#options.executeTool,
107
+ taskToolName: taskTools.task,
108
+ taskOutputToolName: taskTools.output,
109
+ listTools: this.#options.listTools,
110
+ signal: request.signal,
111
+ emitStatus: () => {},
112
+ marshalToolResult,
113
+ });
114
+ }
115
+
86
116
  async getKernel(language: EvalLanguage, onMessage: (message: KernelToHostMessage) => void): Promise<EvalKernel> {
87
117
  if (this.#disposePromise) throw new CodemodeSessionDisposedError();
88
118
  // Persistent kernels are reused across cells, but each cell needs its OWN
@@ -109,6 +139,12 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
109
139
  return await this.#options.complete(request, ctx);
110
140
  }
111
141
 
142
+ bridgeEndpoint(): BridgeEndpoint {
143
+ const bridge = this.#bridge;
144
+ if (!bridge) throw new Error("codemode bridge server is not running");
145
+ return { port: bridge.port, token: bridge.token };
146
+ }
147
+
112
148
  setContext(ctx: ExtensionContext): void {
113
149
  this.#context = ctx;
114
150
  }
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import type { EvalSchemaToolInfo } from "./bridges/schema-bridge.ts";
5
5
  import { type CompletionRequest, type CompletionResult, createCompletionHandler } from "./completion/handler.ts";
6
6
  import { defaultCodemodeSettings } from "./config/settings.ts";
7
7
  import { EvalNotifier } from "./extension/eval-notifier.ts";
8
+ import { EVAL_CELLS_STATUS_KEY, formatEvalCellStatus } from "./extension/eval-status.ts";
8
9
  import {
9
10
  createExecuteTool,
10
11
  createRuntime,
@@ -13,7 +14,7 @@ import {
13
14
  } from "./extension/runtime-factory.ts";
14
15
  import type { CodemodeSessionManager, CreateCodemodeSessionManagerOptions } from "./extension/session-manager.ts";
15
16
  import { SessionManagerProxy } from "./extension/session-manager-proxy.ts";
16
- import { EvalDetachedCellManager } from "./tool/detached-cell-manager.ts";
17
+ import { EvalDetachedCellManager, type EvalDetachedCellStatusEntry } from "./tool/detached-cell-manager.ts";
17
18
  import { createEvalTool } from "./tool/eval-tool.ts";
18
19
  import { renderEvalCall, renderEvalResult } from "./tool/render.ts";
19
20
 
@@ -58,6 +59,18 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
58
59
  getContext: () => activeContext,
59
60
  getMode: () => "wake",
60
61
  });
62
+ const showDetachedCells = (entries: readonly EvalDetachedCellStatusEntry[]): void => {
63
+ const ctx = activeContext;
64
+ if (ctx?.ui?.setStatus === undefined) return;
65
+ const status = formatEvalCellStatus(entries);
66
+ const theme = ctx.ui.theme;
67
+ ctx.ui.setStatus(
68
+ EVAL_CELLS_STATUS_KEY,
69
+ status === undefined || ctx.mode !== "tui" || theme === undefined
70
+ ? status
71
+ : theme.bg("selectedBg", theme.fg("text", status)),
72
+ );
73
+ };
61
74
  const registerEvalForRuntime = (
62
75
  runtime: SessionRuntime,
63
76
  modelId: string | undefined,
@@ -101,7 +114,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
101
114
  listTools: () => pi.getAllTools(),
102
115
  complete,
103
116
  settings: defaultCodemodeSettings,
104
- cellManager: new EvalDetachedCellManager({ notifier }),
117
+ cellManager: new EvalDetachedCellManager({ notifier, onStatusChange: showDetachedCells }),
105
118
  executionTracker: manager,
106
119
  renderers,
107
120
  hostLine: hostLine(),
@@ -126,7 +139,11 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
126
139
  if (!replaced) return;
127
140
  notifier.reset();
128
141
  activeContext = ctx;
129
- const cellManager = new EvalDetachedCellManager({ artifactsDir: runtime.artifactsDir, notifier });
142
+ const cellManager = new EvalDetachedCellManager({
143
+ artifactsDir: runtime.artifactsDir,
144
+ notifier,
145
+ onStatusChange: showDetachedCells,
146
+ });
130
147
  activeCells = cellManager;
131
148
  activeRuntime = runtime;
132
149
  activeModelId = ctx.model?.id;
@@ -49,7 +49,7 @@ function parseProgram(code: string): ReturnType<typeof parse> | undefined {
49
49
  allowSuperOutsideMethod: true,
50
50
  allowUndeclaredExports: true,
51
51
  errorRecovery: true,
52
- plugins: ["typescript", "importAttributes"],
52
+ plugins: ["typescript"],
53
53
  });
54
54
  } catch (error) {
55
55
  if (error instanceof SyntaxError) return undefined;
@@ -130,6 +130,11 @@ function rewriteImportDeclaration(node: ImportDeclaration): string {
130
130
  }
131
131
 
132
132
  function dynamicImportEdit(node: AstNode): TextEdit | undefined {
133
+ // Babel 8 parses dynamic import() as an ImportExpression node; Babel 7 used
134
+ // a CallExpression with an Import callee. Handle both shapes.
135
+ if (node.type === "ImportExpression") {
136
+ return { start: node.start, end: node.start + "import".length, text: DYNAMIC_IMPORT_CALLEE };
137
+ }
133
138
  if (node.type !== "CallExpression") return undefined;
134
139
  const callee = nodeFrom(node.value.callee);
135
140
  if (callee?.type !== "Import") return undefined;
@@ -41,9 +41,18 @@ export interface EvalDetachedCellNotifier {
41
41
  notify(cells: readonly EvalDetachedCellNotification[]): void;
42
42
  }
43
43
 
44
+ /** One live detached cell, as shown in the footer status line. */
45
+ export interface EvalDetachedCellStatusEntry {
46
+ readonly cellId: string;
47
+ readonly language: EvalLanguage;
48
+ readonly title?: string;
49
+ }
50
+
44
51
  export interface EvalDetachedCellManagerOptions {
45
52
  readonly artifactsDir?: string;
46
53
  readonly notifier?: EvalDetachedCellNotifier;
54
+ /** Called with every detached cell whenever that set changes; empty clears the status. */
55
+ readonly onStatusChange?: (entries: readonly EvalDetachedCellStatusEntry[]) => void;
47
56
  }
48
57
 
49
58
  /**
@@ -56,6 +65,7 @@ export interface EvalDetachedCellManagerOptions {
56
65
  export class EvalDetachedCellManager {
57
66
  readonly #artifactsDir: string | undefined;
58
67
  readonly #notifier: EvalDetachedCellNotifier | undefined;
68
+ readonly #onStatusChange: ((entries: readonly EvalDetachedCellStatusEntry[]) => void) | undefined;
59
69
  readonly #cells = new Map<string, ManagedCell>();
60
70
  readonly #detachedByLanguage = new Map<EvalLanguage, ManagedCell>();
61
71
  #notificationQueue: ManagedCell[] = [];
@@ -64,6 +74,7 @@ export class EvalDetachedCellManager {
64
74
  constructor(options: EvalDetachedCellManagerOptions = {}) {
65
75
  this.#artifactsDir = options.artifactsDir;
66
76
  this.#notifier = options.notifier;
77
+ this.#onStatusChange = options.onStatusChange;
67
78
  }
68
79
 
69
80
  create(cellId: string, input: EvalToolInput): ManagedCell {
@@ -105,6 +116,7 @@ export class EvalDetachedCellManager {
105
116
  if (!cell.canDetach || !this.#transition(cell, "detached")) return false;
106
117
  cell.wasDetached = true;
107
118
  this.#detachedByLanguage.set(cell.input.language, cell);
119
+ this.#emitStatus();
108
120
  return true;
109
121
  }
110
122
 
@@ -164,11 +176,24 @@ export class EvalDetachedCellManager {
164
176
  if (cell.wasDetached && next !== "detached") {
165
177
  if (this.#detachedByLanguage.get(cell.input.language) === cell)
166
178
  this.#detachedByLanguage.delete(cell.input.language);
179
+ this.#emitStatus();
167
180
  this.#queueNotification(cell);
168
181
  }
169
182
  return true;
170
183
  }
171
184
 
185
+ #emitStatus(): void {
186
+ const emit = this.#onStatusChange;
187
+ if (emit === undefined) return;
188
+ emit(
189
+ [...this.#detachedByLanguage.values()].map((cell) => ({
190
+ cellId: cell.cellId,
191
+ language: cell.input.language,
192
+ ...(cell.input.title === undefined ? {} : { title: cell.input.title }),
193
+ })),
194
+ );
195
+ }
196
+
172
197
  #queueNotification(cell: ManagedCell): void {
173
198
  if (cell.notificationQueued) return;
174
199
  cell.notificationQueued = true;