@llblab/pi-kit 0.1.4 → 0.1.6

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +5 -5
  3. package/node_modules/@llblab/pi-actors/BACKLOG.md +1 -1
  4. package/node_modules/@llblab/pi-actors/CHANGELOG.md +14 -1
  5. package/node_modules/@llblab/pi-actors/README.md +19 -3
  6. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +29 -10
  7. package/node_modules/@llblab/pi-actors/dist/lib/inspector-overlay.d.ts +3 -0
  8. package/node_modules/@llblab/pi-actors/dist/lib/inspector-overlay.js +120 -38
  9. package/node_modules/@llblab/pi-actors/dist/lib/limits.d.ts +1 -0
  10. package/node_modules/@llblab/pi-actors/dist/lib/limits.js +1 -0
  11. package/node_modules/@llblab/pi-actors/dist/lib/observability.js +6 -1
  12. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.d.ts +10 -3
  13. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +109 -52
  14. package/node_modules/@llblab/pi-actors/dist/lib/runs-artifacts.js +24 -4
  15. package/node_modules/@llblab/pi-actors/dist/lib/runtime.d.ts +2 -0
  16. package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +24 -4
  17. package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.d.ts +1 -0
  18. package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.js +3 -2
  19. package/node_modules/@llblab/pi-actors/dist/lib/state-readers.d.ts +5 -1
  20. package/node_modules/@llblab/pi-actors/dist/lib/state-readers.js +30 -3
  21. package/node_modules/@llblab/pi-actors/dist/lib/tools-access.d.ts +1 -0
  22. package/node_modules/@llblab/pi-actors/dist/lib/tools-access.js +13 -11
  23. package/node_modules/@llblab/pi-actors/dist/lib/tools-inspect.js +5 -5
  24. package/node_modules/@llblab/pi-actors/dist/lib/tools-message.d.ts +1 -0
  25. package/node_modules/@llblab/pi-actors/dist/lib/tools-message.js +3 -1
  26. package/node_modules/@llblab/pi-actors/docs/README.md +1 -0
  27. package/node_modules/@llblab/pi-actors/docs/actor-inspector.md +21 -5
  28. package/node_modules/@llblab/pi-actors/docs/async-runs.md +16 -4
  29. package/node_modules/@llblab/pi-actors/docs/command-templates.md +5 -4
  30. package/node_modules/@llblab/pi-actors/docs/inspection.md +83 -0
  31. package/node_modules/@llblab/pi-actors/docs/recipe-library.md +1 -1
  32. package/node_modules/@llblab/pi-actors/docs/template-recipes.md +225 -66
  33. package/node_modules/@llblab/pi-actors/docs/tool-registry.md +24 -3
  34. package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +26 -10
  35. package/node_modules/@llblab/pi-actors/lib/inspector-overlay.ts +161 -33
  36. package/node_modules/@llblab/pi-actors/lib/limits.ts +1 -0
  37. package/node_modules/@llblab/pi-actors/lib/observability.ts +5 -1
  38. package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +128 -50
  39. package/node_modules/@llblab/pi-actors/lib/runs-artifacts.ts +34 -4
  40. package/node_modules/@llblab/pi-actors/lib/runtime.ts +26 -7
  41. package/node_modules/@llblab/pi-actors/lib/session-evidence.ts +7 -2
  42. package/node_modules/@llblab/pi-actors/lib/state-readers.ts +45 -3
  43. package/node_modules/@llblab/pi-actors/lib/tools-access.ts +26 -12
  44. package/node_modules/@llblab/pi-actors/lib/tools-inspect.ts +9 -5
  45. package/node_modules/@llblab/pi-actors/lib/tools-message.ts +8 -1
  46. package/node_modules/@llblab/pi-actors/package.json +1 -1
  47. package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
  48. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
  49. package/node_modules/@llblab/pi-telegram/README.md +1 -1
  50. package/node_modules/@llblab/pi-telegram/docs/architecture.md +3 -3
  51. package/node_modules/@llblab/pi-telegram/index.ts +25 -0
  52. package/node_modules/@llblab/pi-telegram/lib/journal.ts +192 -9
  53. package/node_modules/@llblab/pi-telegram/lib/runtime.ts +52 -2
  54. package/node_modules/@llblab/pi-telegram/lib/status.ts +3 -3
  55. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +5 -1
  56. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  57. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +14 -6
  58. package/package.json +4 -4
package/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  All notable changes to `@llblab/pi-kit` are documented here.
4
4
 
5
+ ## 0.1.6 - 2026-08-22
6
+
7
+ - `Extension Releases`: Advances `@llblab/pi-actors` to `0.50.0` and `@llblab/pi-telegram` to `0.36.10`, bringing their latest released runtime, Skill, and Telegram integration updates into the bundled kit.
8
+ - `Branding`: Standardizes the public package description on the LLB Lab name.
9
+
10
+ ## 0.1.5 - 2026-08-20
11
+
12
+ - `Installation`: Makes the npm installation command version-agnostic so the README stays current across future kit releases.
13
+
5
14
  ## 0.1.4 - 2026-08-20
6
15
 
7
16
  - `Installation`: Updates the npm example to the current kit version and replaces the local-path example with direct installation from the GitHub repository.
package/README.md CHANGED
@@ -1,24 +1,24 @@
1
1
  # @llblab/pi-kit
2
2
 
