@ian-pascoe/pi-codemode 0.2.0 → 0.3.1

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/README.md CHANGED
@@ -87,12 +87,18 @@ type CodeModeSessionsResult = {
87
87
  The execute, result, and cancel tools return:
88
88
 
89
89
  ```ts
90
+ type CodeModeConsoleEntry = {
91
+ method: "log" | "info" | "warn" | "error" | "debug";
92
+ text: string;
93
+ };
94
+
90
95
  type CodeModeResult =
91
96
  | {
92
97
  result: "success";
93
98
  sessionId: string;
94
99
  data?: JsonValue;
95
100
  reclaimedSessionId?: string;
101
+ console?: CodeModeConsoleEntry[];
96
102
  }
97
103
  | { result: "pending"; sessionId: string }
98
104
  | {
@@ -112,6 +118,7 @@ type CodeModeResult =
112
118
  | "runtime";
113
119
  message: string;
114
120
  };
121
+ console?: CodeModeConsoleEntry[];
115
122
  };
116
123
  ```
117
124
 
@@ -119,15 +126,41 @@ type CodeModeResult =
119
126
  result text returned to the model. Pi retains additional bounded Presentation
120
127
  Snapshots in tool-result details for Transcript replay and the TUI.
121
128
 
129
+ Cells may call `console.log`, `console.info`, `console.warn`, `console.error`,
130
+ and `console.debug`. One call creates one ordered entry without a trailing
131
+ newline, while embedded newlines remain intact:
132
+
133
+ ```ts
134
+ console.log("answer:", 42);
135
+ return 42;
136
+ ```
137
+
138
+ ```json
139
+ {
140
+ "result": "success",
141
+ "sessionId": "...",
142
+ "data": 42,
143
+ "console": [{ "method": "log", "text": "answer: 42" }]
144
+ }
145
+ ```
146
+
147
+ Formatting matches the pinned Deno Console for format tokens, primitives,
148
+ spacing, and multiline inspection. Getters, coercion hooks, and custom
149
+ inspectors do not run; CodeMode uses safe inspection instead when that differs
150
+ from Deno. Console output arrives only with terminal results. Ordinary script,
151
+ serialization, and worker-reported runtime failures retain prior calls. A
152
+ timeout, cancellation, termination, or process death may omit them because the
153
+ parent kills the worker before it can reply.
154
+
122
155
  ## Transcript and Observer UI
123
156
 
124
157
  The CodeMode Transcript gives all four tools semantic collapsed and expanded
125
158
  rendering. Collapsed rows prioritize Cell lifecycle, a short Session ID, Cell
126
- Ordinal, returned-value shape, nested-tool count, and elapsed time. Expanded
127
- rows show the full Session ID, explicit call arguments, TypeScript source,
128
- structured returned data or error, and bounded nested-tool names, outcomes, and
129
- durations. Nested arguments and raw nested outputs are never copied into the
130
- presentation.
159
+ Ordinal, returned-value shape, Console-call count, nested-tool count, and elapsed
160
+ time. Expanded rows show the full Session ID, explicit call arguments,
161
+ TypeScript source, bounded Console output before structured returned data or the
162
+ error, and bounded nested-tool names, outcomes, and durations. Nested arguments
163
+ and raw nested outputs are never copied into the presentation.
131
164
 
132
165
  Status always uses a symbol and text together:
133
166
 
@@ -138,7 +171,8 @@ Status always uses a symbol and text together:
138
171
 
139
172
  Awaited Cells publish a presentation update immediately and once per second.
140
173
  Collapsed calls show one highlighted line inline or the first eight highlighted
141
- lines of a multi-line Cell. Expanded calls show the complete TypeScript source.
174
+ lines of a multi-line Cell, truncating long lines to the viewport width. Expanded
175
+ calls wrap long lines and show the complete TypeScript source.
142
176
  Returned-data display uses Pi's 2,000-line/50-KB limit; complete oversized data
143
177
  is written to a private Result Spill while the model-facing result remains
144
178
  unchanged. Result Spill files last for the live Pi session. Replayed history
@@ -255,13 +289,16 @@ generated helper source out of ordinary source locations. Every operating-system
255
289
  permission class is denied: filesystem read/write, network, environment, system
256
290
  information, subprocesses, FFI, and remote imports.
257
291
 
258
- Guest code receives ECMAScript built-ins, a read-only `tools` object, and only a
259
- frozen `Deno.version` identity. Raw process and standard-stream access,
260
- `console`, `Worker`, timers, filesystem/network APIs, and module loading are
261
- withheld. The parent watchdog terminates the subprocess for timeout or an
262
- infinite loop. Deno/V8 bounds each Session to a 128 MiB old-space heap and a
263
- 1 MiB stack. Protocol inputs, tool results, and Cell results remain JSON-only
264
- and limited to 8 MiB of UTF-8.
292
+ Guest code receives ECMAScript built-ins, a read-only `tools` object, a frozen
293
+ five-method Console facade, and only a frozen `Deno.version` identity. Raw
294
+ process and standard-stream access, `Worker`, timers, filesystem/network APIs,
295
+ and module loading are withheld. Console calls never write to worker streams and
296
+ do not include output from registered Pi tool handlers. The parent watchdog
297
+ terminates the subprocess for timeout or an infinite loop. Deno/V8 bounds each
298
+ Session to a 128 MiB old-space heap and a 1 MiB stack. Protocol inputs, tool
299
+ results, returned data, and Console entries share one 8 MiB UTF-8 worker-message
300
+ limit. An oversized response becomes the bounded `serialization` failure and
301
+ may omit its Console entries.
265
302
 
266
303
  Registered Pi tools still execute in Pi's parent process with their normal
267
304
  permissions and lifecycle hooks. Cancellation aborts them through Pi's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian-pascoe/pi-codemode",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "private": false,
5
5
  "description": "Process-isolated persistent TypeScript tool composition for Pi",
6
6
  "keywords": [
@@ -0,0 +1,11 @@
1
+ /** Console methods captured from a CodeMode Cell in call order. */
2
+ export const CODEMODE_CONSOLE_METHODS = ["log", "info", "warn", "error", "debug"] as const;
3
+
4
+ /** One supported CodeMode Cell Console method. */
5
+ export type CodeModeConsoleMethod = (typeof CODEMODE_CONSOLE_METHODS)[number];
6
+
7
+ /** One captured CodeMode Cell Console call without a synthetic trailing newline. */
8
+ export type CodeModeConsoleEntry = {
9
+ readonly method: CodeModeConsoleMethod;
10
+ readonly text: string;
11
+ };
@@ -12,6 +12,11 @@ import type {
12
12
  CodeModeObserverSnapshot,
13
13
  CodeModeUnexpectedFailure,
14
14
  } from "./codemode-session-coordinator.js";
15
+ import {
16
+ boundedCodeModeElapsedMs,
17
+ formatCodeModeDuration,
18
+ shortestUniqueCodeModeSessionPrefix,
19
+ } from "./codemode-session-coordinator.js";
15
20
 
16
21
  const CODEMODE_OBSERVER_UI_KEY = "codemode-observer";
17
22
  const CODEMODE_OBSERVER_ROW_LIMIT = 8;
@@ -131,25 +136,12 @@ function shortestUniqueCodeModeSessionPrefixes(
131
136
  const safe = sanitizeCodeModeObserverText(session.sessionId);
132
137
  return safe.length === 0 ? "unknown" : safe;
133
138
  });
134
- return identifiers.map((identifier, index) => {
135
- let length = Math.min(8, identifier.length);
136
- while (
137
- length < identifier.length &&
138
- identifiers.some(
139
- (candidate, candidateIndex) =>
140
- candidateIndex !== index && candidate.startsWith(identifier.slice(0, length)),
141
- )
142
- ) {
143
- length += 1;
144
- }
145
- return identifier.slice(0, length);
146
- });
147
- }
148
-
149
- function boundedElapsedMilliseconds(nowMs: number, startedAtMs: number): number {
150
- const elapsed = Math.round(nowMs - startedAtMs);
151
- if (!Number.isFinite(elapsed)) return 0;
152
- return Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, elapsed));
139
+ return identifiers.map((identifier, index) =>
140
+ shortestUniqueCodeModeSessionPrefix(
141
+ identifier,
142
+ identifiers.filter((_candidate, candidateIndex) => candidateIndex !== index),
143
+ ),
144
+ );
153
145
  }
154
146
 
155
147
  function codeModeTerminalObserverState(
@@ -182,7 +174,7 @@ function projectCodeModeObserverRow(
182
174
  sessionPrefix,
183
175
  state: "running",
184
176
  cellOrdinal: session.current_cell.ordinal,
185
- elapsedMs: boundedElapsedMilliseconds(nowMs, session.current_cell.started_at_ms),
177
+ elapsedMs: boundedCodeModeElapsedMs(session.current_cell.started_at_ms, nowMs),
186
178
  activeToolNames,
187
179
  activeToolCount: Math.max(session.current_cell.active_tool_count, activeToolNames.length),
188
180
  };
@@ -232,13 +224,6 @@ export function buildCodeModeObserverView(
232
224
  };
233
225
  }
234
226
 
235
- function formatCodeModeObserverDuration(elapsedMs: number): string {
236
- if (elapsedMs < 1_000) return `${elapsedMs}ms`;
237
- if (elapsedMs < 60_000) return `${(elapsedMs / 1_000).toFixed(1)}s`;
238
- const seconds = Math.floor(elapsedMs / 1_000);
239
- return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
240
- }
241
-
242
227
  function formatCodeModeObserverToolActivity(
243
228
  row: Extract<CodeModeObserverRow, { state: "running" }>,
244
229
  ): string | undefined {
@@ -256,10 +241,6 @@ function codeModeObserverRowDetail(row: CodeModeObserverRow): string {
256
241
  : `Cell ${row.cellOrdinal}`;
257
242
  }
258
243
 
259
- function joinCodeModeObserverRow(parts: readonly string[], separator: string): string {
260
- return parts.join(separator);
261
- }
262
-
263
244
  function renderCodeModeObserverRow(
264
245
  row: CodeModeObserverRow,
265
246
  width: number,
@@ -270,9 +251,7 @@ function renderCodeModeObserverRow(
270
251
  const status = theme.fg(presentation.color, presentation.label);
271
252
  const detail = theme.fg("muted", codeModeObserverRowDetail(row));
272
253
  const duration =
273
- row.state === "running"
274
- ? theme.fg("muted", formatCodeModeObserverDuration(row.elapsedMs))
275
- : undefined;
254
+ row.state === "running" ? theme.fg("muted", formatCodeModeDuration(row.elapsedMs)) : undefined;
276
255
  const toolActivity =
277
256
  row.state === "running" ? formatCodeModeObserverToolActivity(row) : undefined;
278
257
  const themedToolActivity =
@@ -285,13 +264,13 @@ function renderCodeModeObserverRow(
285
264
  [identity, status],
286
265
  ].map((parts) => parts.filter((part): part is string => part !== undefined));
287
266
  for (const parts of candidates) {
288
- const line = joinCodeModeObserverRow(parts, separator);
267
+ const line = parts.join(separator);
289
268
  if (visibleWidth(line) <= width) return line;
290
269
  }
291
270
 
292
271
  const compactSeparator = theme.fg("dim", CODEMODE_OBSERVER_COMPACT_SEPARATOR_TEXT);
293
272
  const compactIdentity = `${theme.fg(presentation.color, presentation.symbol)} ${theme.bold(row.sessionPrefix)}`;
294
- const compact = joinCodeModeObserverRow([compactIdentity, status], compactSeparator);
273
+ const compact = [compactIdentity, status].join(compactSeparator);
295
274
  if (visibleWidth(compact) <= width) return compact;
296
275
  return truncateToWidth(compact, width, CODEMODE_OBSERVER_ELLIPSIS);
297
276
  }