@ian-pascoe/pi-codemode 0.1.0

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.
@@ -0,0 +1,517 @@
1
+ import type { ExtensionContext, Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ stripTerminalSequences,
4
+ truncateToWidth,
5
+ visibleWidth,
6
+ type Component,
7
+ type TUI,
8
+ } from "@earendil-works/pi-tui";
9
+ import type { CodeModeRuntime, CodeModeTimerHandle } from "./codemode-runtime.js";
10
+ import type {
11
+ CodeModeObservedSession,
12
+ CodeModeObserverSnapshot,
13
+ CodeModeUnexpectedFailure,
14
+ } from "./codemode-session-coordinator.js";
15
+
16
+ const CODEMODE_OBSERVER_UI_KEY = "codemode-observer";
17
+ const CODEMODE_OBSERVER_ROW_LIMIT = 8;
18
+ const CODEMODE_OBSERVER_TOOL_NAME_LIMIT = 3;
19
+ const CODEMODE_OBSERVER_TOOL_NAME_WIDTH = 32;
20
+ const CODEMODE_OBSERVER_REFRESH_MS = 1_000;
21
+ const CODEMODE_OBSERVER_COOLDOWN_MS = 10_000;
22
+ const CODEMODE_OBSERVER_SEPARATOR_TEXT = " · ";
23
+ const CODEMODE_OBSERVER_COMPACT_SEPARATOR_TEXT = " · ";
24
+ const CODEMODE_OBSERVER_ELLIPSIS = "…";
25
+
26
+ /** Theme operations used by the read-only CodeMode Observer widget. */
27
+ export type CodeModeObserverWidgetTheme = Pick<Theme, "bold" | "fg">;
28
+
29
+ /** Explicit clock and timer capabilities owned by the CodeMode Observer UI controller. */
30
+ export type CodeModeObserverUiRuntime = Pick<
31
+ CodeModeRuntime,
32
+ "now" | "setTimeout" | "clearTimeout"
33
+ >;
34
+
35
+ type CodeModeObserverWidgetTui = Pick<TUI, "requestRender">;
36
+ type CodeModeObserverWidgetFactory = (
37
+ tui: CodeModeObserverWidgetTui,
38
+ theme: CodeModeObserverWidgetTheme,
39
+ ) => Component;
40
+
41
+ /** Minimal TUI surface accepted structurally from one Pi ExtensionContext. */
42
+ export type CodeModeObserverUiContext = {
43
+ readonly mode: ExtensionContext["mode"];
44
+ readonly ui: {
45
+ readonly theme: CodeModeObserverWidgetTheme;
46
+ readonly notify: (message: string, level: "info" | "warning" | "error") => void;
47
+ readonly setStatus: (key: string, status: string | undefined) => void;
48
+ readonly setWidget: (
49
+ key: string,
50
+ widget: CodeModeObserverWidgetFactory | undefined,
51
+ options?: { readonly placement: "aboveEditor" },
52
+ ) => void;
53
+ };
54
+ };
55
+
56
+ /** One responsive CodeMode Observer row with lifecycle-specific detail. */
57
+ export type CodeModeObserverRow = {
58
+ /** Shortest unique Session prefix among currently visible Observer rows. */
59
+ readonly sessionPrefix: string;
60
+ } & (
61
+ | {
62
+ readonly state: "running";
63
+ /** One-based Cell Ordinal within the Session. */
64
+ readonly cellOrdinal: number;
65
+ /** Current Cell duration in parent wall-clock milliseconds. */
66
+ readonly elapsedMs: number;
67
+ /** At most three sanitized active registered-tool names. */
68
+ readonly activeToolNames: readonly string[];
69
+ /** Exact active nested-call count, including omitted names. */
70
+ readonly activeToolCount: number;
71
+ }
72
+ /** Idle Session summary retained only during relevant Observer activity. */
73
+ | { readonly state: "idle"; readonly cellCount: number }
74
+ | {
75
+ readonly state: "failed" | "cancelled" | "timed_out";
76
+ /** Most recent one-based Cell Ordinal, or Session Cell count when no final Cell exists. */
77
+ readonly cellOrdinal: number;
78
+ }
79
+ );
80
+
81
+ /** Bounded read-only projection rendered by the CodeMode Observer widget and footer. */
82
+ export type CodeModeObserverView = {
83
+ /** Exact count of Sessions with a running Cell. */
84
+ readonly runningCount: number;
85
+ /** Exact non-terminal Session count represented by the current snapshot. */
86
+ readonly liveCount: number;
87
+ /** At most eight responsive Observer rows. */
88
+ readonly rows: readonly CodeModeObserverRow[];
89
+ /** Relevant Sessions omitted after the eight-row cap. */
90
+ readonly overflowCount: number;
91
+ };
92
+
93
+ type CodeModeObserverState = CodeModeObserverRow["state"];
94
+ type CodeModeObserverStatePresentation = {
95
+ readonly symbol: string;
96
+ readonly label: string;
97
+ readonly color: ThemeColor;
98
+ };
99
+
100
+ const CODEMODE_OBSERVER_STATE_PRESENTATION = {
101
+ running: { symbol: "◉", label: "running", color: "accent" },
102
+ idle: { symbol: "○", label: "idle", color: "muted" },
103
+ failed: { symbol: "×", label: "failed", color: "error" },
104
+ cancelled: { symbol: "■", label: "cancelled", color: "warning" },
105
+ timed_out: { symbol: "!", label: "timed out", color: "error" },
106
+ } satisfies Record<CodeModeObserverState, CodeModeObserverStatePresentation>;
107
+
108
+ function sanitizeCodeModeObserverText(value: string): string {
109
+ return (
110
+ stripTerminalSequences(value)
111
+ .replaceAll("\r\n", " ")
112
+ .replaceAll("\r", " ")
113
+ .replaceAll("\n", " ")
114
+ .replaceAll("\t", " ")
115
+ .replace(/\s+/g, " ")
116
+ .trim()
117
+ // oxlint-disable-next-line eslint/no-control-regex -- SAFETY: Observer text must remove terminal C0/C1 controls after preserving ordinary spacing above.
118
+ .replace(/[\u0000-\u001f\u007f-\u009f]/g, "")
119
+ );
120
+ }
121
+
122
+ function boundedCodeModeObserverText(value: string, width: number): string {
123
+ return truncateToWidth(sanitizeCodeModeObserverText(value), width, CODEMODE_OBSERVER_ELLIPSIS);
124
+ }
125
+
126
+ function shortestUniqueCodeModeSessionPrefixes(
127
+ sessions: readonly CodeModeObservedSession[],
128
+ ): string[] {
129
+ const identifiers = sessions.map((session) => {
130
+ const safe = sanitizeCodeModeObserverText(session.sessionId);
131
+ return safe.length === 0 ? "unknown" : safe;
132
+ });
133
+ return identifiers.map((identifier, index) => {
134
+ let length = Math.min(8, identifier.length);
135
+ while (
136
+ length < identifier.length &&
137
+ identifiers.some(
138
+ (candidate, candidateIndex) =>
139
+ candidateIndex !== index && candidate.startsWith(identifier.slice(0, length)),
140
+ )
141
+ ) {
142
+ length += 1;
143
+ }
144
+ return identifier.slice(0, length);
145
+ });
146
+ }
147
+
148
+ function boundedElapsedMilliseconds(nowMs: number, startedAtMs: number): number {
149
+ const elapsed = Math.round(nowMs - startedAtMs);
150
+ if (!Number.isFinite(elapsed)) return 0;
151
+ return Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, elapsed));
152
+ }
153
+
154
+ function codeModeTerminalObserverState(
155
+ session: CodeModeObservedSession,
156
+ ): Extract<CodeModeObserverState, "failed" | "cancelled" | "timed_out"> {
157
+ if (session.terminal_error_code === "cancellation" || session.last_cell?.state === "cancelled") {
158
+ return "cancelled";
159
+ }
160
+ if (session.terminal_error_code === "timeout" || session.last_cell?.state === "timed_out") {
161
+ return "timed_out";
162
+ }
163
+ return "failed";
164
+ }
165
+
166
+ function projectCodeModeObserverRow(
167
+ session: CodeModeObservedSession,
168
+ sessionPrefix: string,
169
+ nowMs: number,
170
+ ): CodeModeObserverRow {
171
+ if (session.lifecycle === "running" && session.current_cell !== undefined) {
172
+ const activeToolNames = [
173
+ ...new Set(
174
+ session.current_cell.active_tool_names
175
+ .map((name) => boundedCodeModeObserverText(name, CODEMODE_OBSERVER_TOOL_NAME_WIDTH))
176
+ .filter(Boolean),
177
+ ),
178
+ ].slice(0, CODEMODE_OBSERVER_TOOL_NAME_LIMIT);
179
+ return {
180
+ sessionPrefix,
181
+ state: "running",
182
+ cellOrdinal: session.current_cell.ordinal,
183
+ elapsedMs: boundedElapsedMilliseconds(nowMs, session.current_cell.started_at_ms),
184
+ activeToolNames,
185
+ activeToolCount: Math.max(session.current_cell.active_tool_count, activeToolNames.length),
186
+ };
187
+ }
188
+ if (session.lifecycle === "idle") {
189
+ return { sessionPrefix, state: "idle", cellCount: session.cell_count };
190
+ }
191
+ return {
192
+ sessionPrefix,
193
+ state: codeModeTerminalObserverState(session),
194
+ cellOrdinal: session.last_cell?.ordinal ?? session.cell_count,
195
+ };
196
+ }
197
+
198
+ /** Project immutable coordinator facts into the sorted, eight-row CodeMode Observer view. */
199
+ export function buildCodeModeObserverView(
200
+ snapshot: CodeModeObserverSnapshot,
201
+ nowMs: number,
202
+ ): CodeModeObserverView {
203
+ const visibleSessions = snapshot.sessions.filter(
204
+ (session) =>
205
+ session.lifecycle !== "terminal" ||
206
+ nowMs - session.last_activity_at_ms < CODEMODE_OBSERVER_COOLDOWN_MS,
207
+ );
208
+ const prefixes = shortestUniqueCodeModeSessionPrefixes(visibleSessions);
209
+ const candidates = visibleSessions
210
+ .map((session, index) => ({
211
+ session,
212
+ sessionPrefix: prefixes[index] ?? "unknown",
213
+ inputOrder: index,
214
+ }))
215
+ .sort(
216
+ (left, right) =>
217
+ Number(right.session.lifecycle === "running") -
218
+ Number(left.session.lifecycle === "running") ||
219
+ right.session.last_activity_at_ms - left.session.last_activity_at_ms ||
220
+ left.inputOrder - right.inputOrder,
221
+ );
222
+ const rows = candidates
223
+ .slice(0, CODEMODE_OBSERVER_ROW_LIMIT)
224
+ .map(({ session, sessionPrefix }) => projectCodeModeObserverRow(session, sessionPrefix, nowMs));
225
+ return {
226
+ runningCount: visibleSessions.filter((session) => session.lifecycle === "running").length,
227
+ liveCount: visibleSessions.filter((session) => session.lifecycle !== "terminal").length,
228
+ rows,
229
+ overflowCount: Math.max(0, candidates.length - rows.length),
230
+ };
231
+ }
232
+
233
+ function formatCodeModeObserverDuration(elapsedMs: number): string {
234
+ if (elapsedMs < 1_000) return `${elapsedMs}ms`;
235
+ if (elapsedMs < 60_000) return `${(elapsedMs / 1_000).toFixed(1)}s`;
236
+ const seconds = Math.floor(elapsedMs / 1_000);
237
+ return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
238
+ }
239
+
240
+ function formatCodeModeObserverToolActivity(
241
+ row: Extract<CodeModeObserverRow, { state: "running" }>,
242
+ ): string | undefined {
243
+ if (row.activeToolCount === 0) return undefined;
244
+ if (row.activeToolNames.length === 0) {
245
+ return `${row.activeToolCount} tool${row.activeToolCount === 1 ? "" : "s"}`;
246
+ }
247
+ const omitted = Math.max(0, row.activeToolCount - row.activeToolNames.length);
248
+ return `${row.activeToolNames.join(", ")}${omitted === 0 ? "" : ` +${omitted}`}`;
249
+ }
250
+
251
+ function codeModeObserverRowDetail(row: CodeModeObserverRow): string {
252
+ return row.state === "idle"
253
+ ? `${row.cellCount} Cell${row.cellCount === 1 ? "" : "s"}`
254
+ : `Cell ${row.cellOrdinal}`;
255
+ }
256
+
257
+ function joinCodeModeObserverRow(parts: readonly string[], separator: string): string {
258
+ return parts.join(separator);
259
+ }
260
+
261
+ function renderCodeModeObserverRow(
262
+ row: CodeModeObserverRow,
263
+ width: number,
264
+ theme: CodeModeObserverWidgetTheme,
265
+ ): string {
266
+ const presentation = CODEMODE_OBSERVER_STATE_PRESENTATION[row.state];
267
+ const identity = ` ${theme.fg(presentation.color, presentation.symbol)} ${theme.bold(row.sessionPrefix)}`;
268
+ const status = theme.fg(presentation.color, presentation.label);
269
+ const detail = theme.fg("muted", codeModeObserverRowDetail(row));
270
+ const duration =
271
+ row.state === "running"
272
+ ? theme.fg("muted", formatCodeModeObserverDuration(row.elapsedMs))
273
+ : undefined;
274
+ const toolActivity =
275
+ row.state === "running" ? formatCodeModeObserverToolActivity(row) : undefined;
276
+ const themedToolActivity =
277
+ toolActivity === undefined ? undefined : theme.fg("muted", toolActivity);
278
+ const separator = theme.fg("dim", CODEMODE_OBSERVER_SEPARATOR_TEXT);
279
+ const candidates = [
280
+ [identity, status, detail, duration, themedToolActivity],
281
+ [identity, status, detail, duration],
282
+ [identity, status, detail],
283
+ [identity, status],
284
+ ].map((parts) => parts.filter((part): part is string => part !== undefined));
285
+ for (const parts of candidates) {
286
+ const line = joinCodeModeObserverRow(parts, separator);
287
+ if (visibleWidth(line) <= width) return line;
288
+ }
289
+
290
+ const compactSeparator = theme.fg("dim", CODEMODE_OBSERVER_COMPACT_SEPARATOR_TEXT);
291
+ const compactIdentity = `${theme.fg(presentation.color, presentation.symbol)} ${theme.bold(row.sessionPrefix)}`;
292
+ const compact = joinCodeModeObserverRow([compactIdentity, status], compactSeparator);
293
+ if (visibleWidth(compact) <= width) return compact;
294
+ return truncateToWidth(compact, width, CODEMODE_OBSERVER_ELLIPSIS);
295
+ }
296
+
297
+ /** Render ANSI-safe CodeMode Observer widget lines using right-to-left detail degradation. */
298
+ export function renderCodeModeObserverWidgetLines(
299
+ view: CodeModeObserverView,
300
+ width: number,
301
+ theme: CodeModeObserverWidgetTheme,
302
+ ): string[] {
303
+ if (width <= 0) return [];
304
+ const separator = theme.fg("dim", CODEMODE_OBSERVER_SEPARATOR_TEXT);
305
+ const header = [
306
+ theme.fg("toolTitle", theme.bold("CodeMode")),
307
+ view.liveCount > 0 ? theme.fg("muted", `${view.liveCount} live`) : undefined,
308
+ view.runningCount > 0 ? theme.fg("accent", `${view.runningCount} running`) : undefined,
309
+ ]
310
+ .filter((part): part is string => part !== undefined)
311
+ .join(separator);
312
+ const lines = [
313
+ truncateToWidth(header, width, CODEMODE_OBSERVER_ELLIPSIS),
314
+ ...view.rows.map((row) => renderCodeModeObserverRow(row, width, theme)),
315
+ ];
316
+ if (view.overflowCount > 0) {
317
+ lines.push(
318
+ truncateToWidth(
319
+ theme.fg("dim", ` … +${view.overflowCount} more`),
320
+ width,
321
+ CODEMODE_OBSERVER_ELLIPSIS,
322
+ ),
323
+ );
324
+ }
325
+ return lines;
326
+ }
327
+
328
+ class CodeModeObserverWidgetComponent implements Component {
329
+ constructor(
330
+ private view: CodeModeObserverView,
331
+ private readonly tui: CodeModeObserverWidgetTui,
332
+ private readonly theme: CodeModeObserverWidgetTheme,
333
+ ) {}
334
+
335
+ update(view: CodeModeObserverView): void {
336
+ this.view = view;
337
+ this.tui.requestRender();
338
+ }
339
+
340
+ render(width: number): string[] {
341
+ return renderCodeModeObserverWidgetLines(this.view, width, this.theme);
342
+ }
343
+
344
+ invalidate(): void {}
345
+ }
346
+
347
+ /** Own the TUI-only CodeMode Observer widget, live footer, refresh, cooldown, and cleanup. */
348
+ export class CodeModeObserverUiController {
349
+ private disposed = false;
350
+ private widgetMounted = false;
351
+ private latestSnapshot: CodeModeObserverSnapshot = { sessions: [] };
352
+ private currentView: CodeModeObserverView = {
353
+ runningCount: 0,
354
+ liveCount: 0,
355
+ rows: [],
356
+ overflowCount: 0,
357
+ };
358
+ private widgetComponent: CodeModeObserverWidgetComponent | undefined;
359
+ private refreshTimer: CodeModeTimerHandle | undefined;
360
+ private cooldownTimer: CodeModeTimerHandle | undefined;
361
+ private readonly notifiedUnexpectedFailures = new Set<string>();
362
+
363
+ /** Creates an inert non-TUI controller or a live TUI observer using explicit time capabilities. */
364
+ constructor(
365
+ private readonly context: CodeModeObserverUiContext,
366
+ private readonly runtime: CodeModeObserverUiRuntime,
367
+ ) {}
368
+
369
+ /** Apply one immutable coordinator snapshot immediately without controlling any Session. */
370
+ onSnapshotChange(snapshot: CodeModeObserverSnapshot): void {
371
+ if (this.disposed || this.context.mode !== "tui") return;
372
+ this.latestSnapshot = snapshot;
373
+ this.currentView = buildCodeModeObserverView(snapshot, this.runtime.now());
374
+ this.applyCurrentView();
375
+ }
376
+
377
+ /** Notify once when an idle CodeMode worker fails without an active Transcript result. */
378
+ onUnexpectedFailure(failure: CodeModeUnexpectedFailure): void {
379
+ if (
380
+ this.disposed ||
381
+ this.context.mode !== "tui" ||
382
+ this.notifiedUnexpectedFailures.has(failure.sessionId)
383
+ ) {
384
+ return;
385
+ }
386
+ this.notifiedUnexpectedFailures.add(failure.sessionId);
387
+ const safeSessionId = boundedCodeModeObserverText(failure.sessionId, 256) || "unknown";
388
+ const safeMessage = boundedCodeModeObserverText(failure.message, 1_024) || "worker stopped";
389
+ try {
390
+ this.context.ui.notify(
391
+ `CodeMode Session ${safeSessionId} stopped unexpectedly: ${safeMessage}`,
392
+ "error",
393
+ );
394
+ } catch {
395
+ // A non-authoritative notification failure cannot alter CodeMode lifecycle.
396
+ }
397
+ }
398
+
399
+ /** Clear all CodeMode Observer timers and TUI surfaces; repeated calls do nothing. */
400
+ dispose(): void {
401
+ if (this.disposed) return;
402
+ this.disposed = true;
403
+ this.clearRefreshTimer();
404
+ this.clearCooldownTimer();
405
+ if (this.context.mode === "tui") {
406
+ this.setStatus(undefined);
407
+ this.clearWidget();
408
+ }
409
+ this.widgetMounted = false;
410
+ this.widgetComponent = undefined;
411
+ }
412
+
413
+ private applyCurrentView(): void {
414
+ if (this.currentView.rows.length === 0) {
415
+ this.clearRefreshTimer();
416
+ this.clearCooldownTimer();
417
+ this.setStatus(undefined);
418
+ this.hideWidget();
419
+ return;
420
+ }
421
+ this.showWidget();
422
+ if (this.currentView.runningCount > 0) {
423
+ this.clearCooldownTimer();
424
+ this.setStatus(
425
+ [
426
+ this.context.ui.theme.fg("accent", `◉ ${this.currentView.runningCount} running`),
427
+ this.context.ui.theme.fg("muted", `${this.currentView.liveCount} live`),
428
+ ].join(this.context.ui.theme.fg("dim", " · ")),
429
+ );
430
+ this.ensureRefreshTimer();
431
+ return;
432
+ }
433
+ this.clearRefreshTimer();
434
+ this.setStatus(undefined);
435
+ this.ensureCooldownTimer();
436
+ }
437
+
438
+ private showWidget(): void {
439
+ if (this.widgetMounted) {
440
+ try {
441
+ this.widgetComponent?.update(this.currentView);
442
+ } catch {
443
+ // A non-authoritative redraw failure cannot alter CodeMode lifecycle.
444
+ }
445
+ return;
446
+ }
447
+ try {
448
+ this.context.ui.setWidget(
449
+ CODEMODE_OBSERVER_UI_KEY,
450
+ (tui, theme) => {
451
+ this.widgetComponent = new CodeModeObserverWidgetComponent(this.currentView, tui, theme);
452
+ return this.widgetComponent;
453
+ },
454
+ { placement: "aboveEditor" },
455
+ );
456
+ this.widgetMounted = true;
457
+ } catch {
458
+ this.widgetComponent = undefined;
459
+ }
460
+ }
461
+
462
+ private hideWidget(): void {
463
+ if (!this.widgetMounted) return;
464
+ this.clearWidget();
465
+ }
466
+
467
+ private clearWidget(): void {
468
+ try {
469
+ this.context.ui.setWidget(CODEMODE_OBSERVER_UI_KEY, undefined);
470
+ } catch {
471
+ // A non-authoritative widget cleanup failure cannot alter CodeMode cleanup.
472
+ } finally {
473
+ this.widgetMounted = false;
474
+ this.widgetComponent = undefined;
475
+ }
476
+ }
477
+
478
+ private setStatus(status: string | undefined): void {
479
+ try {
480
+ this.context.ui.setStatus(CODEMODE_OBSERVER_UI_KEY, status);
481
+ } catch {
482
+ // A non-authoritative footer failure cannot alter CodeMode lifecycle.
483
+ }
484
+ }
485
+
486
+ private ensureRefreshTimer(): void {
487
+ if (this.refreshTimer !== undefined) return;
488
+ this.refreshTimer = this.runtime.setTimeout(() => {
489
+ this.refreshTimer = undefined;
490
+ if (this.disposed || this.context.mode !== "tui") return;
491
+ this.currentView = buildCodeModeObserverView(this.latestSnapshot, this.runtime.now());
492
+ if (this.currentView.runningCount === 0) return;
493
+ this.showWidget();
494
+ this.ensureRefreshTimer();
495
+ }, CODEMODE_OBSERVER_REFRESH_MS);
496
+ }
497
+
498
+ private clearRefreshTimer(): void {
499
+ if (this.refreshTimer === undefined) return;
500
+ this.runtime.clearTimeout(this.refreshTimer);
501
+ this.refreshTimer = undefined;
502
+ }
503
+
504
+ private ensureCooldownTimer(): void {
505
+ this.clearCooldownTimer();
506
+ this.cooldownTimer = this.runtime.setTimeout(() => {
507
+ this.cooldownTimer = undefined;
508
+ if (!this.disposed) this.hideWidget();
509
+ }, CODEMODE_OBSERVER_COOLDOWN_MS);
510
+ }
511
+
512
+ private clearCooldownTimer(): void {
513
+ if (this.cooldownTimer === undefined) return;
514
+ this.runtime.clearTimeout(this.cooldownTimer);
515
+ this.cooldownTimer = undefined;
516
+ }
517
+ }
@@ -0,0 +1,16 @@
1
+ import { isCodeModeJsonObject, type CodeModeJsonValue } from "./codemode-tool-contract.js";
2
+
3
+ function orderedCodeModePresentationValue(value: CodeModeJsonValue): CodeModeJsonValue {
4
+ if (Array.isArray(value)) return value.map(orderedCodeModePresentationValue);
5
+ if (!isCodeModeJsonObject(value)) return value;
6
+ return Object.fromEntries(
7
+ Object.entries(value)
8
+ .sort(([left], [right]) => left.localeCompare(right))
9
+ .map(([key, entryValue]) => [key, orderedCodeModePresentationValue(entryValue)]),
10
+ );
11
+ }
12
+
13
+ /** Format returned JSON data deterministically for Transcript display and Result Spills. */
14
+ export function formatCodeModePresentationData(value: CodeModeJsonValue): string {
15
+ return JSON.stringify(orderedCodeModePresentationValue(value), undefined, 2);
16
+ }
@@ -0,0 +1,24 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ /** Timer handle returned by the parent Node runtime. */
4
+ export type CodeModeTimerHandle = ReturnType<typeof setTimeout>;
5
+
6
+ /** Parent-runtime capabilities consumed by CodeMode resource owners. */
7
+ export type CodeModeRuntime = {
8
+ /** Produces a candidate identifier for a new CodeMode Session. */
9
+ readonly createSessionId: () => string;
10
+ /** Reads parent wall-clock time in milliseconds since the Unix epoch. */
11
+ readonly now: () => number;
12
+ /** Schedules one parent-side watchdog or process lifecycle deadline. */
13
+ readonly setTimeout: (callback: () => void, delayMs: number) => CodeModeTimerHandle;
14
+ /** Cancels one parent-side deadline. */
15
+ readonly clearTimeout: (handle: CodeModeTimerHandle) => void;
16
+ };
17
+
18
+ /** Production Node clock and UUID capabilities for the Pi extension composition root. */
19
+ export const CODEMODE_SYSTEM_RUNTIME: CodeModeRuntime = {
20
+ createSessionId: randomUUID,
21
+ now: Date.now,
22
+ setTimeout: globalThis.setTimeout,
23
+ clearTimeout: globalThis.clearTimeout,
24
+ };