3
- `@llblab/pi-kit` is the version-pinned LLBLAB extension distribution for [Pi](https://github.com/badlogic/pi-mono). It provides one installable package while each included extension keeps its own repository, release line, and development lifecycle.
3
+ `@llblab/pi-kit` is the version-pinned LLB Lab extension distribution for [Pi](https://github.com/badlogic/pi-mono). It provides one installable package while each included extension keeps its own repository, release line, and development lifecycle.
4
4
 
5
5
  ## Included packages
6
6
 
7
7
  | Package | Version | Resources |
8
8
  | --- | ---: | --- |
9
- | `@llblab/pi-actors` | `0.49.1` | Extension and Skills |
9
+ | `@llblab/pi-actors` | `0.50.0` | Extension and Skills |
10
10
  | `@llblab/pi-codex-usage` | `0.9.3` | Extension |
11
11
  | `@llblab/pi-grow-loop` | `0.7.2` | Extension and Skills |
12
- | `@llblab/pi-telegram` | `0.36.7` | Extension and Skills |
12
+ | `@llblab/pi-telegram` | `0.36.10` | Extension and Skills |
13
13
 
14
14
  Versions are exact by design. Updating an extension does not change an installed kit until this repository explicitly advances that dependency and publishes a new kit version.
15
15
 
16
16
  ## Install
17
17
 
18
- From npm after publication:
18
+ From npm:
19
19
 
20
20
  ```bash
21
- pi install npm:@llblab/pi-kit@0.1.4
21
+ pi install npm:@llblab/pi-kit
22
22
  ```
23
23
 
24
24
  From GitHub:
@@ -1,6 +1,6 @@
1
1
  # Project Backlog
2
2
 
3
- - [ ] `Linux MPRIS media integration`: Expose the active `music-player/playback` singleton as one optional generation-fenced MPRIS2 player so GNOME and compatible desktop shells can show current media and native controls without making D-Bus a second playback authority.
3
+ - [ ] `Future minor — Linux MPRIS media integration`: Expose the active `music-player/playback` singleton as one optional generation-fenced MPRIS2 player so GNOME and compatible desktop shells can show current media and native controls without making D-Bus a second playback authority; keep this feature outside the 0.49.2 and 0.50.0 cohorts.
4
4
  - [ ] Publish `PlaybackStatus`, track metadata, duration, read-time position, volume, and supported capabilities under one stable session-scoped bus identity; disappear cleanly when the Run stops or its generation is replaced, and fail soft when the user D-Bus session is unavailable.
5
5
  - [ ] Map `Play`, `Pause`, `PlayPause`, `Next`, `Previous`, `Stop`, `Seek`, `SetPosition`, and `Volume` back into the existing generation-fenced music-player Control/helper contract rather than signaling the backend or editing Run state directly.
6
6
  - [ ] Validate deterministic D-Bus contract behavior plus a live GNOME smoke showing the media surface, metadata, progress, volume, and controls while preserving backend independence and exact Actor ownership.
@@ -2,7 +2,20 @@
2
2
 
3
3
  > Each release keeps at most 8 outcome records of at most 512 characters.
4
4
 
5
- ## Unreleased
5
+ ## 0.50.0: Hardened Actor Baseline
6
+
7
+ - `Template Recipe Standard`: Rebuilt the Recipe authoring guide around one current-state contract: formats, identity, uniformly detailed file-level and command-node field tables, resolution precedence, imports, async/singleton lifecycle, Control, artifacts, runtime origins, provenance, authoring workflow, and validation. Removed migration history and legacy-reference narration from the normative document.
8
+ - `Documentation Contract Reconciliation`: Audited every human-facing document against package metadata, public schemas, implementation, maintained Skills, and tests. Added the canonical Inspect target/view matrix and exact-owner authorization boundary; completed installation, runtime triage, registered-tool lifecycle/schema, detached-build, and bounded-evidence guidance; corrected lifecycle ownership, command-template shapes, Inspector zero-based navigation/focus behavior, and remaining historical or ambiguous prose.
9
+ - `Owner-Safe Public Runs`: Run-specific Inspect and Control now require a live coordinator identity, reject ownerless and cross-owner state, and keep runtime Run inventory exact-owner filtered. Missing-session and mismatch failures provide supported runtime inspection guidance instead of removed session targets.
10
+ - `Bounded Evidence Inspection`: Session evidence rejects oversized JSONL before materialization with explicit truncation diagnostics, while artifact manifests compute exact size and SHA-256 through fixed-size chunks instead of loading complete artifacts into memory.
11
+ - `Operator Truth`: Runtime status now uses the canonical automatic-review policy for `0`, `false`, and case-insensitive `off`. Maintained guidance documents exact runtime-owned `kill`, `archive`, and artifact-preserving `prune` calls with their running/terminal state constraints.
12
+ - `Consistent Inspector Indexing`: Actor Inspector now uses zero-based numbering for Runs, Trace rows, and complex array items. Structured items render as accent keys such as `#0: {` on one line, removing list bullets and redundant opening-brace rows while preserving nested indentation, commas, wrapping, and object-label grammar.
13
+ - `Inspector Focus Semantics`: Alternating content stripes retain `customMessageBg`, while focused Run controls, tabs, Trace rows, selector options, and confirmation choices use `selectedBg`. Trace focus gains symmetric trailing padding and composes its selected span beside—rather than around—the independent full-row stripe, avoiding nested ANSI background resets.
14
+ - `Layered Inspector Selectors`: Run choices open directly below the focused Run control, and Trace-source choices below the focused tabs. Each menu is ANSI-aware composited over only its bounded rectangle while the underlying tabs, separator, documents, Trace rows, stripes, overlay height, and footer remain rendered and restore unchanged on close. The parent Run control or Trace tab retains `selectedBg` while its child selector is active. Trace-source choices use the compact `Trace: <source>` label, begin twelve cells into the base layer beneath the tab region, and preserve distinct label/value colors under selection. A non-`all` source projects onto the parent tab with the same colon grammar and value color while `Trace:`, including the colon, and its focus brackets retain their established tab accent. Run choices use aligned zero-based sequence, name, and status columns without a redundant `run:` prefix, preserve the same semantic status colors as the parent control, and begin four cells inside the outer frame. Both insets preserve—rather than blank—the underlying base-layer prefix before the composited menu rectangle.
15
+
16
+ ## 0.49.2: Stale-Context Lifecycle Hotfix
17
+
18
+ - `Stale-Context Lifecycle Hotfix`: Contains Run UI animation, delayed watcher, reconciliation, error-handler, retirement, and Recipe-reload callbacks so invalidated Pi contexts cannot escape into the host event loop. Session owners are captured while live, stale shutdown cannot close a replacement session, and parent teardown plus shutdown notification remain identity-fenced and no-throw (GitHub issue #125).
6
19
 
7
20
  ## 0.49.1: Maintained Telegram View Routing
8
21
 
@@ -19,6 +19,8 @@ This topology does not require every task to become a subagent. Short work with
19
19
 
20
20
  ## Install
21
21
 
22
+ Requires Node.js 22.19.0 or newer.
23
+
22
24
  ```bash
23
25
  pi install npm:@llblab/pi-actors
24
26
  ```
@@ -59,7 +61,7 @@ Send one exact Control:
59
61
  }
60
62
  ```
61
63
 
62
- Run targets accept only actions declared by the captured Recipe. Runtime targets accept only reserved review actions:
64
+ Run targets accept Recipe-declared actor-local actions plus runtime-owned `kill`, `archive`, and `prune`, subject to Run state. Runtime targets accept only reserved review actions:
63
65
 
64
66
  ```text
65
67
  message target=runtime action=review.retry input={"scope":"draft"}
@@ -79,12 +81,14 @@ inspect target=run:test view=recipe
79
81
  inspect target=run:test view=trace source=lifecycle lines=40
80
82
  inspect target=run:test view=control
81
83
  inspect target=runtime view=status
84
+ inspect target=runtime view=runs status=failed
85
+ inspect target=runtime view=triage
82
86
  inspect target=recipes view=status
83
87
  inspect target=recipes view=doctor identity=music-player/playback
84
88
  inspect target=tool:my_tool view=status
85
89
  ```
86
90
 
87
- A Run exposes exactly `recipe`, `trace`, and `control` views.
91
+ A Run exposes exactly `recipe`, `trace`, and `control` views. Run-specific `inspect` and `message` require a current coordinator session whose id exactly matches the persisted non-empty Run owner; possessing `run:<id>` alone is not authorization. Missing-session, ownerless, and cross-owner access fails closed. Use `inspect target=runtime view=runs` for the current session's exact-owner inventory. See the complete [management inspection matrix](./docs/inspection.md).
88
92
 
89
93
  ### `register_tool`
90
94
 
@@ -151,7 +155,17 @@ Attention is a live wake hint, not a durable queue: persist recovery state or an
151
155
 
152
156
  ## Control
153
157
 
154
- Controls persist to `controls.jsonl` before transport. One token-owned lock rejects a 65th pending Control or 1 MiB rewrite before admission, fails closed on malformed or stale-generation evidence, and atomically admits one record. Canonical transitions retain a 128-terminal tail, expected-state fencing, and 4 KiB errors. Admitted nonterminal Controls never expire automatically. Every record carries immutable `run_instance_id`. `inspect view=control` reports pending capacity, saturation, stale work, journal bytes, and bounded diagnostics. If a stuck Run is saturated, use runtime-owned `kill`; it bypasses actor-local capacity and creates no synthetic Control.
158
+ Controls persist to `controls.jsonl` before transport. One token-owned lock rejects a 65th pending Control or 1 MiB rewrite before admission, fails closed on malformed or stale-generation evidence, and atomically admits one record. Canonical transitions retain a 128-terminal tail, expected-state fencing, and 4 KiB errors. Admitted nonterminal Controls never expire automatically. Every record carries immutable `run_instance_id`. `inspect view=control` reports pending capacity, saturation, stale work, journal bytes, and bounded diagnostics.
159
+
160
+ Runtime-owned lifecycle and retention actions use the same `message` tool:
161
+
162
+ ```text
163
+ message target=run:<id> action=kill
164
+ message target=run:<id> action=archive
165
+ message target=run:<id> action=prune input={"preserve_artifacts":true}
166
+ ```
167
+
168
+ `kill` accepts only a currently running owned generation, bypasses actor-local capacity, and creates no synthetic Control. `archive` and `prune` accept only terminal owned Runs. Archive moves the entire Run state directory and leaves a tombstone. Prune removes Run state; declared existing artifacts survive only when `preserve_artifacts` is explicitly true, in which case they are copied to retained artifact storage before removal.
155
169
 
156
170
  Long-lived services publish `control-endpoint.json` only when ready:
157
171
 
@@ -222,6 +236,8 @@ npm run validate
222
236
  npm run test:preservation
223
237
  ```
224
238
 
239
+ The build produces the JavaScript runtime used by detached Actor processes in npm installations. Pi can load a TypeScript extension entrypoint, but standalone Node processes cannot type-strip modules under `node_modules`; keeping compiled Run modules preserves process isolation without a runtime TypeScript loader.
240
+
225
241
  See the [documentation index](./docs/README.md), [Run lifecycle](./docs/async-runs.md), and [Recipe library](./docs/recipe-library.md).
226
242
 
227
243
  Project context: [AGENTS.md](./AGENTS.md) · [BACKLOG.md](./BACKLOG.md) · [CHANGELOG.md](./CHANGELOG.md).
@@ -17,6 +17,8 @@ import * as Tools from "./tools.js";
17
17
  import * as ToolsResponse from "./tools-response.js";
18
18
  export function createActorExtensionRuntime(pi) {
19
19
  let activeRunContext;
20
+ let activeRunOwnerId;
21
+ const runOwnerIdsByContext = new WeakMap();
20
22
  const recipeResolutionContextsBySession = new Map();
21
23
  const getRunOwnerId = Pi.getSessionId;
22
24
  const getRecipeResolutionContext = (ctx) => {
@@ -32,9 +34,21 @@ export function createActorExtensionRuntime(pi) {
32
34
  getRunOwnerId,
33
35
  getThinkingLevel: () => pi.getThinkingLevel(),
34
36
  });
35
- const runUiRuntime = RunUiRuntime.createRunUiRuntime({
37
+ let recipeReload;
38
+ let runUiRuntime;
39
+ const closeActiveSessionRuntimes = () => {
40
+ const ownerId = activeRunOwnerId;
41
+ activeRunContext = undefined;
42
+ activeRunOwnerId = undefined;
43
+ runUiRuntime?.close();
44
+ automaticReview.close();
45
+ recipeReload?.close();
46
+ if (ownerId)
47
+ recipeResolutionContextsBySession.delete(ownerId);
48
+ };
49
+ runUiRuntime = RunUiRuntime.createRunUiRuntime({
36
50
  getActiveContext: () => activeRunContext,
37
- getRunOwnerId,
51
+ onCallbackError: closeActiveSessionRuntimes,
38
52
  onRunEvent: automaticReview.schedule,
39
53
  pi,
40
54
  });
@@ -77,10 +91,11 @@ export function createActorExtensionRuntime(pi) {
77
91
  reservedToolNames: Tools.RESERVED_TOOL_NAMES,
78
92
  setActiveTools: (toolNames) => pi.setActiveTools(toolNames),
79
93
  });
80
- const recipeReload = Runtime.createRecipeToolReloadWatcher(runtime, {
94
+ recipeReload = Runtime.createRecipeToolReloadWatcher(runtime, {
81
95
  getResolutionContext: () => activeRunContext
82
96
  ? getRecipeResolutionContext(activeRunContext)
83
97
  : undefined,
98
+ onCallbackError: closeActiveSessionRuntimes,
84
99
  });
85
100
  return {
86
101
  beforeAgentStart(systemPrompt, skills, ctx) {
@@ -102,25 +117,29 @@ export function createActorExtensionRuntime(pi) {
102
117
  automaticReview.schedule();
103
118
  },
104
119
  onSessionShutdown(reason, ctx) {
105
- recipeResolutionContextsBySession.delete(getRunOwnerId(ctx));
106
- activeRunContext = undefined;
107
- automaticReview.close();
108
- recipeReload.close();
109
- runUiRuntime.shutdown(reason, ctx);
120
+ const ownerId = runOwnerIdsByContext.get(ctx);
121
+ runOwnerIdsByContext.delete(ctx);
122
+ if (activeRunContext === ctx)
123
+ closeActiveSessionRuntimes();
124
+ if (ownerId)
125
+ recipeResolutionContextsBySession.delete(ownerId);
126
+ runUiRuntime.shutdown(reason, ownerId, ctx);
110
127
  },
111
128
  async onSessionStart(ctx) {
112
129
  const sessionId = getRunOwnerId(ctx);
113
130
  recipeResolutionContextsBySession.set(sessionId, RecipeResolution.createEmptyRecipeResolutionContext(sessionId, ctx.cwd));
114
131
  ctx.ui.setWidget("zz-pi-actors-comms", undefined);
115
132
  activeRunContext = ctx;
133
+ activeRunOwnerId = sessionId;
134
+ runOwnerIdsByContext.set(ctx, sessionId);
116
135
  runUiRuntime.close();
117
136
  automaticReview.close();
118
137
  recipeReload.close();
119
138
  await Temp.prepareExtensionTempDir(Paths.EXTENSION_RUNTIME_PATHS.tempDir);
120
- if (activeRunContext !== ctx)
139
+ if (activeRunContext !== ctx || activeRunOwnerId !== sessionId)
121
140
  return;
122
141
  automaticReview.start(ctx);
123
- runUiRuntime.start(ctx);
142
+ runUiRuntime.start(ctx, sessionId);
124
143
  recipeReload.watch(ctx);
125
144
  },
126
145
  registerCoreTools() {
@@ -32,6 +32,7 @@ export declare class ActorInspectorOverlay {
32
32
  private readonly theme;
33
33
  private readonly tui;
34
34
  private readonly refreshTimer;
35
+ private contentSelectedRows;
35
36
  private contentStripeIndices;
36
37
  private documentScroll;
37
38
  private detailScroll;
@@ -71,6 +72,7 @@ export declare class ActorInspectorOverlay {
71
72
  private renderTabs;
72
73
  private renderContent;
73
74
  private renderSelector;
75
+ private runSelectorOptions;
74
76
  private renderMenuBox;
75
77
  private renderTraceList;
76
78
  private renderTraceDetail;
@@ -91,6 +93,7 @@ export declare class ActorInspectorOverlay {
91
93
  private renderKeyHints;
92
94
  private statusColor;
93
95
  private footerBorder;
96
+ private compositeMenuLine;
94
97
  private stripeBackground;
95
98
  private stripedRow;
96
99
  private row;
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { readFileSync, statSync } from "node:fs";
7
7
  import * as path from "node:path";
8
- import { matchesKey, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
8
+ import { matchesKey, sliceByColumn, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
9
9
  import * as ActorInspector from "./inspector.js";
10
10
  import * as ControlProjection from "./control-projection.js";
11
11
  import * as RunsControls from "./runs-controls.js";
@@ -35,6 +35,7 @@ export class ActorInspectorOverlay {
35
35
  theme;
36
36
  tui;
37
37
  refreshTimer;
38
+ contentSelectedRows = new Set();
38
39
  contentStripeIndices = [];
39
40
  documentScroll = 0;
40
41
  detailScroll = 0;
@@ -219,9 +220,10 @@ export class ActorInspectorOverlay {
219
220
  if (this.killConfirmation)
220
221
  return this.renderKillDialog(innerWidth);
221
222
  const lines = [this.border("╭", " Actor Inspector ", "╮", innerWidth)];
222
- lines.push(this.row(this.renderRunControl(run, runs.length - this.runIndex), innerWidth));
223
+ lines.push(this.row(this.renderRunControl(run, runs.length - this.runIndex - 1), innerWidth));
223
224
  lines.push(this.row(this.renderTabs(run), innerWidth));
224
225
  lines.push(this.border("├", "", "┤", innerWidth));
226
+ this.contentSelectedRows.clear();
225
227
  this.contentStripeIndices = [];
226
228
  let content = this.renderContent(run, innerWidth);
227
229
  if (this.feedback) {
@@ -231,14 +233,28 @@ export class ActorInspectorOverlay {
231
233
  ? "error"
232
234
  : "warning";
233
235
  content = [this.theme.fg(color, ` ${this.feedback.message}`), ...content];
236
+ this.contentSelectedRows = new Set([...this.contentSelectedRows].map((index) => index + 1));
234
237
  this.contentStripeIndices = [0, ...this.contentStripeIndices];
235
238
  }
236
239
  const viewportRows = this.contentViewportRows();
237
240
  content = content.slice(0, viewportRows);
238
241
  for (let index = 0; index < viewportRows; index += 1) {
239
- lines.push(this.stripedRow(content[index] ?? "", innerWidth, this.contentStripeIndices[index] ?? index));
242
+ lines.push(this.stripedRow(content[index] ?? "", innerWidth, this.contentStripeIndices[index] ?? index, this.contentSelectedRows.has(index)));
240
243
  }
241
244
  lines.push(this.footerBorder(this.renderKeyHints(), innerWidth));
245
+ if (this.focus === "select") {
246
+ const runSelector = this.selectorMode === "run";
247
+ const startRow = runSelector ? 2 : 3;
248
+ const horizontalOffset = runSelector ? 4 : 12;
249
+ const availableRows = Math.max(3, lines.length - startRow - 1);
250
+ const menu = this.renderSelector(run, availableRows);
251
+ for (const [offset, menuLine] of menu.entries()) {
252
+ const target = startRow + offset;
253
+ if (target >= lines.length - 1)
254
+ break;
255
+ lines[target] = this.compositeMenuLine(lines[target], menuLine, innerWidth, horizontalOffset);
256
+ }
257
+ }
242
258
  return lines;
243
259
  }
244
260
  get tab() {
@@ -399,10 +415,10 @@ export class ActorInspectorOverlay {
399
415
  renderKillDialog(innerWidth) {
400
416
  const confirmation = this.killConfirmation;
401
417
  const cancel = this.killDialogChoice === "cancel"
402
- ? this.theme.bg("customMessageBg", this.theme.fg("accent", " Cancel "))
418
+ ? this.theme.bg("selectedBg", this.theme.fg("accent", " Cancel "))
403
419
  : this.theme.fg("muted", " Cancel ");
404
420
  const kill = this.killDialogChoice === "kill"
405
- ? this.theme.bg("customMessageBg", this.theme.fg("error", " Kill actor "))
421
+ ? this.theme.bg("selectedBg", this.theme.fg("error", " Kill actor "))
406
422
  : this.theme.fg("error", " Kill actor ");
407
423
  const body = [
408
424
  "",
@@ -433,30 +449,36 @@ export class ActorInspectorOverlay {
433
449
  const suffix = active ? this.theme.fg("accent", " → ") : " ";
434
450
  if (!run) {
435
451
  const empty = this.theme.fg("muted", `${prefix}Run: none${suffix}`);
436
- return this.focus === "runs" ? this.theme.bg("customMessageBg", empty) : empty;
452
+ return active ? this.theme.bg("selectedBg", empty) : empty;
437
453
  }
438
454
  const value = `${prefix}${this.theme.fg("muted", "Run: ")}${this.theme.fg("text", `#${sequence}`)} ${this.theme.fg("accent", run.run)} ${this.theme.fg(this.statusColor(run.status), run.status)}${suffix}`;
439
- return this.focus === "runs" ? this.theme.bg("customMessageBg", value) : value;
455
+ return active ? this.theme.bg("selectedBg", value) : value;
440
456
  }
441
457
  renderTabs(run) {
442
458
  const source = this.traceSource(run);
443
459
  return TABS.map((tab, index) => {
444
460
  const base = `${tab[0].toUpperCase()}${tab.slice(1)}`;
445
- const label = tab === "trace" && source !== "all" ? `${base} (${source})` : base;
461
+ const label = tab === "trace" && source !== "all" ? `${base}: ${source}` : base;
446
462
  const selected = index === this.tabIndex;
447
- const display = selected && this.focus !== "runs" ? `[ ${label} ]` : ` ${label} `;
448
- const value = ` ${display} `;
449
- if (!selected)
450
- return this.theme.fg("muted", value);
451
- const colored = this.theme.fg("accent", value);
452
- return this.focus === "tabs" ? this.theme.bg("customMessageBg", colored) : colored;
463
+ const bracketed = selected && this.focus !== "runs";
464
+ if (!selected) {
465
+ const display = bracketed ? `[ ${label} ]` : ` ${label} `;
466
+ return this.theme.fg("muted", ` ${display} `);
467
+ }
468
+ const coloredLabel = tab === "trace" && source !== "all"
469
+ ? `${this.theme.fg("accent", `${base}:`)} ${this.theme.fg("text", source)}`
470
+ : this.theme.fg("accent", label);
471
+ const colored = bracketed
472
+ ? ` ${this.theme.fg("accent", "[ ")}${coloredLabel}${this.theme.fg("accent", " ]")} `
473
+ : ` ${coloredLabel} `;
474
+ const active = this.focus === "tabs"
475
+ || (this.focus === "select" && this.selectorMode === "source");
476
+ return active ? this.theme.bg("selectedBg", colored) : colored;
453
477
  }).join(" ");
454
478
  }
455
479
  renderContent(run, width) {
456
480
  if (!run)
457
481
  return [this.theme.fg("muted", " No owned actor runs")];
458
- if (this.focus === "select")
459
- return this.renderSelector(run);
460
482
  if (this.focus === "detail")
461
483
  return this.renderTraceDetail(width);
462
484
  if (this.tab === "trace")
@@ -471,30 +493,49 @@ export class ActorInspectorOverlay {
471
493
  this.contentStripeIndices = projection.stripes.slice(this.documentScroll, this.documentScroll + this.contentViewportRows());
472
494
  return projection.lines.slice(this.documentScroll, this.documentScroll + this.contentViewportRows());
473
495
  }
474
- renderSelector(run) {
496
+ renderSelector(run, availableRows) {
475
497
  const options = this.selectorMode === "run"
476
- ? this.runs().map((item) => `run:${item.run} ${item.status}`)
477
- : this.traceSources(run).map((source) => `Trace source: ${source}`);
478
- const lines = this.renderMenuBox(options, this.selectorIndex);
479
- this.contentStripeIndices = lines.map(() => 0);
480
- return lines;
481
- }
482
- renderMenuBox(options, focusedIndex) {
498
+ ? this.runSelectorOptions()
499
+ : this.traceSources(run).map((source) => ({
500
+ content: `${this.theme.fg("accent", "Trace:")} ${this.theme.fg("text", source)}`,
501
+ preserveColors: true,
502
+ }));
503
+ return this.renderMenuBox(options, this.selectorIndex, availableRows);
504
+ }
505
+ runSelectorOptions() {
506
+ const runs = this.runs();
507
+ const sequenceWidth = Math.max(2, ...runs.map((_item, index) => `#${runs.length - index - 1}`.length));
508
+ const runWidth = Math.max(1, ...runs.map((item) => visibleWidth(item.run)));
509
+ return runs.map((item, index) => ({
510
+ content: [
511
+ this.theme.fg("text", this.fit(`#${runs.length - index - 1}`, sequenceWidth)),
512
+ this.theme.fg("accent", this.fit(item.run, runWidth)),
513
+ this.theme.fg(this.statusColor(item.status), item.status),
514
+ ].join(" "),
515
+ preserveColors: true,
516
+ }));
517
+ }
518
+ renderMenuBox(options, focusedIndex, availableRows) {
483
519
  if (options.length === 0)
484
520
  return [this.theme.fg("muted", " No options")];
485
- const visibleLimit = Math.max(1, Math.min(this.contentViewportRows(), options.length));
521
+ const visibleLimit = Math.max(1, Math.min(availableRows - 2, options.length));
486
522
  const maxStart = Math.max(0, options.length - visibleLimit);
487
523
  const start = Math.max(0, Math.min(focusedIndex - Math.floor(visibleLimit / 2), maxStart));
488
524
  const visible = options.slice(start, start + visibleLimit);
489
- const width = Math.max(8, ...options.map((option) => visibleWidth(option) + 4));
525
+ const width = Math.max(8, ...options.map((option) => visibleWidth(option.content) + 4));
490
526
  const border = (left, right, marker = "") => this.theme.fg("borderAccent", `${left}${marker}${"─".repeat(Math.max(0, width - visibleWidth(marker)))}${right}`);
491
527
  return [
492
528
  border("╭", "╮", start > 0 ? "↑" : ""),
493
529
  ...visible.map((option, offset) => {
494
530
  const index = start + offset;
495
- const row = this.fit(`${index === focusedIndex ? " ▶ " : " "}${option}`, width);
496
- const colored = index === focusedIndex ? this.theme.fg("accent", row) : row;
497
- const styled = index === focusedIndex ? this.theme.bg("customMessageBg", colored) : colored;
531
+ const marker = index === focusedIndex
532
+ ? this.theme.fg("accent", " ")
533
+ : " ";
534
+ const row = this.fit(`${marker}${option.content}`, width);
535
+ const colored = index === focusedIndex && !option.preserveColors
536
+ ? this.theme.fg("accent", row)
537
+ : row;
538
+ const styled = index === focusedIndex ? this.theme.bg("selectedBg", colored) : colored;
498
539
  return `${this.theme.fg("borderAccent", "│")}${styled}${this.theme.fg("borderAccent", "│")}`;
499
540
  }),
500
541
  border("╰", "╯", start + visible.length < options.length ? "↓" : ""),
@@ -523,6 +564,12 @@ export class ActorInspectorOverlay {
523
564
  this.contentStripeIndices = items
524
565
  .slice(start, start + viewportRows)
525
566
  .map((_item, offset) => start + offset);
567
+ const selectedViewportRow = banner.length + this.rowIndex - start;
568
+ if (this.focus === "list" &&
569
+ selectedViewportRow >= banner.length &&
570
+ selectedViewportRow < viewportRows) {
571
+ this.contentSelectedRows.add(selectedViewportRow);
572
+ }
526
573
  return [...banner, ...items.slice(start, start + viewportRows - banner.length).map((item, offset) => {
527
574
  const index = start + offset;
528
575
  const detail = item.detail && typeof item.detail === "object"
@@ -536,7 +583,7 @@ export class ActorInspectorOverlay {
536
583
  const prefix = this.focus === "list" && index === this.rowIndex
537
584
  ? this.theme.fg("accent", " ▶ ")
538
585
  : " ";
539
- const row = `${prefix}${this.theme.fg("text", `#${items.length - index}`)} ${marker} ${this.theme.fg("muted", item.source)}/${this.theme.fg(item.level === "error" ? "error" : "accent", item.kind)} ${this.theme.fg("text", item.summary)}`;
586
+ const row = `${prefix}${this.theme.fg("text", `#${items.length - index - 1}`)} ${marker} ${this.theme.fg("muted", item.source)}/${this.theme.fg(item.level === "error" ? "error" : "accent", item.kind)} ${this.theme.fg("text", item.summary)}`;
540
587
  return this.focus === "list" && index === this.rowIndex
541
588
  ? this.theme.fg("accent", row)
542
589
  : row;
@@ -676,11 +723,27 @@ export class ActorInspectorOverlay {
676
723
  return [
677
724
  `${indent}[`,
678
725
  ...value.flatMap((item, index) => {
679
- const marker = `${indent} - #${index + 1}`;
726
+ const itemDepth = depth + 1;
727
+ const itemIndent = " ".repeat(itemDepth);
728
+ const marker = `#${index}`;
680
729
  const itemInline = this.inlineDocumentValue(item);
681
- return itemInline !== undefined
682
- ? [`${marker}: ${itemInline}`]
683
- : [marker, ...this.document(item, depth + 2)];
730
+ let lines;
731
+ if (itemInline !== undefined) {
732
+ lines = [`${itemIndent}${marker}: ${itemInline}`];
733
+ }
734
+ else if (item && typeof item === "object") {
735
+ const nested = this.document(item, itemDepth);
736
+ lines = [
737
+ `${itemIndent}${marker}: ${nested[0].slice(itemIndent.length)}`,
738
+ ...nested.slice(1),
739
+ ];
740
+ }
741
+ else {
742
+ lines = this.labeledScalarLines(marker, item, itemDepth);
743
+ }
744
+ if (index < value.length - 1)
745
+ lines[lines.length - 1] += ",";
746
+ return lines;
684
747
  }),
685
748
  `${indent}]`,
686
749
  ];
@@ -731,9 +794,13 @@ export class ActorInspectorOverlay {
731
794
  styleDocumentLine(line) {
732
795
  const indent = line.match(/^ */u)?.[0] ?? "";
733
796
  const content = line.slice(indent.length);
734
- if (/^- #\d+$/u.test(content) || content.endsWith(":")) {
797
+ if (content.endsWith(":")) {
735
798
  return `${indent}${this.theme.fg("accent", content)}`;
736
799
  }
800
+ const indexed = /^(#\d+:)(.*)$/u.exec(content);
801
+ if (indexed) {
802
+ return `${indent}${this.theme.fg("accent", indexed[1])}${this.theme.fg("text", indexed[2])}`;
803
+ }
737
804
  const labeled = /^([^:]+:)(.*)$/u.exec(content);
738
805
  if (labeled) {
739
806
  return `${indent}${this.theme.fg("muted", labeled[1])}${this.theme.fg("text", labeled[2])}`;
@@ -799,12 +866,27 @@ export class ActorInspectorOverlay {
799
866
  const fill = "─".repeat(Math.max(0, width - visibleWidth(prefix) - visibleWidth(suffix) - visibleWidth(content)));
800
867
  return `${this.theme.fg("borderAccent", `╰${prefix}`)}${content}${this.theme.fg("borderAccent", `${suffix}${fill}╯`)}`;
801
868
  }
869
+ compositeMenuLine(baseLine, menuLine, innerWidth, horizontalOffset) {
870
+ const offset = Math.max(0, Math.min(horizontalOffset, innerWidth));
871
+ const menuWidth = Math.min(innerWidth - offset, visibleWidth(menuLine));
872
+ const baseInner = sliceByColumn(baseLine, 1, innerWidth, true);
873
+ const prefix = sliceByColumn(baseInner, 0, offset, true);
874
+ const suffixStart = offset + menuWidth;
875
+ const suffix = sliceByColumn(baseInner, suffixStart, Math.max(0, innerWidth - suffixStart), true);
876
+ const remainder = this.fit(suffix, innerWidth - suffixStart);
877
+ return `${this.theme.fg("borderAccent", "│")}${this.fit(prefix, offset)}${truncateToWidth(menuLine, menuWidth, "")}${remainder}${this.theme.fg("borderAccent", "│")}`;
878
+ }
802
879
  stripeBackground(content, index) {
803
880
  return index % 2 === 0 ? content : this.theme.bg("customMessageBg", content);
804
881
  }
805
- stripedRow(content, width, index) {
806
- const fitted = this.fit(content, width);
807
- return `${this.theme.fg("borderAccent", "│")}${this.stripeBackground(fitted, index)}${this.theme.fg("borderAccent", "│")}`;
882
+ stripedRow(content, width, index, selected) {
883
+ if (!selected) {
884
+ const fitted = this.fit(content, width);
885
+ return `${this.theme.fg("borderAccent", "│")}${this.stripeBackground(fitted, index)}${this.theme.fg("borderAccent", "│")}`;
886
+ }
887
+ const selectedContent = truncateToWidth(`${content} `, width, "");
888
+ const remainder = " ".repeat(Math.max(0, width - visibleWidth(selectedContent)));
889
+ return `${this.theme.fg("borderAccent", "│")}${this.theme.bg("selectedBg", selectedContent)}${this.stripeBackground(remainder, index)}${this.theme.fg("borderAccent", "│")}`;
808
890
  }
809
891
  row(content, width) {
810
892
  return `${this.theme.fg("borderAccent", "│")}${this.fit(content, width)}${this.theme.fg("borderAccent", "│")}`;
@@ -11,6 +11,7 @@ export declare const CONTROL_INPUT_MAX_BYTES = 380;
11
11
  export declare const CONTROL_WIRE_MAX_BYTES = 512;
12
12
  export declare const INSPECTOR_BODY_PREVIEW_CHARS = 320;
13
13
  export declare const DOCTOR_ACTION_PREVIEW_CHARS = 72;
14
+ export declare const SESSION_EVIDENCE_MAX_BYTES: number;
14
15
  export declare const SESSION_EVIDENCE_MAX_TURNS = 100;
15
16
  export declare const SESSION_EVIDENCE_TEXT_CHARS = 4000;
16
17
  export declare const SESSION_EVIDENCE_MAX_TOOL_CALLS = 100;
@@ -11,6 +11,7 @@ export const CONTROL_INPUT_MAX_BYTES = 380;
11
11
  export const CONTROL_WIRE_MAX_BYTES = 512;
12
12
  export const INSPECTOR_BODY_PREVIEW_CHARS = 320;
13
13
  export const DOCTOR_ACTION_PREVIEW_CHARS = 72;
14
+ export const SESSION_EVIDENCE_MAX_BYTES = 4 * 1024 * 1024;
14
15
  export const SESSION_EVIDENCE_MAX_TURNS = 100;
15
16
  export const SESSION_EVIDENCE_TEXT_CHARS = 4_000;
16
17
  export const SESSION_EVIDENCE_MAX_TOOL_CALLS = 100;
@@ -229,7 +229,12 @@ export function createRunTerminalReconciliationLoop(input) {
229
229
  input.reconcile();
230
230
  }
231
231
  catch (error) {
232
- input.onError?.(error);
232
+ try {
233
+ input.onError?.(error);
234
+ }
235
+ catch {
236
+ /* reconciliation callbacks must never escape into the host event loop */
237
+ }
233
238
  }
234
239
  };
235
240
  const close = () => {
@@ -3,16 +3,23 @@
3
3
  * Zones: run watcher lifecycle, terminal reconciliation, status animation, shutdown teardown
4
4
  * Owns event-driven run UI coordination without owning actor execution semantics.
5
5
  */
6
+ import * as AsyncRuns from "./async-runs.ts";
7
+ import * as Observability from "./observability.ts";
6
8
  import * as Pi from "./pi.ts";
7
9
  export interface RunUiRuntime {
8
10
  close(): void;
9
- shutdown(eventReason: string, ctx: Pi.ExtensionContext): void;
10
- start(ctx: Pi.ExtensionContext): void;
11
+ shutdown(eventReason: string, ownerId: string | undefined, ctx?: Pi.ExtensionContext): void;
12
+ start(ctx: Pi.ExtensionContext, ownerId: string): void;
11
13
  }
12
14
  export interface RunUiRuntimeDeps {
15
+ animationIntervalMs?: number;
16
+ createRunStateWatcher?: typeof Observability.createRunStateWatcher;
17
+ createRunTerminalReconciliationLoop?: typeof Observability.createRunTerminalReconciliationLoop;
13
18
  getActiveContext(): Pi.ExtensionContext | undefined;
14
- getRunOwnerId(ctx: Pi.ExtensionContext): string;
19
+ notificationDelayMs?: number;
20
+ onCallbackError?: (error: unknown) => void;
15
21
  onRunEvent(): void;
16
22
  pi: Pi.ExtensionAPI;
23
+ teardownRunsOwnedByParent?: typeof AsyncRuns.teardownRunsOwnedByParent;
17
24
  }
18
25
  export declare function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime;