@timurproko/a1 0.1.8-dev.563 → 0.1.8-dev.573

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 (71) hide show
  1. package/README.md +13 -11
  2. package/dist/app/session-shell/prompt-chips.js +20 -28
  3. package/dist/app/session-shell/session-shell-root.d.ts +2 -0
  4. package/dist/app/session-shell/session-shell-root.js +60 -17
  5. package/dist/app/session-shell/session-shell.js +35 -1
  6. package/dist/app/session-shell/session-viewport-controller.d.ts +1 -0
  7. package/dist/app/session-shell/session-viewport-controller.js +52 -30
  8. package/dist/cli/dispatch.d.ts +3 -1
  9. package/dist/cli/dispatch.js +56 -56
  10. package/dist/cli/package-messages.d.ts +4 -3
  11. package/dist/cli/package-messages.js +23 -11
  12. package/dist/contracts/owned-ui/index.d.ts +2 -0
  13. package/dist/contracts/owned-ui/index.js +1 -0
  14. package/dist/contracts/owned-ui/prompt-chips.d.ts +15 -0
  15. package/dist/contracts/owned-ui/prompt-chips.js +59 -0
  16. package/dist/contracts/owned-ui/validation.js +1 -1
  17. package/dist/integrations/pi/components/index.d.ts +1 -0
  18. package/dist/integrations/pi/components/index.js +1 -0
  19. package/dist/integrations/pi/components/modal-frame.d.ts +41 -0
  20. package/dist/integrations/pi/components/modal-frame.js +110 -0
  21. package/dist/integrations/pi/components/models-dialog.js +19 -5
  22. package/dist/integrations/pi/components/shell-footer-status.d.ts +4 -3
  23. package/dist/integrations/pi/components/shell-footer-status.js +53 -15
  24. package/dist/integrations/pi/components/shell-selectors-dialogs.js +38 -2
  25. package/dist/integrations/pi/components/shell-shared-facade.d.ts +9 -1
  26. package/dist/integrations/pi/components/skills-dialog.js +3 -3
  27. package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +1 -1
  28. package/dist/integrations/pi/components/submitted-prompt-adapter.js +5 -2
  29. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +2 -2
  30. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +3 -2
  31. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +2 -1
  32. package/dist/integrations/pi/components/upstream/components/extension-editor.js +7 -6
  33. package/dist/integrations/pi/components/upstream/components/extension-input.js +9 -8
  34. package/dist/integrations/pi/components/upstream/components/extension-selector.js +10 -9
  35. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
  36. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +3 -3
  37. package/dist/integrations/pi/components/upstream/components/session-selector.js +13 -6
  38. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +7 -6
  39. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +15 -12
  40. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +1 -1
  41. package/dist/integrations/pi/components/upstream/components/tree-selector.js +13 -6
  42. package/dist/integrations/pi/components/upstream/components/trust-selector.js +9 -9
  43. package/dist/integrations/pi/engine/compaction-progress.d.ts +3 -7
  44. package/dist/integrations/pi/engine/compaction-progress.js +11 -10
  45. package/dist/integrations/pi/engine/repository-pr.d.ts +3 -3
  46. package/dist/integrations/pi/engine/repository-pr.js +6 -6
  47. package/dist/integrations/pi/engine/session-runtime.js +5 -4
  48. package/dist/integrations/pi/engine/settings-bridge.js +1 -1
  49. package/dist/integrations/pi/engine/settings-port.js +1 -1
  50. package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -0
  51. package/dist/integrations/pi/tui-runtime/adapter.js +11 -2
  52. package/dist/integrations/pi/tui-runtime/contracts.d.ts +2 -0
  53. package/dist/integrations/pi/tui-runtime/mouse-report-input.d.ts +6 -0
  54. package/dist/integrations/pi/tui-runtime/mouse-report-input.js +34 -0
  55. package/dist/native/darwin-arm64/manifest.json +1 -1
  56. package/dist/native/linux-x64/manifest.json +1 -1
  57. package/dist/native/win32-x64/manifest.json +3 -3
  58. package/dist/native/win32-x64/process-guardian.exe +0 -0
  59. package/dist/ui/components/dialog-panel.js +10 -6
  60. package/dist/ui/components/index.d.ts +2 -1
  61. package/dist/ui/components/index.js +1 -1
  62. package/dist/ui/components/mouse.d.ts +2 -2
  63. package/dist/ui/components/mouse.js +2 -2
  64. package/dist/ui/components/progress-status.d.ts +9 -2
  65. package/dist/ui/components/progress-status.js +24 -3
  66. package/dist/ui/components/transcript-viewport.d.ts +9 -3
  67. package/dist/ui/components/transcript-viewport.js +116 -136
  68. package/docs/architecture/ui-reference-provenance.md +8 -5
  69. package/docs/openspec-archive-automation.md +2 -2
  70. package/docs/validation.md +1 -1
  71. package/package.json +1 -1
@@ -4,7 +4,7 @@
4
4
  * Modifications: Source-synchronized session selector port: preserve threaded/current/all scope,
5
5
  * search, sort, named/path filters, rename, delete confirmation, active-session protection, loading
6
6
  * progress, cancellation, focus, and disposal while remapping public helpers, owned keybindings/theme,
7
- * canonical path handling, and the shared bare-A1 modal shortcut row.
7
+ * canonical path handling, and the shared bare-A1 modal shortcut row and compact padded modal frame.
8
8
  * Deviations: owned-modal-shortcut-hints.
9
9
  */
10
10
  import { spawnSync } from "node:child_process";
@@ -14,6 +14,7 @@ import * as os from "node:os";
14
14
  import { Container, getKeybindings, Input, Spacer, Text, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
15
15
  import { KeybindingsManager } from "../adjacent/core/keybindings.js";
16
16
  import { DynamicBorder } from "../../../startup-public.js";
17
+ import { addPiModalHeader, adoptPiModalFrame } from "../../modal-frame.js";
17
18
  import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
18
19
  const theme = new Proxy({}, {
19
20
  get(_target, property) {
@@ -665,15 +666,21 @@ export class SessionSelectorComponent extends Container {
665
666
  buildBaseLayout(content, options) {
666
667
  this.clear();
667
668
  this.addChild(new Spacer(1));
668
- this.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
669
+ const showHeader = options?.showHeader ?? true;
670
+ const modalHeader = showHeader
671
+ ? addPiModalHeader(this, new DynamicBorder((s) => theme.fg("accent", s)), this.header)
672
+ : undefined;
673
+ if (!showHeader)
674
+ this.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
669
675
  this.addChild(new Spacer(1));
670
- if (options?.showHeader ?? true) {
671
- this.addChild(this.header);
672
- this.addChild(new Spacer(1));
673
- }
674
676
  this.addChild(content);
675
677
  this.addChild(new Spacer(1));
676
678
  this.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
679
+ const frame = { topIndex: 1, bottomIndex: this.children.length - 1 };
680
+ if (modalHeader === undefined)
681
+ adoptPiModalFrame(this, frame);
682
+ else
683
+ adoptPiModalFrame(this, { ...frame, header: modalHeader });
677
684
  }
678
685
  constructor(currentSessionsLoader, allSessionsLoader, onSelect, onCancel, onExit, requestRender, options, currentSessionFilePath) {
679
686
  super();
@@ -3,11 +3,12 @@
3
3
  * packages/coding-agent/src/modes/interactive/components/thinking-selector.ts.
4
4
  * Modifications: Preserve the searchable thinking-level selector, current/default semantics,
5
5
  * selection, save, cancellation, and focus while accepting the active bare-A1 cycle-key label from the
6
- * shell, styling the title with the established bold semantic accent treatment, placing its muted hint
7
- * directly below it, deduplicating levels, and rendering aligned muted descriptions after adjacent
8
- * active and bracketed default markers. All list and border colors use the owned theme and its
9
- * explicit color mode, and the footer uses the shared bare-A1 modal shortcut row. The comparison
10
- * profile retains the public pinned component.
6
+ * shell, styling the title with the established bold semantic accent treatment, placing it directly
7
+ * below the top rule through the shared compact padded modal frame and its muted hint directly below
8
+ * it, deduplicating levels, and rendering aligned muted descriptions after adjacent active and
9
+ * bracketed default markers. All list and border colors use the owned theme and its explicit color
10
+ * mode, and the footer uses the shared bare-A1 modal shortcut row. The comparison profile retains the
11
+ * public pinned component.
11
12
  * Deviations: owned-modal-shortcut-hints, owned-level-cycle-shortcut, owned-thinking-selector-heading.
12
13
  */
13
14
  import { Container, type Focusable, SelectList } from "@earendil-works/pi-tui";
@@ -16,7 +17,7 @@ export type ThinkingSelectorLevel = "off" | "minimal" | "low" | "medium" | "high
16
17
  export declare class OwnedThinkingSelectorComponent extends Container implements Focusable {
17
18
  private searchInput;
18
19
  private selectList;
19
- private selectListChildIndex;
20
+ private selectListContainer;
20
21
  private allItems;
21
22
  private onSelect;
22
23
  private onCancel;
@@ -3,15 +3,17 @@
3
3
  * packages/coding-agent/src/modes/interactive/components/thinking-selector.ts.
4
4
  * Modifications: Preserve the searchable thinking-level selector, current/default semantics,
5
5
  * selection, save, cancellation, and focus while accepting the active bare-A1 cycle-key label from the
6
- * shell, styling the title with the established bold semantic accent treatment, placing its muted hint
7
- * directly below it, deduplicating levels, and rendering aligned muted descriptions after adjacent
8
- * active and bracketed default markers. All list and border colors use the owned theme and its
9
- * explicit color mode, and the footer uses the shared bare-A1 modal shortcut row. The comparison
10
- * profile retains the public pinned component.
6
+ * shell, styling the title with the established bold semantic accent treatment, placing it directly
7
+ * below the top rule through the shared compact padded modal frame and its muted hint directly below
8
+ * it, deduplicating levels, and rendering aligned muted descriptions after adjacent active and
9
+ * bracketed default markers. All list and border colors use the owned theme and its explicit color
10
+ * mode, and the footer uses the shared bare-A1 modal shortcut row. The comparison profile retains the
11
+ * public pinned component.
11
12
  * Deviations: owned-modal-shortcut-hints, owned-level-cycle-shortcut, owned-thinking-selector-heading.
12
13
  */
13
14
  import { Container, fuzzyFilter, getKeybindings, Input, SelectList, Spacer, Text, } from "@earendil-works/pi-tui";
14
15
  import { DynamicBorder } from "../../../startup-public.js";
16
+ import { addPiModalHeader, adoptPiModalFrame } from "../../modal-frame.js";
15
17
  import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
16
18
  const THINKING_SELECT_LIST_LAYOUT = {
17
19
  minPrimaryColumnWidth: 12,
@@ -30,7 +32,7 @@ const LEVEL_DESCRIPTIONS = {
30
32
  export class OwnedThinkingSelectorComponent extends Container {
31
33
  searchInput;
32
34
  selectList;
33
- selectListChildIndex;
35
+ selectListContainer;
34
36
  allItems;
35
37
  onSelect;
36
38
  onCancel;
@@ -57,9 +59,7 @@ export class OwnedThinkingSelectorComponent extends Container {
57
59
  label: level,
58
60
  description: LEVEL_DESCRIPTIONS[level],
59
61
  }));
60
- this.addChild(new DynamicBorder((text) => piTheme().fg("border", text)));
61
- this.addChild(new Spacer(1));
62
- this.addChild(new Text(piTheme().fg("accent", piTheme().bold("Thinking Level")), 0, 0));
62
+ const header = addPiModalHeader(this, new DynamicBorder((text) => piTheme().fg("border", text)), new Text(piTheme().fg("accent", piTheme().bold("Thinking Level")), 0, 0));
63
63
  this.addChild(new Text(piTheme().fg("muted", `${cycleKeyDisplay} cycles thinking levels in-session`), 0, 0));
64
64
  this.addChild(new Spacer(1));
65
65
  this.searchInput = new Input();
@@ -67,8 +67,9 @@ export class OwnedThinkingSelectorComponent extends Container {
67
67
  this.addChild(this.searchInput);
68
68
  this.addChild(new Spacer(1));
69
69
  this.selectList = this.buildSelectList(this.allItems, currentLevel);
70
- this.selectListChildIndex = this.children.length;
71
- this.addChild(this.selectList);
70
+ this.selectListContainer = new Container();
71
+ this.selectListContainer.addChild(this.selectList);
72
+ this.addChild(this.selectListContainer);
72
73
  this.addChild(new Spacer(1));
73
74
  this.addChild(new Text(renderPiModalShortcutHints([
74
75
  { key: this.keyDisplayText("tui.select.confirm"), action: "to select" },
@@ -76,6 +77,7 @@ export class OwnedThinkingSelectorComponent extends Container {
76
77
  { key: this.keyDisplayText("tui.select.cancel"), action: "to cancel" },
77
78
  ]), 0, 0));
78
79
  this.addChild(new DynamicBorder((text) => piTheme().fg("border", text)));
80
+ adoptPiModalFrame(this, { topIndex: 0, bottomIndex: this.children.length - 1, header });
79
81
  }
80
82
  keyDisplayText(keybinding) {
81
83
  return getKeybindings().getKeys(keybinding)
@@ -122,7 +124,8 @@ export class OwnedThinkingSelectorComponent extends Container {
122
124
  : this.allItems;
123
125
  const selectedValue = this.selectList.getSelectedItem()?.value;
124
126
  const newList = this.buildSelectList(filtered, selectedValue);
125
- this.children[this.selectListChildIndex] = newList;
127
+ this.selectListContainer.clear();
128
+ this.selectListContainer.addChild(newList);
126
129
  this.selectList = newList;
127
130
  }
128
131
  handleInput(keyData) {
@@ -4,7 +4,7 @@
4
4
  * Modifications: Source-synchronized tree selector port: preserve filtering, folding, labels, copying,
5
5
  * tree navigation, key hints, focus, and viewport behavior while remapping public types/components
6
6
  * plus owned keybindings/theme helpers required to avoid the pinned package nested pi-tui singleton;
7
- * bare A1 uses the shared semantic modal shortcut row.
7
+ * bare A1 uses the shared semantic modal shortcut row and compact padded modal frame.
8
8
  * Deviations: owned-modal-shortcut-hints.
9
9
  */
10
10
  import { type Component, Container, type Focusable } from "@earendil-works/pi-tui";
@@ -4,12 +4,13 @@
4
4
  * Modifications: Source-synchronized tree selector port: preserve filtering, folding, labels, copying,
5
5
  * tree navigation, key hints, focus, and viewport behavior while remapping public types/components
6
6
  * plus owned keybindings/theme helpers required to avoid the pinned package nested pi-tui singleton;
7
- * bare A1 uses the shared semantic modal shortcut row.
7
+ * bare A1 uses the shared semantic modal shortcut row and compact padded modal frame.
8
8
  * Deviations: owned-modal-shortcut-hints.
9
9
  */
10
10
  import { Container, Input, Spacer, sliceByColumn, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
11
11
  import { DynamicBorder } from "../../../startup-public.js";
12
12
  import { KeybindingsManager } from "../adjacent/core/keybindings.js";
13
+ import { addPiModalHeader, adoptPiModalFrame } from "../../modal-frame.js";
13
14
  import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
14
15
  const theme = new Proxy({}, {
15
16
  get(_target, property) {
@@ -1080,8 +1081,8 @@ class TreeHelp {
1080
1081
  return renderPiModalShortcutHints([key ? { key, action: label, ...(labelFirst === undefined ? {} : { actionFirst: labelFirst }) } : { action: label }]);
1081
1082
  });
1082
1083
  const availableWidth = Math.max(1, width);
1083
- // Match the title's Text padding plus its two-cell authored inset.
1084
- const indent = " ";
1084
+ // The frame supplies the shared outer cell; help content needs no duplicate chrome inset.
1085
+ const indent = "";
1085
1086
  const separator = " ";
1086
1087
  const lines = [];
1087
1088
  let currentLine = "";
@@ -1239,16 +1240,22 @@ export class TreeSelectorComponent extends Container {
1239
1240
  this.treeContainer.addChild(this.treeList);
1240
1241
  this.labelInputContainer = new Container();
1241
1242
  this.addChild(new Spacer(1));
1242
- this.addChild(new DynamicBorder());
1243
- this.addChild(new Text(theme.bold(" Session Tree"), 1, 0));
1243
+ const header = addPiModalHeader(this, new DynamicBorder(), new Text(theme.bold("Session Tree"), 0, 0));
1244
1244
  this.addChild(new TreeHelp());
1245
1245
  this.addChild(new SearchLine(this.treeList));
1246
- this.addChild(new DynamicBorder());
1246
+ const separator = new DynamicBorder();
1247
+ this.addChild(separator);
1247
1248
  this.addChild(new Spacer(1));
1248
1249
  this.addChild(this.treeContainer);
1249
1250
  this.addChild(this.labelInputContainer);
1250
1251
  this.addChild(new Spacer(1));
1251
1252
  this.addChild(new DynamicBorder());
1253
+ adoptPiModalFrame(this, {
1254
+ topIndex: 1,
1255
+ bottomIndex: this.children.length - 1,
1256
+ header,
1257
+ fullWidthContent: [separator],
1258
+ });
1252
1259
  if (tree.length === 0) {
1253
1260
  setTimeout(() => onCancel(), 100);
1254
1261
  }
@@ -3,11 +3,12 @@
3
3
  * packages/coding-agent/src/modes/interactive/components/trust-selector.ts.
4
4
  * Modifications: Mechanical source-synchronized trust selector port with injected public
5
5
  * ProjectTrustStore-derived options, remapped owned theme imports, and the shared bare-A1 modal
6
- * shortcut row.
6
+ * shortcut row and compact padded modal frame.
7
7
  * Deviations: owned-modal-shortcut-hints.
8
8
  */
9
9
  import { DynamicBorder } from "../../../startup-public.js";
10
10
  import { Container, getKeybindings, Spacer, Text } from "@earendil-works/pi-tui";
11
+ import { addPiModalHeader, adoptPiModalFrame } from "../../modal-frame.js";
11
12
  import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
12
13
  function formatDecision(trustPath, decision) {
13
14
  if (decision === null)
@@ -29,13 +30,11 @@ export class TrustSelectorComponent extends Container {
29
30
  && options.savedDecision?.decision === option.trusted
30
31
  && options.savedDecision.path === option.savedPath;
31
32
  this.selectedIndex = Math.max(0, this.trustOptions.findIndex(isSaved));
32
- this.addChild(new DynamicBorder());
33
- this.addChild(new Spacer(1));
34
- this.addChild(new Text(piTheme().fg("accent", piTheme().bold("Project trust")), 1, 0));
35
- this.addChild(new Text(piTheme().fg("muted", options.cwd), 1, 0));
33
+ const header = addPiModalHeader(this, new DynamicBorder(), new Text(piTheme().fg("accent", piTheme().bold("Project trust")), 0, 0));
34
+ this.addChild(new Text(piTheme().fg("muted", options.cwd), 0, 0));
36
35
  this.addChild(new Spacer(1));
37
- this.addChild(new Text(piTheme().fg("muted", `Saved decision: ${formatDecision(this.trustOptions[0]?.savedPath, options.savedDecision)}`), 1, 0));
38
- this.addChild(new Text(piTheme().fg("muted", `Current session: ${options.projectTrusted ? "trusted" : "untrusted"}`), 1, 0));
36
+ this.addChild(new Text(piTheme().fg("muted", `Saved decision: ${formatDecision(this.trustOptions[0]?.savedPath, options.savedDecision)}`), 0, 0));
37
+ this.addChild(new Text(piTheme().fg("muted", `Current session: ${options.projectTrusted ? "trusted" : "untrusted"}`), 0, 0));
39
38
  this.addChild(new Spacer(1));
40
39
  this.listContainer = new Container();
41
40
  this.addChild(this.listContainer);
@@ -45,9 +44,10 @@ export class TrustSelectorComponent extends Container {
45
44
  { key: "↑↓", action: "navigate" },
46
45
  { key: keybindings.getKeys("tui.select.confirm").join("/"), action: "save" },
47
46
  { key: keybindings.getKeys("tui.select.cancel").join("/"), action: "cancel" },
48
- ]), 1, 0));
47
+ ]), 0, 0));
49
48
  this.addChild(new Spacer(1));
50
49
  this.addChild(new DynamicBorder());
50
+ adoptPiModalFrame(this, { topIndex: 0, bottomIndex: this.children.length - 1, header });
51
51
  this.updateList(options.savedDecision);
52
52
  this.handleInput = data => {
53
53
  const kb = getKeybindings();
@@ -81,7 +81,7 @@ export class TrustSelectorComponent extends Container {
81
81
  const prefix = selected ? piTheme().fg("accent", "→ ") : " ";
82
82
  const label = selected ? piTheme().fg("accent", option.label) : piTheme().fg("text", option.label);
83
83
  const currentMarker = current ? piTheme().fg("accent", "✓ ") : " ";
84
- this.listContainer.addChild(new Text(`${prefix}${currentMarker}${label}`, 1, 0));
84
+ this.listContainer.addChild(new Text(`${prefix}${currentMarker}${label}`, 0, 0));
85
85
  }
86
86
  }
87
87
  }
@@ -10,7 +10,7 @@ export interface CompactionProgressSession {
10
10
  } | undefined;
11
11
  }
12
12
  export interface CompactionProgressObserver {
13
- /** Marks `compaction_start`: resets the streamed count and captures the expected summary size. */
13
+ /** Marks `compaction_start`: resets the estimate and immediately reports zero percent. */
14
14
  begin(): void;
15
15
  /** Marks `compaction_end`: later stream chunks are ignored. */
16
16
  end(): void;
@@ -18,11 +18,7 @@ export interface CompactionProgressObserver {
18
18
  dispose(): void;
19
19
  }
20
20
  /**
21
- * Observes the summarization stream of a compaction through the session agent's public stream
22
- * function and reports an estimated integer percent. Pi never iterates that stream itself (it
23
- * reads only its final result), so the observer iterates it in the background between
24
- * `begin()` and `end()` and returns the same stream object to Pi. Outside that window the
25
- * wrapper is a pass-through. Returns null when the agent exposes no callable stream function,
26
- * in which case compaction proceeds without progress.
21
+ * Observes summary-stream progress without changing the stream returned to Pi. Returns null
22
+ * when the session exposes no callable public stream function.
27
23
  */
28
24
  export declare function observeCompactionProgress(session: CompactionProgressSession, onProgress: (percent: number) => void): CompactionProgressObserver | null;
@@ -1,14 +1,11 @@
1
1
  /** Expected summary size for a first compaction, when no previous summary on the branch can serve as the estimate. */
2
2
  export const DEFAULT_EXPECTED_COMPACTION_SUMMARY_CHARS = 4000;
3
- /** The percent shown while a compaction is still running never reaches this value. */
4
- const MAX_RUNNING_PERCENT = 99;
3
+ /** Character-based estimates stay below terminal stream completion. */
4
+ const MAX_STREAMING_PERCENT = 99;
5
+ const COMPLETE_PERCENT = 100;
5
6
  /**
6
- * Observes the summarization stream of a compaction through the session agent's public stream
7
- * function and reports an estimated integer percent. Pi never iterates that stream itself (it
8
- * reads only its final result), so the observer iterates it in the background between
9
- * `begin()` and `end()` and returns the same stream object to Pi. Outside that window the
10
- * wrapper is a pass-through. Returns null when the agent exposes no callable stream function,
11
- * in which case compaction proceeds without progress.
7
+ * Observes summary-stream progress without changing the stream returned to Pi. Returns null
8
+ * when the session exposes no callable public stream function.
12
9
  */
13
10
  export function observeCompactionProgress(session, onProgress) {
14
11
  const agent = session.agent;
@@ -21,8 +18,7 @@ export function observeCompactionProgress(session, onProgress) {
21
18
  let streamed = 0;
22
19
  let expected = DEFAULT_EXPECTED_COMPACTION_SUMMARY_CHARS;
23
20
  let reported = null;
24
- const report = () => {
25
- const percent = Math.min(MAX_RUNNING_PERCENT, Math.floor((100 * streamed) / expected));
21
+ const report = (percent = Math.min(MAX_STREAMING_PERCENT, Math.floor((100 * streamed) / expected))) => {
26
22
  if (percent === reported)
27
23
  return;
28
24
  reported = percent;
@@ -45,6 +41,10 @@ export function observeCompactionProgress(session, onProgress) {
45
41
  report();
46
42
  }
47
43
  }
44
+ // Invariant: normal exhaustion is the only observed completion boundary. The real
45
+ // compaction_end event owns work-state exit; errors and stale generations never claim 100%.
46
+ if (observed === generation && active && !disposed)
47
+ report(COMPLETE_PERCENT);
48
48
  }
49
49
  catch {
50
50
  return;
@@ -60,6 +60,7 @@ export function observeCompactionProgress(session, onProgress) {
60
60
  reported = null;
61
61
  expected = latestCompactionSummaryLength(session) ?? DEFAULT_EXPECTED_COMPACTION_SUMMARY_CHARS;
62
62
  active = true;
63
+ report();
63
64
  },
64
65
  end() {
65
66
  active = false;
@@ -3,6 +3,6 @@ export interface PiPullRequestIdentity {
3
3
  readonly url: string;
4
4
  }
5
5
  export type PiPullRequestProbe = (cwd: string, branch: string, signal: AbortSignal) => Promise<PiPullRequestIdentity | null>;
6
- /** Read the open GitHub pull request associated with one exact local branch. */
7
- export declare function readOpenPullRequest(cwd: string, branch: string, signal: AbortSignal, environment?: NodeJS.ProcessEnv): Promise<PiPullRequestIdentity | null>;
8
- export declare function parseOpenPullRequest(stdout: string, branch: string): PiPullRequestIdentity | null;
6
+ /** Read an exact branch's open or merged PR. */
7
+ export declare function readPullRequest(cwd: string, branch: string, signal: AbortSignal, environment?: NodeJS.ProcessEnv): Promise<PiPullRequestIdentity | null>;
8
+ export declare function parsePullRequest(stdout: string, branch: string): PiPullRequestIdentity | null;
@@ -2,19 +2,19 @@ import { execFile } from "node:child_process";
2
2
  import { PRODUCT_IDENTITY } from "../../../product-identity.js";
3
3
  const GH_TIMEOUT_MS = 5_000;
4
4
  const GH_MAX_BUFFER_BYTES = 64 * 1024;
5
- /** Read the open GitHub pull request associated with one exact local branch. */
6
- export async function readOpenPullRequest(cwd, branch, signal, environment = process.env) {
5
+ /** Read an exact branch's open or merged PR. */
6
+ export async function readPullRequest(cwd, branch, signal, environment = process.env) {
7
7
  if (branch.length === 0 || signal.aborted)
8
8
  return null;
9
9
  const preview = environment[PRODUCT_IDENTITY.environment.prFooterPreview];
10
10
  if (preview !== undefined)
11
- return parseOpenPullRequest(preview, branch);
11
+ return parsePullRequest(preview, branch);
12
12
  const stdout = await executeGh(cwd, signal);
13
13
  if (stdout === null)
14
14
  return null;
15
- return parseOpenPullRequest(stdout, branch);
15
+ return parsePullRequest(stdout, branch);
16
16
  }
17
- export function parseOpenPullRequest(stdout, branch) {
17
+ export function parsePullRequest(stdout, branch) {
18
18
  let value;
19
19
  try {
20
20
  value = JSON.parse(stdout);
@@ -25,7 +25,7 @@ export function parseOpenPullRequest(stdout, branch) {
25
25
  if (!isRecord(value)
26
26
  || !Number.isSafeInteger(value.number)
27
27
  || value.number <= 0
28
- || value.state !== "OPEN"
28
+ || (value.state !== "OPEN" && value.state !== "MERGED")
29
29
  || value.headRefName !== branch
30
30
  || typeof value.url !== "string")
31
31
  return null;
@@ -6,7 +6,7 @@ import { observeCompactionProgress } from "./compaction-progress.js";
6
6
  import { createPiRuntimeIntegration } from "./runtime-integration.js";
7
7
  import { PiSessionCommandIntegration } from "./session-integration.js";
8
8
  import { DefaultPackageManager, VERSION } from "../startup-public.js";
9
- import { readOpenPullRequest, } from "./repository-pr.js";
9
+ import { readPullRequest, } from "./repository-pr.js";
10
10
  const execFileAsync = promisify(execFile);
11
11
  const PULL_REQUEST_REFRESH_MS = 60_000;
12
12
  const REPOSITORY_CONTEXT_POLL_MS = 5_000;
@@ -56,7 +56,7 @@ export class PiEngineRuntime {
56
56
  ?? (options.createRuntime
57
57
  ? async () => []
58
58
  : settingsManager => checkDefaultPiPackageUpdates(this.#cwd, options.agentDir, settingsManager));
59
- this.#pullRequestProbe = options.pullRequestProbe ?? readOpenPullRequest;
59
+ this.#pullRequestProbe = options.pullRequestProbe ?? readPullRequest;
60
60
  this.#pullRequestRefreshMs = options.pullRequestRefreshMs ?? PULL_REQUEST_REFRESH_MS;
61
61
  this.#repositoryContextPollMs = options.repositoryContextPollMs ?? REPOSITORY_CONTEXT_POLL_MS;
62
62
  this.#repositoryContextReader = options.repositoryContextReader ?? (async () => null);
@@ -393,6 +393,8 @@ function samePullRequest(left, right) {
393
393
  return left?.number === right?.number && left?.url === right?.url;
394
394
  }
395
395
  async function readGitBranch(cwd, signal) {
396
+ if (!existsSync(cwd))
397
+ return null;
396
398
  try {
397
399
  const { stdout } = await execFileAsync("git", ["branch", "--show-current"], {
398
400
  cwd,
@@ -402,8 +404,7 @@ async function readGitBranch(cwd, signal) {
402
404
  signal,
403
405
  encoding: "utf8",
404
406
  });
405
- const branch = stdout.trim();
406
- return branch.length > 0 ? branch : null;
407
+ return stdout.trim() || null;
407
408
  }
408
409
  catch {
409
410
  return null;
@@ -35,7 +35,7 @@ export const PI_SETTING_EFFECTS = Object.freeze({
35
35
  tuiMode: hiddenEffect("next-session", "shell", "pinned regular/fullscreen selector and terminal lifecycle", "pi-terminal-operation-parity"),
36
36
  fullscreenExitOutput: hiddenEffect("current-exit", "shutdown", "pinned styled transcript and compact dim resume hint", "pinned-fullscreen-exit-parity"),
37
37
  fullscreenScrollbar: hiddenEffect("live", "shell", "pinned fullscreen scrollbar reservation", "pi-terminal-operation-parity"),
38
- fullscreenCopyOnSelect: hiddenEffect("live", "shell", "pinned fullscreen copy-on-select toggle", "pi-terminal-operation-parity"),
38
+ fullscreenCopyOnSelect: effect("live", "shell", "terminal-status", "pinned fullscreen copy-on-select toggle and copy acknowledgement", "pi-terminal-operation-parity"),
39
39
  warnings: effect("live", "agent", "transcript-notice", "pinned warning rows by warning part", "pinned-transcript-lifecycle-parity"),
40
40
  });
41
41
  function effect(application, owner, visualClass, pinnedSurface, evidence) {
@@ -219,7 +219,7 @@ export class PiEngineSettings {
219
219
  tuiMode: setting(settings?.getTuiMode, "regular"),
220
220
  fullscreenExitOutput: setting(settings?.getFullscreenExitOutput, "transcript"),
221
221
  fullscreenScrollbar: setting(settings?.getFullscreenScrollbar, "auto"),
222
- fullscreenCopyOnSelect: setting(settings?.getFullscreenCopyOnSelect, false),
222
+ fullscreenCopyOnSelect: setting(settings?.getFullscreenCopyOnSelect, true),
223
223
  warnings: setting(settings?.getWarnings, { anthropicExtraUsage: true }),
224
224
  };
225
225
  }
@@ -47,6 +47,8 @@ export declare class PiTuiRuntimeAdapter {
47
47
  getClearOnShrink(): boolean;
48
48
  setTerminalProgress(active: boolean): void;
49
49
  setTitle(title: string): void;
50
+ /** Shows a bounded fullscreen acknowledgement without changing root layout geometry. */
51
+ showFlash(message: string, durationMs?: number): void;
50
52
  writeAfterStop(text: string): void;
51
53
  invalidate(): void;
52
54
  requestRender(force?: boolean): void;
@@ -1,5 +1,5 @@
1
1
  import { HStack, ProcessTerminal, ScrollView, TuiAltScreen, TuiMainScreen, VStack, visibleWidth, } from "@earendil-works/pi-tui";
2
- import { MouseReportInput } from "./mouse-report-input.js";
2
+ import { MouseReportInput, stripSgrMouseReports } from "./mouse-report-input.js";
3
3
  import { GeometryObservedAltScreen, OverlayGeometryTracker } from "./overlay-geometry.js";
4
4
  import { boundedCleanup, EMERGENCY_TERMINAL_RESET } from "../../../foundation/terminal-cleanup/terminal-reset.js";
5
5
  import { InputPresentationCoordinator, } from "./input-presentation-coordinator.js";
@@ -108,6 +108,7 @@ export class PiTuiRuntimeAdapter {
108
108
  #overlayInputCoordination = new Map();
109
109
  #inputListeners = new Map();
110
110
  #preInputListeners = new Set();
111
+ #consumeUnhandledMouse;
111
112
  #inputCoordinator;
112
113
  #inputDiagnostics;
113
114
  #diagnosticNow;
@@ -121,6 +122,7 @@ export class PiTuiRuntimeAdapter {
121
122
  this.#root = options.root;
122
123
  this.#overlayGeometry = options.onOverlayGeometry === undefined ? undefined : new OverlayGeometryTracker(options.onOverlayGeometry);
123
124
  this.#terminal = options.terminal ?? new ProcessTerminal();
125
+ this.#consumeUnhandledMouse = options.consumeUnhandledMouse ?? false;
124
126
  this.#inputDiagnostics = options.inputDiagnostics;
125
127
  this.#diagnosticNow = options.inputDiagnostics?.now ?? (() => performance.now());
126
128
  // Invariant: the pinned renderer's frames pass through this gate, while writeControl and
@@ -392,6 +394,11 @@ export class PiTuiRuntimeAdapter {
392
394
  setTitle(title) {
393
395
  this.#terminal.setTitle(title);
394
396
  }
397
+ /** Shows a bounded fullscreen acknowledgement without changing root layout geometry. */
398
+ showFlash(message, durationMs) {
399
+ this.#assertRunning("flash");
400
+ this.#requireFullscreen().flash(message, durationMs);
401
+ }
395
402
  writeAfterStop(text) {
396
403
  if (this.#state !== "stopped")
397
404
  throw new Error("terminal output requires a stopped Pi TUI runtime");
@@ -484,7 +491,9 @@ export class PiTuiRuntimeAdapter {
484
491
  else if (result?.consume === true)
485
492
  current = "";
486
493
  }
487
- return current;
494
+ // Invariant: bare A1 owns every fullscreen pointer path. Listeners keep first
495
+ // refusal, then this boundary prevents Pi's reverse-video fallback selection.
496
+ return this.#consumeUnhandledMouse ? stripSgrMouseReports(current) : current;
488
497
  }
489
498
  #focusedOverlayInputCoordination() {
490
499
  for (const [bridge, coordination] of this.#overlayInputCoordination) {
@@ -143,6 +143,8 @@ export interface PiTuiRuntimeAdapterOptions {
143
143
  };
144
144
  readonly hardwareCursor?: boolean;
145
145
  readonly mouse?: boolean;
146
+ /** Consume residual SGR mouse reports after every pre-input listener has run. */
147
+ readonly consumeUnhandledMouse?: boolean;
146
148
  readonly wheelScrollLines?: number;
147
149
  readonly openUrl?: (url: string) => void;
148
150
  readonly onRightClickPaste?: () => void;
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Removes complete SGR mouse reports outside bracketed paste while preserving
3
+ * every adjacent byte in order. Incomplete reports remain available to the
4
+ * caller's framing boundary rather than being guessed at here.
5
+ */
6
+ export declare function stripSgrMouseReports(data: string): string;
1
7
  /**
2
8
  * Keeps mouse reports and adjacent keyboard input in physical delivery order.
3
9
  * ProcessTerminal already frames escape/paste sequences; this also handles hosts
@@ -1,3 +1,37 @@
1
+ const BRACKETED_PASTE_START = "\u001b[200~";
2
+ const BRACKETED_PASTE_END = "\u001b[201~";
3
+ const SGR_MOUSE_REPORT_PATTERN = /\u001b\[<\d+;\d+;\d+[Mm]/g;
4
+ /**
5
+ * Removes complete SGR mouse reports outside bracketed paste while preserving
6
+ * every adjacent byte in order. Incomplete reports remain available to the
7
+ * caller's framing boundary rather than being guessed at here.
8
+ */
9
+ export function stripSgrMouseReports(data) {
10
+ if (!data.includes("\u001b[<"))
11
+ return data;
12
+ let output = "";
13
+ let from = 0;
14
+ while (from < data.length) {
15
+ const pasteStart = data.indexOf(BRACKETED_PASTE_START, from);
16
+ SGR_MOUSE_REPORT_PATTERN.lastIndex = from;
17
+ const report = SGR_MOUSE_REPORT_PATTERN.exec(data);
18
+ if (pasteStart >= 0 && (report === null || pasteStart < report.index)) {
19
+ output += data.slice(from, pasteStart);
20
+ const pasteEnd = data.indexOf(BRACKETED_PASTE_END, pasteStart + BRACKETED_PASTE_START.length);
21
+ if (pasteEnd < 0)
22
+ return output + data.slice(pasteStart);
23
+ const afterPaste = pasteEnd + BRACKETED_PASTE_END.length;
24
+ output += data.slice(pasteStart, afterPaste);
25
+ from = afterPaste;
26
+ continue;
27
+ }
28
+ if (report === null)
29
+ return output + data.slice(from);
30
+ output += data.slice(from, report.index);
31
+ from = report.index + report[0].length;
32
+ }
33
+ return output;
34
+ }
1
35
  /**
2
36
  * Keeps mouse reports and adjacent keyboard input in physical delivery order.
3
37
  * ProcessTerminal already frames escape/paste sequences; this also handles hosts
@@ -5,7 +5,7 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-23T16:42:04.513Z",
8
+ "builtAt": "2026-09-24T07:17:37.739Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "9db1726bbe3fc2e8292f2ead1e7e9d0fd4d7d0dc9217b372582bf354b0f565dc",
@@ -5,7 +5,7 @@
5
5
  "platform": "linux",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-23T16:42:11.967Z",
8
+ "builtAt": "2026-09-24T07:17:22.316Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "d8cda6b0c7cb36c0cc41e90802aceebf6c02eaebbc08a83d7d963d2e918dbd7a",
@@ -5,11 +5,11 @@
5
5
  "platform": "win32",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-23T16:43:16.752Z",
8
+ "builtAt": "2026-09-24T07:18:02.282Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian.exe",
11
- "sha256": "051b57f8b1980f1cf2accf7d9f102941e5fcf74b4264f9375d924581e950b0d6",
12
- "size": 177664
11
+ "sha256": "c7d92e5f9cec0d36ec4482b25e518464acb4e1bbc5bdd6701f1e04ada7775950",
12
+ "size": 178176
13
13
  },
14
14
  "provenance": {
15
15
  "language": "Rust",
@@ -13,6 +13,10 @@ export function dialogValueColumn(rows) {
13
13
  export function renderDialogPanel(state, width, theme) {
14
14
  const labelColumn = Math.min(LABEL_COLUMN_CAP, Math.max(0, ...state.rows.map(row => displayWidth(row.label))));
15
15
  const rule = theme.fg("border", "─".repeat(Math.max(0, width)));
16
+ const contentPadding = Math.min(1, Math.max(0, width));
17
+ const contentWidth = Math.max(0, width - contentPadding);
18
+ const inset = " ".repeat(contentPadding);
19
+ const contentRow = (painted, raw) => `${inset}${pad(truncateToWidth(painted, contentWidth), contentWidth, raw)}`;
16
20
  const rows = state.rows.map((row, index) => {
17
21
  const selected = index === state.index;
18
22
  const padded = `${row.label}${" ".repeat(Math.max(0, labelColumn - displayWidth(row.label)))}`;
@@ -23,20 +27,20 @@ export function renderDialogPanel(state, width, theme) {
23
27
  const painted = selected
24
28
  ? `${theme.fg("accent", cursor)}${theme.fg("accent", padded)} ${theme.fg("accent", row.value)}`
25
29
  : `${cursor}${padded} ${theme.fg("muted", row.value)}`;
26
- return pad(truncateToWidth(painted, width), width, raw);
30
+ return contentRow(painted, raw);
27
31
  });
28
32
  const description = state.rows[state.index]?.description ?? "";
29
33
  const hint = typeof state.hint === "string"
30
- ? theme.fg("dim", ` ${state.hint}`)
31
- : renderShortcutHints(state.hint, theme, 1);
32
- const plainHint = typeof state.hint === "string" ? ` ${state.hint}` : shortcutHintsText(state.hint, 1);
34
+ ? theme.fg("dim", state.hint)
35
+ : renderShortcutHints(state.hint, theme, 0);
36
+ const plainHint = typeof state.hint === "string" ? state.hint : shortcutHintsText(state.hint, 0);
33
37
  return [
34
38
  rule,
35
39
  ...rows,
36
40
  "",
37
- pad(truncateToWidth(theme.fg("dim", ` ${description}`), width), width, ` ${description}`),
41
+ contentRow(theme.fg("dim", ` ${description}`), ` ${description}`),
38
42
  "",
39
- pad(truncateToWidth(hint, width), width, plainHint),
43
+ contentRow(hint, plainHint),
40
44
  rule,
41
45
  ];
42
46
  }