@timurproko/a1 0.1.8-dev.260 → 0.1.8-dev.271

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 (28) hide show
  1. package/bin/update-recovery.js +272 -0
  2. package/dist/foundation/release/index.d.ts +1 -0
  3. package/dist/foundation/release/index.js +1 -0
  4. package/dist/foundation/release/release-gc.js +2 -0
  5. package/dist/foundation/release/update-recovery.d.ts +74 -0
  6. package/dist/foundation/release/update-recovery.js +352 -0
  7. package/dist/foundation/release/update-transaction.d.ts +10 -0
  8. package/dist/foundation/release/update-transaction.js +12 -1
  9. package/dist/foundation/release/update.d.ts +19 -1
  10. package/dist/foundation/release/update.js +67 -10
  11. package/dist/integrations/pi/components/owned-editor-ux.js +61 -8
  12. package/dist/integrations/pi/components/path-word-ranges.d.ts +2 -0
  13. package/dist/integrations/pi/components/path-word-ranges.js +37 -0
  14. package/dist/integrations/pi/engine/conformance.d.ts +1 -1
  15. package/dist/integrations/pi/engine/conformance.js +9 -1
  16. package/dist/integrations/pi/engine/runtime-integration.js +10 -1
  17. package/dist/integrations/pi/engine/windows-filesystem-hygiene.d.ts +21 -0
  18. package/dist/integrations/pi/engine/windows-filesystem-hygiene.js +55 -0
  19. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +1 -1
  20. package/dist/integrations/pi/session-ui/session-shell-root.js +29 -20
  21. package/dist/native/darwin-arm64/manifest.json +1 -1
  22. package/dist/native/linux-x64/manifest.json +1 -1
  23. package/dist/native/win32-x64/manifest.json +2 -2
  24. package/dist/native/win32-x64/process-guardian.exe +0 -0
  25. package/dist/runtime-payload-inventory.json +1 -0
  26. package/dist/ui/components/transcript-viewport.d.ts +8 -0
  27. package/dist/ui/components/transcript-viewport.js +30 -6
  28. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { CURSOR_MARKER, decodeKittyPrintable, visibleWidth, } from "#pi-tui";
2
+ import { promptPathWordRanges } from "./path-word-ranges.js";
2
3
  export class OwnedEditorUxInterception {
3
4
  interceptors;
4
5
  fallback;
@@ -59,12 +60,13 @@ class PromptSelectionInterceptor {
59
60
  #lastClick;
60
61
  #redoStack = [];
61
62
  #selectionRevision = 0;
63
+ #wordDirection;
62
64
  #geometry;
63
65
  constructor(editor, keybindings, options) {
64
66
  this.editor = editor;
65
67
  this.keybindings = keybindings;
66
68
  this.options = options;
67
- installAtomicSegmentation(editor, options.atomicRanges);
69
+ installAtomicSegmentation(editor, options.atomicRanges, () => this.#wordDirection);
68
70
  }
69
71
  handleInput(data, next) {
70
72
  if (this.keybindings.matches(data, "owned.editor.selectAll")) {
@@ -167,24 +169,30 @@ class PromptSelectionInterceptor {
167
169
  if (this.keybindings.matches(data, "tui.editor.cursorWordLeft")) {
168
170
  const before = this.#cursor();
169
171
  const startedWithAtomicFocus = this.#atomicFocus() !== undefined;
170
- next();
172
+ this.#delegateWord(-1, next);
171
173
  let after = this.#cursor();
172
174
  const landedAtomic = this.#atomicRangeAt(after);
173
175
  if (!startedWithAtomicFocus && landedAtomic?.start === after.col && after.col > 0) {
174
176
  const line = editorState(this.editor).lines[after.line] ?? "";
175
177
  const previous = [...GRAPHEMES.segment(line.slice(0, after.col))].at(-1);
176
178
  if (previous !== undefined && !/^\s+$/u.test(previous.segment)) {
177
- next();
179
+ this.#delegateWord(-1, next);
178
180
  after = this.#cursor();
179
181
  }
180
182
  }
181
- if (!samePosition(before, after))
183
+ const line = editorState(this.editor).lines[after.line] ?? "";
184
+ const landedPath = promptPathWordRanges(line).some(range => range.start === after.col);
185
+ if (!samePosition(before, after) && !landedPath)
182
186
  this.#moveOntoPreviousSeparator();
183
187
  this.#requestRender();
184
188
  return;
185
189
  }
186
190
  const beforeText = this.editor.getText();
187
- next();
191
+ const wordDirection = this.#wordDirectionFor(data);
192
+ if (wordDirection === undefined)
193
+ next();
194
+ else
195
+ this.#delegateWord(wordDirection, next);
188
196
  if (this.editor.getText() !== beforeText)
189
197
  this.#redoStack = [];
190
198
  }
@@ -584,11 +592,28 @@ class PromptSelectionInterceptor {
584
592
  #snapshot() {
585
593
  return { text: this.editor.getText(), cursor: this.#cursor() };
586
594
  }
595
+ #wordDirectionFor(data) {
596
+ if (this.keybindings.matches(data, "tui.editor.deleteWordBackward"))
597
+ return -1;
598
+ if (this.keybindings.matches(data, "tui.editor.deleteWordForward")
599
+ || this.keybindings.matches(data, "tui.editor.cursorWordRight"))
600
+ return 1;
601
+ return undefined;
602
+ }
603
+ #delegateWord(direction, next) {
604
+ this.#wordDirection = direction;
605
+ try {
606
+ next();
607
+ }
608
+ finally {
609
+ this.#wordDirection = undefined;
610
+ }
611
+ }
587
612
  #requestRender() {
588
613
  this.options.requestRender();
589
614
  }
590
615
  }
591
- function installAtomicSegmentation(editor, rangesForText) {
616
+ function installAtomicSegmentation(editor, rangesForText, wordDirection) {
592
617
  if (Reflect.get(editor, ATOMIC_SEGMENTATION) === true)
593
618
  return;
594
619
  const originalValue = Reflect.get(editor, "segment");
@@ -597,7 +622,14 @@ function installAtomicSegmentation(editor, rangesForText) {
597
622
  const original = originalValue.bind(editor);
598
623
  Reflect.set(editor, "segment", (text, mode) => {
599
624
  const segments = [...original(text, mode)].filter(isEditorSegment);
600
- const ranges = rangesForText(text);
625
+ const ranges = rangesForText(text).map(range => ({ ...range, wordLike: false }));
626
+ if (mode === "word") {
627
+ for (const range of contextualPathRanges(editor, text, wordDirection())) {
628
+ if (!ranges.some(existing => rangesOverlap(existing, range)))
629
+ ranges.push({ ...range, wordLike: true });
630
+ }
631
+ }
632
+ ranges.sort((left, right) => left.start - right.start);
601
633
  if (ranges.length === 0)
602
634
  return segments;
603
635
  const merged = [];
@@ -608,10 +640,12 @@ function installAtomicSegmentation(editor, rangesForText) {
608
640
  const range = ranges[rangeIndex];
609
641
  if (range !== undefined && segment.index >= range.start && segment.index < range.end) {
610
642
  if (segment.index === range.start) {
643
+ const source = text.slice(range.start, range.end);
611
644
  merged.push({
612
- segment: text.slice(range.start, range.end).replaceAll(" ", ATOMIC_SPACE_SENTINEL),
645
+ segment: range.wordLike ? "w".repeat(source.length) : source.replaceAll(" ", ATOMIC_SPACE_SENTINEL),
613
646
  index: range.start,
614
647
  input: text,
648
+ ...(range.wordLike ? { isWordLike: true } : {}),
615
649
  });
616
650
  }
617
651
  continue;
@@ -622,6 +656,25 @@ function installAtomicSegmentation(editor, rangesForText) {
622
656
  });
623
657
  Reflect.set(editor, ATOMIC_SEGMENTATION, true);
624
658
  }
659
+ function contextualPathRanges(editor, text, direction) {
660
+ if (direction === undefined)
661
+ return promptPathWordRanges(text);
662
+ const state = editorState(editor);
663
+ const line = state.lines[state.cursorLine] ?? "";
664
+ const offset = direction < 0 ? 0 : state.cursorCol;
665
+ if (text !== (direction < 0 ? line.slice(0, state.cursorCol) : line.slice(state.cursorCol))) {
666
+ return promptPathWordRanges(text);
667
+ }
668
+ const end = offset + text.length;
669
+ return promptPathWordRanges(line).flatMap(range => {
670
+ const start = Math.max(range.start, offset);
671
+ const finish = Math.min(range.end, end);
672
+ return finish <= start ? [] : [{ start: start - offset, end: finish - offset }];
673
+ });
674
+ }
675
+ function rangesOverlap(left, right) {
676
+ return left.start < right.end && right.start < left.end;
677
+ }
625
678
  function isEditorSegment(value) {
626
679
  if (typeof value !== "object" || value === null)
627
680
  return false;
@@ -0,0 +1,2 @@
1
+ import type { PiShellEditorTextRange } from "./shell-shared-facade.js";
2
+ export declare function promptPathWordRanges(line: string): readonly PiShellEditorTextRange[];
@@ -0,0 +1,37 @@
1
+ const WHITESPACE = /\s/u;
2
+ const DRIVE_ROOT = /^[A-Za-z]:[\\/]/u;
3
+ const UNC_ROOT = /^(?:\\\\|\/\/)[^\\/\s]+[\\/][^\\/\s]+/u;
4
+ const POSIX_ROOT = /^\//u;
5
+ const EXPLICIT_RELATIVE_ROOT = /^(?:\.{1,2}|~)[\\/]/u;
6
+ export function promptPathWordRanges(line) {
7
+ const ranges = [];
8
+ let index = 0;
9
+ while (index < line.length) {
10
+ while (index < line.length && WHITESPACE.test(line[index] ?? ""))
11
+ index += 1;
12
+ if (index >= line.length)
13
+ break;
14
+ const start = index;
15
+ const quote = line[index] === '"' || line[index] === "'" ? line[index] : undefined;
16
+ if (quote !== undefined) {
17
+ const closing = line.indexOf(quote, index + 1);
18
+ if (closing >= 0 && (closing + 1 === line.length || WHITESPACE.test(line[closing + 1] ?? ""))) {
19
+ if (isExplicitPath(line.slice(index + 1, closing)))
20
+ ranges.push({ start, end: closing + 1 });
21
+ index = closing + 1;
22
+ continue;
23
+ }
24
+ }
25
+ while (index < line.length && !WHITESPACE.test(line[index] ?? ""))
26
+ index += 1;
27
+ if (isExplicitPath(line.slice(start, index)))
28
+ ranges.push({ start, end: index });
29
+ }
30
+ return ranges;
31
+ }
32
+ function isExplicitPath(value) {
33
+ return DRIVE_ROOT.test(value)
34
+ || UNC_ROOT.test(value)
35
+ || POSIX_ROOT.test(value)
36
+ || EXPLICIT_RELATIVE_ROOT.test(value);
37
+ }
@@ -9,7 +9,7 @@ export declare const REQUIRED_PI_CAPABILITY_OPERATIONS: Readonly<{
9
9
  readonly "commands-events": readonly ["prompt", "steer", "followUp", "abort", "compact", "setModel", "setThinkingLevel", "subscribe", "dispose"];
10
10
  readonly "models-authentication": readonly ["models.list", "models.refresh", "auth.status", "auth.login", "auth.logout", "auth.cancel"];
11
11
  readonly settings: readonly ["settings.read", "settings.write", "settings.flush"];
12
- readonly "resources-extensions": readonly ["resources.discover", "extensions.bind", "extensions.reload", "renderers.invoke"];
12
+ readonly "resources-extensions": readonly ["resources.discover", "extensions.inline", "extensions.bind", "extensions.reload", "renderers.invoke"];
13
13
  readonly workflows: readonly ["workflow.route", "workflow.validate", "workflow.diagnostics"];
14
14
  readonly disposal: readonly ["subscription.dispose", "session.dispose", "services.cleanup"];
15
15
  }>;
@@ -4,13 +4,14 @@ import { join } from "node:path";
4
4
  import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, ModelRuntime, SessionManager, VERSION, } from "@earendil-works/pi-coding-agent";
5
5
  import { OWNED_UI_CONTRACT_VERSION } from "../../../contracts/owned-ui/index.js";
6
6
  import { PRODUCT_IDENTITY } from "../../../product-identity.js";
7
+ import { createWindowsNulCleanupExtension } from "./windows-filesystem-hygiene.js";
7
8
  export const REQUIRED_PI_CAPABILITY_OPERATIONS = Object.freeze({
8
9
  "public-exports": ["services.create", "session.create", "runtime.create"],
9
10
  "session-lifecycle": ["session.new", "session.resume", "session.rebind", "session.dispose"],
10
11
  "commands-events": ["prompt", "steer", "followUp", "abort", "compact", "setModel", "setThinkingLevel", "subscribe", "dispose"],
11
12
  "models-authentication": ["models.list", "models.refresh", "auth.status", "auth.login", "auth.logout", "auth.cancel"],
12
13
  settings: ["settings.read", "settings.write", "settings.flush"],
13
- "resources-extensions": ["resources.discover", "extensions.bind", "extensions.reload", "renderers.invoke"],
14
+ "resources-extensions": ["resources.discover", "extensions.inline", "extensions.bind", "extensions.reload", "renderers.invoke"],
14
15
  workflows: ["workflow.route", "workflow.validate", "workflow.diagnostics"],
15
16
  disposal: ["subscription.dispose", "session.dispose", "services.cleanup"],
16
17
  });
@@ -59,11 +60,18 @@ export async function runPiUpgradeConformance() {
59
60
  refreshOnCreate: false,
60
61
  allowModelNetwork: false,
61
62
  });
63
+ const cleanupExtension = createWindowsNulCleanupExtension({ platform: "win32" });
64
+ if (cleanupExtension === null)
65
+ throw new Error("Windows NUL cleanup extension is unavailable");
62
66
  services = await createAgentSessionServices({
63
67
  cwd: root,
64
68
  agentDir: join(root, "agent"),
65
69
  modelRuntime,
70
+ resourceLoaderOptions: { extensionFactories: [cleanupExtension] },
66
71
  });
72
+ if (!services.resourceLoader.getExtensions().extensions.some(extension => extension.path === "<inline:windows-nul-file-cleanup>")) {
73
+ throw new Error("named inline extension factory was not loaded");
74
+ }
67
75
  }
68
76
  catch (error) {
69
77
  throw new PiUpgradeConformanceError("services", error);
@@ -2,6 +2,7 @@ import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentS
2
2
  import { resolvePiProjectTrustPreflight, } from "./project-trust-preflight.js";
3
3
  import { openSelectedPiSession, resolveSessionArgumentPath } from "./session-selection.js";
4
4
  import { markStartupPhase } from "../../../foundation/startup/index.js";
5
+ import { createWindowsNulCleanupExtension } from "./windows-filesystem-hygiene.js";
5
6
  /**
6
7
  * Mirrors pinned Pi's CLI startup: resolve the `models` patterns from settings
7
8
  * into a scoped model list, keep the resolver's warnings (e.g. "No models match
@@ -44,7 +45,15 @@ export async function createPiRuntimeServicesAfterTrust(options) {
44
45
  });
45
46
  const settingsManager = createSettingsManager(options.cwd, options.agentDir, trust.trusted);
46
47
  await markStartupPhase(process.env, "settings-loaded");
47
- const services = await createServices({ cwd: options.cwd, agentDir: options.agentDir, settingsManager });
48
+ const cleanupExtension = createWindowsNulCleanupExtension();
49
+ const services = await createServices({
50
+ cwd: options.cwd,
51
+ agentDir: options.agentDir,
52
+ settingsManager,
53
+ ...(cleanupExtension === null ? {} : {
54
+ resourceLoaderOptions: { extensionFactories: [cleanupExtension] },
55
+ }),
56
+ });
48
57
  await markStartupPhase(process.env, "pi-services");
49
58
  await markStartupPhase(process.env, "resource-discovery");
50
59
  return { services, trust };
@@ -0,0 +1,21 @@
1
+ import { type InlineExtension } from "@earendil-works/pi-coding-agent";
2
+ interface FileMetadata {
3
+ isFile(): boolean;
4
+ isSymbolicLink(): boolean;
5
+ }
6
+ interface CleanupFileSystem {
7
+ lstat(path: string): Promise<FileMetadata>;
8
+ unlink(path: string): Promise<void>;
9
+ }
10
+ interface CleanupPaths {
11
+ dirname(path: string): string;
12
+ join(...paths: string[]): string;
13
+ resolve(...paths: string[]): string;
14
+ }
15
+ export interface WindowsNulCleanupOptions {
16
+ readonly platform?: NodeJS.Platform;
17
+ readonly fileSystem?: CleanupFileSystem;
18
+ readonly paths?: CleanupPaths;
19
+ }
20
+ export declare function createWindowsNulCleanupExtension(options?: WindowsNulCleanupOptions): InlineExtension | null;
21
+ export {};
@@ -0,0 +1,55 @@
1
+ import { isBashToolResult, isEditToolResult, isWriteToolResult, } from "@earendil-works/pi-coding-agent";
2
+ import { lstat, unlink } from "node:fs/promises";
3
+ import { dirname, join, resolve } from "node:path";
4
+ const DEFAULT_FILE_SYSTEM = { lstat, unlink };
5
+ const DEFAULT_PATHS = { dirname, join, resolve };
6
+ const SIMPLE_CD = /(?:^|[;&|\n])\s*cd\s+(?:"([^"\r\n]+)"|'([^'\r\n]+)'|([^\s;&|]+))/;
7
+ export function createWindowsNulCleanupExtension(options = {}) {
8
+ if ((options.platform ?? process.platform) !== "win32")
9
+ return null;
10
+ const fileSystem = options.fileSystem ?? DEFAULT_FILE_SYSTEM;
11
+ const paths = options.paths ?? DEFAULT_PATHS;
12
+ return {
13
+ name: "windows-nul-file-cleanup",
14
+ factory: pi => registerNulCleanup(pi, fileSystem, paths),
15
+ };
16
+ }
17
+ function registerNulCleanup(pi, fileSystem, paths) {
18
+ const cleanDirectory = async (directory) => {
19
+ const candidate = paths.join(directory, "nul");
20
+ try {
21
+ const metadata = await fileSystem.lstat(candidate);
22
+ if (metadata.isFile() && !metadata.isSymbolicLink())
23
+ await fileSystem.unlink(candidate);
24
+ }
25
+ catch {
26
+ return;
27
+ }
28
+ };
29
+ pi.on("session_start", async (_event, ctx) => {
30
+ await cleanDirectory(ctx.cwd);
31
+ });
32
+ pi.on("tool_result", async (event, ctx) => {
33
+ const directories = new Set([ctx.cwd]);
34
+ if (isBashToolResult(event)) {
35
+ const command = stringInput(event.input, "command");
36
+ const target = command === null ? null : simpleCdTarget(command);
37
+ if (target !== null)
38
+ directories.add(paths.resolve(ctx.cwd, target));
39
+ }
40
+ if (isWriteToolResult(event) || isEditToolResult(event)) {
41
+ const target = stringInput(event.input, "path");
42
+ if (target !== null)
43
+ directories.add(paths.dirname(paths.resolve(ctx.cwd, target)));
44
+ }
45
+ await Promise.all(Array.from(directories, cleanDirectory));
46
+ });
47
+ }
48
+ function simpleCdTarget(command) {
49
+ const match = SIMPLE_CD.exec(command);
50
+ return match?.[1] ?? match?.[2] ?? match?.[3] ?? null;
51
+ }
52
+ function stringInput(input, key) {
53
+ const value = input[key];
54
+ return typeof value === "string" && value.length > 0 ? value : null;
55
+ }
@@ -92,7 +92,7 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
92
92
  applyTranscriptBlock(block: OwnedUiSessionViewModel["transcript"][number]): void;
93
93
  render(width: number): readonly string[];
94
94
  viewportFrameDescriptor(): TranscriptViewportFrameDescriptor | null;
95
- /** Visible rows of the transient working-status tail, for rendering evidence. */
95
+ /** Visible non-selectable Steering, alignment, and Working rows, for rendering evidence. */
96
96
  viewportTransientTailRowCount(): number;
97
97
  hasActiveSelection(): boolean;
98
98
  setViewportConfig(config: OwnedUiViewportSettings): void;
@@ -45,7 +45,6 @@ export class OwnedUiSessionShellRoot {
45
45
  #dockInputReuseEnabled;
46
46
  #dockInputCandidate = false;
47
47
  #dockInputSnapshot;
48
- #dockInputStatusSignature;
49
48
  #visibleViewportSnapshot;
50
49
  #fullViewportCompositions = 0;
51
50
  #dockOnlyViewportCompositions = 0;
@@ -232,14 +231,18 @@ export class OwnedUiSessionShellRoot {
232
231
  // intentional final gutter after their right-aligned timestamp.
233
232
  const documentWidth = width;
234
233
  const document = this.#renderDocumentLayout(documentWidth);
234
+ const steeringRows = this.#renderQueued(width);
235
235
  const statusRows = this.#renderStatus(width);
236
- const statusSignature = statusRows.length === 0 ? undefined : `${statusRows.length}\u0000${statusRows.join("\u0000")}`;
236
+ const transientSignature = transientRowsSignature(steeringRows, statusRows);
237
237
  const snapshot = this.#visibleViewportSnapshot;
238
238
  const dockInputCandidate = this.#dockInputCandidate;
239
239
  const dockInputSnapshot = dockInputCandidate ? this.#dockInputSnapshot : undefined;
240
- const dockInputStatusSignature = dockInputCandidate ? this.#dockInputStatusSignature : statusSignature;
241
- // Invariant: queued rows stay docked while live Working rows form one scrollable tail.
242
- const scrollRows = [...document.rows, ...statusRows];
240
+ const dockInputTransientSignature = dockInputCandidate
241
+ ? dockInputSnapshot?.transientSignature ?? transientSignature
242
+ : transientSignature;
243
+ // Invariant: pending Steering and live Working rows share one non-selectable
244
+ // viewport tail; only non-working status, widgets, input, and footer stay docked.
245
+ const scrollRows = [...document.rows, ...steeringRows, ...statusRows];
243
246
  const dockRows = dock.rows;
244
247
  const selectableDocumentRowCount = document.rows.length;
245
248
  const dockStartRow = height - dockRows.length + 1;
@@ -256,7 +259,7 @@ export class OwnedUiSessionShellRoot {
256
259
  && snapshot.width === width
257
260
  && snapshot.height === height
258
261
  && snapshot.documentRows === (dockInputSnapshot?.documentRows ?? scrollRows)
259
- && snapshot.statusSignature === dockInputStatusSignature
262
+ && snapshot.transientSignature === dockInputTransientSignature
260
263
  && snapshot.promptAnchors === (dockInputSnapshot?.promptAnchors ?? document.promptAnchors)
261
264
  && snapshot.dockLength === dockRows.length
262
265
  && snapshot.inputSurface === (dockInputSnapshot?.inputSurface ?? this.#inputSurface)
@@ -264,9 +267,9 @@ export class OwnedUiSessionShellRoot {
264
267
  && snapshot.selectionRevision === (dockInputSnapshot?.selectionRevision ?? this.#viewportController.selectionRevision)
265
268
  ? this.#viewportController.composeDockOnly(dockRows, width, height)
266
269
  : null;
267
- // Performance: a still-active spinner can tick before its first delayed presentation.
268
- // Its newest tail signature is recomputed below and therefore cannot hide that change.
269
- if (frame !== null && dockInputStatusSignature !== statusSignature)
270
+ // Performance: a spinner tick or queue update can race the input-triggered
271
+ // render. Recompute the complete transient signature before reusing rows.
272
+ if (frame !== null && dockInputTransientSignature !== transientSignature)
270
273
  frame = null;
271
274
  if (frame === null) {
272
275
  frame = this.#viewportController.compose({
@@ -274,15 +277,16 @@ export class OwnedUiSessionShellRoot {
274
277
  ...(this.#viewportController.transcriptPointerSelecting
275
278
  ? { paintDocumentRow: heldNativeHyperlinkStyle }
276
279
  : {}),
277
- // Invariant: selection and copying stop at the real document tail; live Working
278
- // rows remain transient presentation chrome at every fit boundary.
280
+ // Invariant: selection and copying stop at the real document tail; Steering,
281
+ // fitting alignment, and live Working remain transient presentation chrome.
279
282
  selectableDocumentRowCount,
283
+ bottomAlignedTailRowCount: statusRows.length,
280
284
  dockRows,
281
285
  promptAnchors: document.promptAnchors,
282
286
  width,
283
287
  height,
284
- // Invariant: the control belongs immediately above the complete dock,
285
- // never on top of a queued or Working row.
288
+ // Invariant: the control belongs immediately above the complete dock and
289
+ // floats over transient viewport content rather than consuming a dock row.
286
290
  bottomControlRow: Math.max(0, height - Math.min(height, dockRows.length) - 1),
287
291
  theme: this.#viewportTheme,
288
292
  });
@@ -294,8 +298,7 @@ export class OwnedUiSessionShellRoot {
294
298
  width,
295
299
  height,
296
300
  documentRows: scrollRows,
297
- statusSignature,
298
- dockInputStatusSignature,
301
+ transientSignature,
299
302
  promptAnchors: document.promptAnchors,
300
303
  dockLength: dockRows.length,
301
304
  inputSurface: this.#inputSurface,
@@ -308,7 +311,7 @@ export class OwnedUiSessionShellRoot {
308
311
  viewportFrameDescriptor() {
309
312
  return this.#viewportController.frame?.descriptor ?? null;
310
313
  }
311
- /** Visible rows of the transient working-status tail, for rendering evidence. */
314
+ /** Visible non-selectable Steering, alignment, and Working rows, for rendering evidence. */
312
315
  viewportTransientTailRowCount() {
313
316
  return this.#viewportController.frame?.hits.transientTail.length ?? 0;
314
317
  }
@@ -338,7 +341,7 @@ export class OwnedUiSessionShellRoot {
338
341
  return this.#renderDockLayout(width).rows;
339
342
  }
340
343
  #renderDockLayout(width) {
341
- const queued = this.#view.editor.queuedSubmissions.length === 0 ? [] : this.#queued.render(width);
344
+ const queued = this.#customViewport ? [] : this.#renderQueued(width);
342
345
  const statusRows = this.#customViewport ? this.#status.renderDock(width) : this.#renderStatus(width);
343
346
  const transientRows = [...queued, ...statusRows];
344
347
  const aboveWidgets = this.#renderWidgets("aboveEditor", width);
@@ -352,6 +355,9 @@ export class OwnedUiSessionShellRoot {
352
355
  inputRows: input.length,
353
356
  };
354
357
  }
358
+ #renderQueued(width) {
359
+ return this.#view.editor.queuedSubmissions.length === 0 ? [] : this.#queued.render(width);
360
+ }
355
361
  layoutRoot() {
356
362
  const document = layoutPort(width => this.#renderDocument(width), () => this.invalidate());
357
363
  const queued = layoutPort(width => this.#view.editor.queuedSubmissions.length === 0 ? [] : this.#queued.render(width), () => this.#queued.invalidate());
@@ -781,14 +787,15 @@ export class OwnedUiSessionShellRoot {
781
787
  }
782
788
  #captureDockInputSnapshot() {
783
789
  const width = Math.max(1, this.#componentRuntime.getColumns());
790
+ const steeringRows = this.#renderQueued(width);
784
791
  const statusRows = this.#renderStatus(width);
785
- this.#dockInputStatusSignature = statusRows.length === 0 ? undefined : `${statusRows.length}\u0000${statusRows.join("\u0000")}`;
792
+ const transientSignature = transientRowsSignature(steeringRows, statusRows);
786
793
  const snapshot = this.#visibleViewportSnapshot;
787
794
  this.#dockInputSnapshot = snapshot === undefined
788
795
  ? undefined
789
796
  : {
790
797
  documentRows: snapshot.documentRows,
791
- statusSignature: this.#dockInputStatusSignature,
798
+ transientSignature,
792
799
  promptAnchors: snapshot.promptAnchors,
793
800
  inputSurface: this.#inputSurface,
794
801
  viewportRevision: this.#viewportController.presentationRevision,
@@ -824,7 +831,6 @@ export class OwnedUiSessionShellRoot {
824
831
  this.#visibleViewportSnapshot = undefined;
825
832
  this.#dockInputCandidate = false;
826
833
  this.#dockInputSnapshot = undefined;
827
- this.#dockInputStatusSignature = undefined;
828
834
  if (this.#inputSurface !== this.editor)
829
835
  this.#inputSurface.dispose?.();
830
836
  this.#extensionHeader?.dispose?.();
@@ -1156,3 +1162,6 @@ function isPackageExtensionSource(sourceInfo) {
1156
1162
  const source = sourceInfo?.source ?? "";
1157
1163
  return source.startsWith("npm:") || source.startsWith("git:");
1158
1164
  }
1165
+ function transientRowsSignature(steeringRows, statusRows) {
1166
+ return `${steeringRows.length}\u0000${steeringRows.join("\u0000")}\u0001${statusRows.length}\u0000${statusRows.join("\u0000")}`;
1167
+ }
@@ -5,7 +5,7 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-06T12:29:18.472Z",
8
+ "builtAt": "2026-09-06T15:05:45.656Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "dc03605e5780e2aeebb4ecafd62868dc673e22ddd38b024a369aff704ff136dc",
@@ -5,7 +5,7 @@
5
5
  "platform": "linux",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-06T12:29:19.551Z",
8
+ "builtAt": "2026-09-06T15:05:42.977Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "ee8a00eaaf79c707459bbbfb52518e9739314967049fbe5fa625f36ce33db9ee",
@@ -5,10 +5,10 @@
5
5
  "platform": "win32",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-06T12:30:04.599Z",
8
+ "builtAt": "2026-09-06T15:06:12.448Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian.exe",
11
- "sha256": "ce28943a65f5bdd6fd5a2c8b7d513a87402652e9c9089fb3a0b6323083fdd9d2",
11
+ "sha256": "7cca17597d89ae0dad9cd14114acc0795f688d54f6580b897463fba11af479ca",
12
12
  "size": 177664
13
13
  },
14
14
  "provenance": {
@@ -5,6 +5,7 @@
5
5
  "bin/guardian.js",
6
6
  "bin/supervisor.js",
7
7
  "bin/ui.js",
8
+ "bin/update-recovery.js",
8
9
  "bin/warmup.js"
9
10
  ],
10
11
  "declaredAssets": [
@@ -24,6 +24,8 @@ export interface TranscriptViewportFrameInput {
24
24
  readonly paintDocumentRow?: (row: string) => string;
25
25
  /** Leading document rows that participate in pointer selection and copying. */
26
26
  readonly selectableDocumentRowCount?: number;
27
+ /** Final transient rows to bottom-align with unused viewport space while content fits. */
28
+ readonly bottomAlignedTailRowCount?: number;
27
29
  readonly dockRows: readonly string[];
28
30
  readonly promptAnchors: readonly TranscriptPromptAnchor[];
29
31
  readonly width: number;
@@ -86,6 +88,12 @@ export interface TranscriptViewportFrameDescriptor {
86
88
  };
87
89
  readonly previousFollowingEnd: boolean | null;
88
90
  readonly followingEnd: boolean;
91
+ /** Complete non-selectable suffix, including pending steering, alignment, and status rows. */
92
+ readonly transientRowCount: number;
93
+ /** Flexible rows inserted before the bottom-aligned live status while content fits. */
94
+ readonly transientAlignmentGapRows: number;
95
+ /** Live status rows at the end of the transient suffix. */
96
+ readonly bottomAlignedTailRowCount: number;
89
97
  readonly verticalShiftRows: number;
90
98
  readonly safeVerticalShift: boolean;
91
99
  /** Monotonic interaction revision used to reject stale selection evidence. */