@timurproko/a1 0.1.8-dev.136 → 0.1.8-dev.139

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 (81) hide show
  1. package/README.md +2 -4
  2. package/dist/cli/capabilities.d.ts +2 -11
  3. package/dist/cli/capabilities.js +1 -1
  4. package/dist/cli/dispatch.js +3 -6
  5. package/dist/composition/owned-ui.js +19 -1
  6. package/dist/contracts/owned-ui/model.d.ts +21 -0
  7. package/dist/contracts/owned-ui/validation.js +3 -0
  8. package/dist/features/launch/development-launch.js +11 -3
  9. package/dist/features/launch/index.d.ts +0 -1
  10. package/dist/features/launch/index.js +0 -1
  11. package/dist/features/launch/intent.d.ts +3 -2
  12. package/dist/features/launch/intent.js +1 -2
  13. package/dist/features/launch/prepare-launch.d.ts +0 -2
  14. package/dist/features/launch/prepare-launch.js +2 -9
  15. package/dist/features/launch/profile-paths.d.ts +1 -2
  16. package/dist/features/launch/profile-paths.js +2 -8
  17. package/dist/features/launch/runtime-selection.d.ts +2 -5
  18. package/dist/features/owned-ui/settings-app.js +36 -13
  19. package/dist/foundation/lifecycle/model.d.ts +1 -1
  20. package/dist/foundation/lifecycle/model.js +1 -1
  21. package/dist/foundation/release/bootstrap.d.ts +1 -3
  22. package/dist/foundation/release/bootstrap.js +12 -4
  23. package/dist/foundation/storage/control-store.js +37 -3
  24. package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
  25. package/dist/integrations/pi/components/shell-footer-status.js +14 -5
  26. package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +3 -2
  27. package/dist/integrations/pi/components/shell-presenters-transcript.js +9 -8
  28. package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +13 -0
  29. package/dist/integrations/pi/components/submitted-prompt-adapter.js +27 -0
  30. package/dist/integrations/pi/engine/adapter.js +16 -2
  31. package/dist/integrations/pi/engine/session-integration.js +5 -2
  32. package/dist/integrations/pi/owned-ui/session-shell-root.d.ts +17 -2
  33. package/dist/integrations/pi/owned-ui/session-shell-root.js +451 -42
  34. package/dist/integrations/pi/owned-ui/session-shell.js +116 -13
  35. package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -1
  36. package/dist/integrations/pi/tui-runtime/adapter.js +51 -2
  37. package/dist/integrations/pi/tui-runtime/contracts.d.ts +2 -0
  38. package/dist/native/darwin-arm64/manifest.json +1 -1
  39. package/dist/native/linux-x64/manifest.json +1 -1
  40. package/dist/native/win32-x64/manifest.json +2 -2
  41. package/dist/native/win32-x64/process-guardian.exe +0 -0
  42. package/dist/product-identity.d.ts +2 -2
  43. package/dist/product-identity.js +3 -4
  44. package/dist/product-identity.json +3 -5
  45. package/dist/ui/components/index.d.ts +3 -0
  46. package/dist/ui/components/index.js +3 -0
  47. package/dist/ui/components/mouse.d.ts +10 -0
  48. package/dist/ui/components/mouse.js +23 -0
  49. package/dist/ui/components/prompt-row.d.ts +14 -0
  50. package/dist/ui/components/prompt-row.js +20 -0
  51. package/dist/ui/components/scrollbar.d.ts +26 -0
  52. package/dist/ui/components/scrollbar.js +35 -0
  53. package/dist/ui/components/spans.d.ts +6 -0
  54. package/dist/ui/components/spans.js +36 -0
  55. package/dist/ui/components/submitted-prompt.d.ts +23 -0
  56. package/dist/ui/components/submitted-prompt.js +50 -0
  57. package/dist/ui/components/text-selection.d.ts +53 -0
  58. package/dist/ui/components/text-selection.js +168 -0
  59. package/dist/ui/components/text.d.ts +14 -0
  60. package/dist/ui/components/text.js +40 -0
  61. package/dist/ui/components/transcript-viewport.d.ts +98 -0
  62. package/dist/ui/components/transcript-viewport.js +353 -0
  63. package/dist/ui/settings/declarations.d.ts +8 -1
  64. package/dist/ui/settings/declarations.js +33 -5
  65. package/dist/ui/settings/migrations.d.ts +1 -1
  66. package/dist/ui/settings/migrations.js +12 -2
  67. package/dist/ui/settings/sections.js +15 -8
  68. package/dist/ui/settings/session.d.ts +2 -0
  69. package/dist/ui/settings/session.js +7 -5
  70. package/docs/architecture/boundaries.md +2 -2
  71. package/docs/architecture/project-structure.md +1 -1
  72. package/docs/architecture/resource-and-data-policy.md +1 -1
  73. package/docs/architecture/toolchain.md +5 -5
  74. package/docs/architecture/ui-reference-provenance.md +11 -2
  75. package/docs/features/launch-profiles.md +7 -14
  76. package/docs/manual-launch-instance-acceptance.md +12 -35
  77. package/docs/manual-owned-ui-checkpoint.md +2 -3
  78. package/docs/manual-pi-boundary-candidate.md +4 -5
  79. package/package.json +1 -2
  80. package/dist/features/launch/profiles.d.ts +0 -12
  81. package/dist/features/launch/profiles.js +0 -26
@@ -15,12 +15,16 @@ export class OwnedUiSessionShell {
15
15
  #stopped;
16
16
  #resolveStopped;
17
17
  #dialogId;
18
+ #dialogSource;
18
19
  #routeHost;
19
20
  #dialogHandle;
20
21
  #sequence = 0;
21
22
  #started = false;
22
23
  #disposed = false;
23
24
  #pointerReporting = false;
25
+ #customViewport;
26
+ #removeViewportPreInput;
27
+ #unsubscribeSettings;
24
28
  #compactionQueue = [];
25
29
  #lastClearTime = 0;
26
30
  #activeLoginDialog;
@@ -30,6 +34,7 @@ export class OwnedUiSessionShell {
30
34
  this.#sessionGeneration = this.backend.sessionGeneration;
31
35
  this.#cwd = options.cwd;
32
36
  this.#routeHost = options.routeHost ?? null;
37
+ this.#customViewport = options.sessionLayout === "custom-viewport";
33
38
  this.#stopped = new Promise(resolve => {
34
39
  this.#resolveStopped = resolve;
35
40
  });
@@ -58,13 +63,50 @@ export class OwnedUiSessionShell {
58
63
  }, this.backend.agentDir, {
59
64
  getMessageRenderer: customType => this.backend.pinnedMessageRenderer(customType),
60
65
  getToolDefinition: toolName => this.backend.pinnedToolDefinition(toolName),
61
- });
62
- const tuiMode = this.backend.disposed ? "regular" : this.backend.pinnedSettingsSnapshot().tuiMode;
63
- const runtimeOptions = options.terminal === undefined
64
- ? { root: this.root, mode: tuiMode, layoutRoot: this.root.layoutRoot(), hardwareCursor: this.backend.view().terminal.hardwareCursor }
65
- : { root: this.root, mode: tuiMode, layoutRoot: this.root.layoutRoot(), terminal: options.terminal, hardwareCursor: this.backend.view().terminal.hardwareCursor };
66
+ }, options.sessionLayout);
67
+ // Bare A1 owns a bounded viewport and therefore always runs on the alternate
68
+ // fullscreen surface. The pinned comparison profiles still honor Pi's mode.
69
+ const tuiMode = this.#customViewport
70
+ ? "fullscreen"
71
+ : this.backend.disposed ? "regular" : this.backend.pinnedSettingsSnapshot().tuiMode;
72
+ const runtimeOptions = {
73
+ root: this.root,
74
+ mode: tuiMode,
75
+ ...(this.#customViewport ? {} : { layoutRoot: this.root.layoutRoot() }),
76
+ ...(options.terminal === undefined ? {} : { terminal: options.terminal }),
77
+ hardwareCursor: this.backend.view().terminal.hardwareCursor,
78
+ };
66
79
  runtime = new PiTuiRuntimeAdapter(runtimeOptions);
67
80
  this.runtime = runtime;
81
+ this.#removeViewportPreInput = this.#customViewport
82
+ ? this.runtime.addPreInputListener(data => {
83
+ // An overlay or editor-replacement screen owns its entire pointer
84
+ // surface. Letting the transcript pre-router inspect those reports
85
+ // steals settings value menus and numeric +/- controls before the
86
+ // settings app can receive them.
87
+ if (this.runtime.hasOverlay() || !this.root.usesDefaultInputSurface())
88
+ return undefined;
89
+ const routed = this.root.handleViewportPreInput(data, true);
90
+ if (routed.copyText !== undefined) {
91
+ this.runtime.writeControl(`\u001b]52;c;${Buffer.from(routed.copyText, "utf8").toString("base64")}\u0007`);
92
+ }
93
+ if (!routed.consumed)
94
+ return routed.data === data ? undefined : { data: routed.data };
95
+ return routed.data.length === 0 ? { consume: true } : { data: routed.data };
96
+ })
97
+ : () => { };
98
+ const applyViewportSettings = () => {
99
+ const snapshot = options.viewportSettings?.snapshot();
100
+ this.root.setViewportConfig(snapshot ?? {
101
+ scrollbarAppearance: "hover",
102
+ scrollbarStyle: "thin",
103
+ scrollbarSpeed: "normal",
104
+ });
105
+ };
106
+ applyViewportSettings();
107
+ this.#unsubscribeSettings = this.#customViewport && options.viewportSettings
108
+ ? options.viewportSettings.onChange(settings => this.root.setViewportConfig(settings))
109
+ : () => { };
68
110
  this.#extensionBridge = createPiExtensionUiBridge({
69
111
  runtime: {
70
112
  getColumns: () => this.runtime.viewport().columns,
@@ -102,9 +144,14 @@ export class OwnedUiSessionShell {
102
144
  // A streamed chunk names one block, and touching only that block is what keeps the
103
145
  // cost of a chunk the same in a long session as in a new one. Everything else
104
146
  // resynchronizes the view, which is cheap next to re-reading the transcript.
147
+ if (event.type === "agent-run-started")
148
+ this.root.resumeViewportFollowing();
149
+ if (event.type === "assistant-message-completed")
150
+ this.root.noteCompletedAssistantMessage();
151
+ const semanticOnly = event.type === "agent-run-started" || event.type === "assistant-message-completed";
105
152
  const view = event.type === "transcript-block" && this.#sessionGeneration === this.backend.sessionGeneration
106
153
  ? this.#syncBlock(event.block)
107
- : this.#syncView();
154
+ : semanticOnly ? this.view() : this.#syncView();
108
155
  if (view.lifecycle === "ready" && this.#compactionQueue.length > 0)
109
156
  void this.#flushCompactionQueue();
110
157
  if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
@@ -121,6 +168,8 @@ export class OwnedUiSessionShell {
121
168
  return;
122
169
  this.#started = true;
123
170
  this.runtime.start();
171
+ if (this.#customViewport)
172
+ this.#setPointerReporting(true);
124
173
  void this.backend.bindExtensionUi(this.#extensionBridge.context, () => { void this.shutdown(); });
125
174
  this.#syncView();
126
175
  }
@@ -172,6 +221,7 @@ export class OwnedUiSessionShell {
172
221
  }
173
222
  const type = this.view().lifecycle === "busy" ? "steer" : "prompt";
174
223
  this.root.editor.addToHistory(input);
224
+ this.root.resumeViewportFollowing();
175
225
  return this.#execute({
176
226
  type,
177
227
  correlationId: this.#correlation(type),
@@ -251,6 +301,7 @@ export class OwnedUiSessionShell {
251
301
  return rejected("nothing to queue");
252
302
  this.root.editor.addToHistory(text);
253
303
  this.root.editor.setText("");
304
+ this.root.resumeViewportFollowing();
254
305
  if (this.view().status.workingMessage?.startsWith("Compacting") === true) {
255
306
  this.#compactionQueue.push({ text, type: "follow-up" });
256
307
  return { outcome: "completed", diagnostic: null };
@@ -272,6 +323,8 @@ export class OwnedUiSessionShell {
272
323
  }
273
324
  showSelector(title, options, onSelect, onCancel) {
274
325
  this.#dialogHandle?.hide();
326
+ this.#dialogSource = "local";
327
+ this.#dialogId = undefined;
275
328
  const component = createPiShellSelector({
276
329
  title,
277
330
  options,
@@ -279,10 +332,12 @@ export class OwnedUiSessionShell {
279
332
  onSelect(id);
280
333
  this.#dialogHandle?.hide();
281
334
  this.#dialogHandle = undefined;
335
+ this.#dialogSource = undefined;
282
336
  },
283
337
  onCancel: () => {
284
338
  this.#dialogHandle?.hide();
285
339
  this.#dialogHandle = undefined;
340
+ this.#dialogSource = undefined;
286
341
  onCancel?.();
287
342
  },
288
343
  });
@@ -486,6 +541,10 @@ export class OwnedUiSessionShell {
486
541
  return;
487
542
  }
488
543
  if (callback === "onTuiModeChange") {
544
+ // The custom bare-A1 surface is permanently fullscreen; this callback
545
+ // remains available only to pinned comparison profiles.
546
+ if (this.#customViewport)
547
+ return;
489
548
  if (value !== "regular" && value !== "fullscreen")
490
549
  return;
491
550
  if (!this.runtime.switchMode(value)) {
@@ -493,6 +552,10 @@ export class OwnedUiSessionShell {
493
552
  this.runtime.requestRender();
494
553
  return;
495
554
  }
555
+ if (this.#customViewport) {
556
+ this.#pointerReporting = false;
557
+ this.#setPointerReporting(true);
558
+ }
496
559
  }
497
560
  const result = this.backend.applyPinnedSettingValue(callback, value);
498
561
  if (result.outcome === "failed")
@@ -704,19 +767,23 @@ export class OwnedUiSessionShell {
704
767
  * button instead of scrolling and selecting itself, so leaving it on outlives the
705
768
  * screen that wanted it and takes the terminal's own scrolling and selection with it.
706
769
  */
707
- #setPointerReporting(enabled) {
708
- if (this.#pointerReporting === enabled)
770
+ #setPointerReporting(enabled, forceOff = false) {
771
+ const effective = forceOff ? false : this.#customViewport || enabled;
772
+ if (this.#pointerReporting === effective)
709
773
  return;
710
- this.#pointerReporting = enabled;
774
+ this.#pointerReporting = effective;
711
775
  if (!this.runtime.active)
712
776
  return;
713
- this.runtime.writeControl(enabled ? MOUSE_TRACKING_ON : MOUSE_TRACKING_OFF);
777
+ this.runtime.writeControl(effective ? MOUSE_TRACKING_ON : MOUSE_TRACKING_OFF);
714
778
  }
715
779
  async dispose() {
716
780
  if (this.#disposed)
717
781
  return;
718
782
  this.#disposed = true;
719
- this.#setPointerReporting(false);
783
+ this.root.clearViewportPointerState();
784
+ this.#setPointerReporting(false, true);
785
+ this.#removeViewportPreInput();
786
+ this.#unsubscribeSettings();
720
787
  this.#unsubscribe();
721
788
  this.#dialogHandle?.hide();
722
789
  await this.backend.unbindExtensionUi();
@@ -741,7 +808,12 @@ export class OwnedUiSessionShell {
741
808
  this.#sessionGeneration = this.backend.sessionGeneration;
742
809
  this.#activeLoginDialog = undefined;
743
810
  this.#extensionBridge.reset();
744
- // A replaced session takes its screens with it, pointer reporting included.
811
+ // A replaced session takes its transient viewport and owned-route state with it.
812
+ this.root.resetViewport();
813
+ this.#dialogHandle?.hide();
814
+ this.#dialogHandle = undefined;
815
+ this.#dialogId = undefined;
816
+ this.#dialogSource = undefined;
745
817
  this.#setPointerReporting(false);
746
818
  this.root.setInputSurface(null);
747
819
  this.root.resetExtensionUi();
@@ -761,6 +833,7 @@ export class OwnedUiSessionShell {
761
833
  if (!this.runtime.active)
762
834
  return { outcome: "failed", diagnostic: "runtime is not active" };
763
835
  this.#dialogHandle?.hide();
836
+ this.#dialogSource = "route";
764
837
  // Any-event reporting: hover and drag are what the screen is driven by, and
765
838
  // it also stops the terminal treating a drag as a text selection.
766
839
  this.#setPointerReporting(true);
@@ -784,13 +857,32 @@ export class OwnedUiSessionShell {
784
857
  // stray interrupt while it is up.
785
858
  return { consume: true };
786
859
  });
860
+ let removeSurfacePreInput = () => { };
787
861
  const closeSurface = () => {
862
+ removeSurfacePreInput();
788
863
  removeInterruptWatch();
789
864
  this.#setPointerReporting(false);
790
865
  this.#dialogHandle?.hide();
791
866
  this.#dialogHandle = undefined;
792
867
  this.#dialogId = undefined;
868
+ this.#dialogSource = undefined;
793
869
  };
870
+ // Fullscreen Pi owns a fallback text-selection layer before focused overlay
871
+ // components see pointer input. Route every mouse report to the owned screen
872
+ // at the pre-input boundary so dropdowns, value hover, and numeric +/- work,
873
+ // and consume even unhandled reports so settings content is never selected.
874
+ removeSurfacePreInput = this.runtime.addPreInputListener(data => {
875
+ const { events, rest } = parseMouseInput(data);
876
+ if (events.length === 0)
877
+ return undefined;
878
+ for (const event of events)
879
+ surface.handleMouse(event);
880
+ if (surface.isClosed())
881
+ closeSurface();
882
+ else
883
+ this.runtime.requestRender();
884
+ return rest.length === 0 ? { consume: true } : { data: rest };
885
+ });
794
886
  const rows = () => Math.max(1, this.runtime.viewport().rows);
795
887
  const component = {
796
888
  render: (width) => [...surface.render(Math.max(1, width), rows())],
@@ -821,22 +913,32 @@ export class OwnedUiSessionShell {
821
913
  if (!this.runtime.active)
822
914
  return;
823
915
  if (dialog === null) {
916
+ // Locally owned routes (notably /settings) are independent of backend
917
+ // lifecycle/status events and remain open while an agent is working.
918
+ if (this.#dialogSource !== "backend")
919
+ return;
824
920
  this.#dialogHandle?.hide();
825
921
  this.#dialogHandle = undefined;
826
922
  this.#dialogId = undefined;
923
+ this.#dialogSource = undefined;
827
924
  return;
828
925
  }
829
- if (this.#dialogId === dialog.id)
926
+ if (this.#dialogSource === "backend" && this.#dialogId === dialog.id)
830
927
  return;
831
928
  this.#dialogHandle?.hide();
929
+ this.#dialogSource = "backend";
832
930
  const component = createPiShellDialog(dialog, {
833
931
  onSelect: () => {
834
932
  this.#dialogHandle?.hide();
835
933
  this.#dialogHandle = undefined;
934
+ this.#dialogId = undefined;
935
+ this.#dialogSource = undefined;
836
936
  },
837
937
  onCancel: () => {
838
938
  this.#dialogHandle?.hide();
839
939
  this.#dialogHandle = undefined;
940
+ this.#dialogId = undefined;
941
+ this.#dialogSource = undefined;
840
942
  },
841
943
  });
842
944
  this.#dialogHandle = this.runtime.showOverlay(component, { width: "70%", maxHeight: "80%", anchor: "center" });
@@ -853,6 +955,7 @@ export class OwnedUiSessionShell {
853
955
  return this.runWorkflow({ command: name, argument });
854
956
  // Unknown slash input, prompt templates, skills, and extension commands remain Pi prompt input.
855
957
  this.root.editor.addToHistory(text);
958
+ this.root.resumeViewportFollowing();
856
959
  return this.#execute({
857
960
  type: this.view().lifecycle === "busy" ? "steer" : "prompt",
858
961
  correlationId: this.#correlation("prompt-command"),
@@ -1,4 +1,4 @@
1
- import type { PiTuiComponentPort, PiTuiInputListener, PiTuiOverlayHandle, PiTuiOverlayOptions, PiTuiRuntimeAdapterOptions, PiTuiRuntimeState, PiTuiScrollState, PiTuiStopOptions, PiTuiViewport } from "./contracts.js";
1
+ import type { PiTuiComponentPort, PiTuiInputListener, PiTuiOverlayHandle, PiTuiOverlayOptions, PiTuiPreInputListener, PiTuiRuntimeAdapterOptions, PiTuiRuntimeState, PiTuiScrollState, PiTuiStopOptions, PiTuiViewport } from "./contracts.js";
2
2
  export type PiTuiRuntimeErrorStage = "construction" | "start" | "input-drain" | "restoration";
3
3
  export declare class PiTuiRuntimeError extends Error {
4
4
  readonly stage: PiTuiRuntimeErrorStage;
@@ -26,6 +26,7 @@ export declare class PiTuiRuntimeAdapter {
26
26
  * the transparent and pinned paths never call it.
27
27
  */
28
28
  writeControl(data: string): void;
29
+ addPreInputListener(listener: PiTuiPreInputListener): () => void;
29
30
  addInputListener(listener: PiTuiInputListener): () => void;
30
31
  switchMode(mode: "regular" | "fullscreen"): boolean;
31
32
  setHardwareCursor(enabled: boolean): void;
@@ -81,6 +81,7 @@ class OverlayHandleBridge {
81
81
  }
82
82
  export class PiTuiRuntimeAdapter {
83
83
  #terminal;
84
+ #tuiTerminal;
84
85
  #tui;
85
86
  #root;
86
87
  #layoutRoot;
@@ -92,12 +93,14 @@ export class PiTuiRuntimeAdapter {
92
93
  #scrollViews = new Map();
93
94
  #overlayDisposers = new Set();
94
95
  #inputListeners = new Map();
96
+ #preInputListeners = new Set();
95
97
  #state = "idle";
96
98
  #stopPromise;
97
99
  #rootDisposed = false;
98
100
  constructor(options) {
99
101
  this.#root = options.root;
100
102
  this.#terminal = options.terminal ?? new ProcessTerminal();
103
+ this.#tuiTerminal = preInputTerminal(this.#terminal, data => this.#routePreInput(data));
101
104
  this.#layoutRoot = options.layoutRoot;
102
105
  this.#logDirectory = options.logDirectory;
103
106
  this.#rootBridge = new ComponentBridge(options.root);
@@ -239,6 +242,12 @@ export class PiTuiRuntimeAdapter {
239
242
  this.#assertRunning("control sequence");
240
243
  this.#terminal.write(data);
241
244
  }
245
+ addPreInputListener(listener) {
246
+ if (this.#preInputListeners.has(listener))
247
+ throw new TypeError("Pi TUI pre-input listener is already registered");
248
+ this.#preInputListeners.add(listener);
249
+ return () => this.#preInputListeners.delete(listener);
250
+ }
242
251
  addInputListener(listener) {
243
252
  this.#assertRunning("input listener");
244
253
  if (this.#inputListeners.has(listener))
@@ -313,6 +322,7 @@ export class PiTuiRuntimeAdapter {
313
322
  return this.#stopPromise;
314
323
  if (this.#state === "idle") {
315
324
  this.#state = "stopped";
325
+ this.#preInputListeners.clear();
316
326
  this.#disposeRoot();
317
327
  return Promise.resolve();
318
328
  }
@@ -352,8 +362,21 @@ export class PiTuiRuntimeAdapter {
352
362
  }
353
363
  #createTui(mode, hardwareCursor) {
354
364
  return mode === "fullscreen"
355
- ? new TuiAltScreen(this.#terminal, hardwareCursor, this.#logDirectory, this.#tuiOptions)
356
- : new TuiMainScreen(this.#terminal, hardwareCursor, this.#logDirectory);
365
+ ? new TuiAltScreen(this.#tuiTerminal, hardwareCursor, this.#logDirectory, this.#tuiOptions)
366
+ : new TuiMainScreen(this.#tuiTerminal, hardwareCursor, this.#logDirectory);
367
+ }
368
+ #routePreInput(data) {
369
+ let current = data;
370
+ for (const listener of this.#preInputListeners) {
371
+ if (current.length === 0)
372
+ break;
373
+ const result = listener(current);
374
+ if (result?.data !== undefined)
375
+ current = result.data;
376
+ else if (result?.consume === true)
377
+ current = "";
378
+ }
379
+ return current;
357
380
  }
358
381
  #mountTui(tui) {
359
382
  tui.addChild(this.#rootBridge);
@@ -458,11 +481,37 @@ export class PiTuiRuntimeAdapter {
458
481
  for (const remove of this.#inputListeners.values())
459
482
  remove();
460
483
  this.#inputListeners.clear();
484
+ this.#preInputListeners.clear();
461
485
  for (const dispose of [...this.#overlayDisposers])
462
486
  dispose();
463
487
  this.#disposeRoot();
464
488
  }
465
489
  }
490
+ function preInputTerminal(terminal, route) {
491
+ return {
492
+ get columns() { return terminal.columns; },
493
+ get rows() { return terminal.rows; },
494
+ get kittyProtocolActive() { return terminal.kittyProtocolActive; },
495
+ start(onInput, onResize) {
496
+ terminal.start(data => {
497
+ const routed = route(data);
498
+ if (routed.length > 0)
499
+ onInput(routed);
500
+ }, onResize);
501
+ },
502
+ stop: () => terminal.stop(),
503
+ drainInput: (maxMs, idleMs) => terminal.drainInput(maxMs, idleMs),
504
+ write: data => terminal.write(data),
505
+ moveBy: lines => terminal.moveBy(lines),
506
+ hideCursor: () => terminal.hideCursor(),
507
+ showCursor: () => terminal.showCursor(),
508
+ clearLine: () => terminal.clearLine(),
509
+ clearFromCursor: () => terminal.clearFromCursor(),
510
+ clearScreen: () => terminal.clearScreen(),
511
+ setTitle: title => terminal.setTitle(title),
512
+ setProgress: active => terminal.setProgress(active),
513
+ };
514
+ }
466
515
  function toOverlayOptions(options) {
467
516
  if (options === undefined)
468
517
  return undefined;
@@ -71,6 +71,8 @@ export interface PiTuiInputListenerResult {
71
71
  readonly data?: string;
72
72
  }
73
73
  export type PiTuiInputListener = (data: string) => PiTuiInputListenerResult | undefined;
74
+ /** Runs at the terminal bridge before Pi TUI receives physical input. */
75
+ export type PiTuiPreInputListener = PiTuiInputListener;
74
76
  export interface PiTuiLayoutEntry {
75
77
  readonly node: PiTuiLayoutNode;
76
78
  readonly basis?: number | "auto";
@@ -5,7 +5,7 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "unsupported",
8
- "builtAt": "2026-08-26T05:59:16.405Z",
8
+ "builtAt": "2026-08-27T09:31:38.156Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "7524bf568992517f50ed53196c861c5edeba0d7275633bb4735ab45bd5963a28",
@@ -5,7 +5,7 @@
5
5
  "platform": "linux",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-08-26T05:59:26.040Z",
8
+ "builtAt": "2026-08-27T09:31:34.074Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "29e22fe29de2828982bc67ef418c4adcaab1490281477b7bea592ec6fe621bcd",
@@ -5,10 +5,10 @@
5
5
  "platform": "win32",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-08-26T05:59:44.317Z",
8
+ "builtAt": "2026-08-27T09:32:08.840Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian.exe",
11
- "sha256": "f36f8c76d82ce8e3d163c7671868bd0ab8b7bc1e14a190714aaeb304b2116a5b",
11
+ "sha256": "41b1b470549052c30109bdbd3187693ce4de9236295d729acc3a1204a2687025",
12
12
  "size": 172544
13
13
  },
14
14
  "provenance": {
@@ -1,6 +1,6 @@
1
- declare const ENVIRONMENT_KEYS: readonly ["certificationTarball", "configDir", "dataDir", "databasePath", "developmentInstanceId", "developmentRoot", "endpoint", "fixture", "fixtureInput", "fixtureToken", "inputAcknowledgement", "internalPackaging", "launchArgumentsJson", "launchProfile", "nativePi", "paneId", "piParityIntentionalMutation", "piPortRoot", "piSourceLedgerPath", "piSourceScanRoot", "probeTrace", "processGuardianPath", "profileHome", "protocolVersion", "releaseDigest", "releaseId", "releaseRoot", "releaseRunnerLabel", "runtimeDir", "structuredFlowLimits", "terminalArgumentsJson", "terminalExecutable", "terminalSessionId"];
1
+ declare const ENVIRONMENT_KEYS: readonly ["certificationTarball", "configDir", "dataDir", "databasePath", "developmentInstanceId", "developmentRoot", "endpoint", "fixture", "fixtureInput", "fixtureToken", "inputAcknowledgement", "internalPackaging", "launchProfile", "nativePi", "paneId", "piParityIntentionalMutation", "piPortRoot", "piSourceLedgerPath", "piSourceScanRoot", "probeTrace", "processGuardianPath", "profileHome", "protocolVersion", "releaseDigest", "releaseId", "releaseRoot", "releaseRunnerLabel", "runtimeDir", "structuredFlowLimits", "terminalArgumentsJson", "terminalExecutable", "terminalSessionId"];
2
2
  declare const FILESYSTEM_KEYS: readonly ["slug", "windowsDirectory", "unixDirectory", "temporaryPrefix"];
3
- declare const STATE_KEYS: readonly ["windowsControlDirectory", "unixControlDirectory", "developmentDirectory", "piAgentProfile", "piVanillaProfile", "piSandboxProfile"];
3
+ declare const STATE_KEYS: readonly ["windowsControlDirectory", "unixControlDirectory", "developmentDirectory", "piAgentProfile", "piVanillaProfile"];
4
4
  declare const ENDPOINT_KEYS: readonly ["windowsPipeStem", "unixSocketFilename", "metadataFilename", "supervisorLogFilename", "databaseFilename"];
5
5
  declare const MANIFEST_KEYS: readonly ["releaseFilename", "packageFilename"];
6
6
  declare const PROTOCOL_KEYS: readonly ["namespace", "controlEnvelope", "supervisorSchema", "nativeHostSchema", "structuredAgentSchema", "controlStoreSchema", "releaseCohortSchema", "updateJournalSchema"];
@@ -12,7 +12,6 @@ const ENVIRONMENT_KEYS = [
12
12
  "fixtureToken",
13
13
  "inputAcknowledgement",
14
14
  "internalPackaging",
15
- "launchArgumentsJson",
16
15
  "launchProfile",
17
16
  "nativePi",
18
17
  "paneId",
@@ -35,7 +34,7 @@ const ENVIRONMENT_KEYS = [
35
34
  "terminalSessionId",
36
35
  ];
37
36
  const FILESYSTEM_KEYS = ["slug", "windowsDirectory", "unixDirectory", "temporaryPrefix"];
38
- const STATE_KEYS = ["windowsControlDirectory", "unixControlDirectory", "developmentDirectory", "piAgentProfile", "piVanillaProfile", "piSandboxProfile"];
37
+ const STATE_KEYS = ["windowsControlDirectory", "unixControlDirectory", "developmentDirectory", "piAgentProfile", "piVanillaProfile"];
39
38
  const ENDPOINT_KEYS = ["windowsPipeStem", "unixSocketFilename", "metadataFilename", "supervisorLogFilename", "databaseFilename"];
40
39
  const MANIFEST_KEYS = ["releaseFilename", "packageFilename"];
41
40
  const PROTOCOL_KEYS = ["namespace", "controlEnvelope", "supervisorSchema", "nativeHostSchema", "structuredAgentSchema", "controlStoreSchema", "releaseCohortSchema", "updateJournalSchema"];
@@ -64,8 +63,8 @@ export function validateProductIdentity(value) {
64
63
  throw new TypeError("product display name must be non-empty without surrounding whitespace");
65
64
  if (schema !== `${commandName}-product-identity-v1`)
66
65
  throw new TypeError("product identity schema must derive from the command name");
67
- if (filesystem.slug !== commandName || filesystem.unixDirectory !== filesystem.slug || filesystem.windowsDirectory !== displayName) {
68
- throw new TypeError("product filesystem identity must derive from display and command names");
66
+ if (filesystem.slug !== commandName || filesystem.unixDirectory !== filesystem.slug || filesystem.windowsDirectory !== filesystem.slug) {
67
+ throw new TypeError("product filesystem identity must derive from the command name");
69
68
  }
70
69
  if (filesystem.temporaryPrefix !== `${filesystem.slug}-`)
71
70
  throw new TypeError("product temporary prefix must derive from the filesystem slug");
@@ -5,7 +5,7 @@
5
5
  "packageName": "@timurproko/a1",
6
6
  "filesystem": {
7
7
  "slug": "a1",
8
- "windowsDirectory": "A1",
8
+ "windowsDirectory": "a1",
9
9
  "unixDirectory": "a1",
10
10
  "temporaryPrefix": "a1-"
11
11
  },
@@ -22,7 +22,6 @@
22
22
  "fixtureToken": "A1_FIXTURE_TOKEN",
23
23
  "inputAcknowledgement": "A1_INPUT_ACK",
24
24
  "internalPackaging": "A1_INTERNAL_PACKAGING",
25
- "launchArgumentsJson": "A1_LAUNCH_ARGUMENTS_JSON",
26
25
  "launchProfile": "A1_LAUNCH_PROFILE",
27
26
  "nativePi": "A1_NATIVE_PI",
28
27
  "paneId": "A1_PANE_ID",
@@ -45,12 +44,11 @@
45
44
  "terminalSessionId": "A1_TERMINAL_SESSION_ID"
46
45
  },
47
46
  "state": {
48
- "windowsControlDirectory": "A1",
47
+ "windowsControlDirectory": "a1",
49
48
  "unixControlDirectory": "a1",
50
49
  "developmentDirectory": "a1-development",
51
50
  "piAgentProfile": ".a1/agent",
52
- "piVanillaProfile": ".pi/agent",
53
- "piSandboxProfile": ".a1/sandbox"
51
+ "piVanillaProfile": ".pi/agent"
54
52
  },
55
53
  "endpoint": {
56
54
  "windowsPipeStem": "a1",
@@ -11,8 +11,11 @@ export * from "./scrollbar.js";
11
11
  export * from "./shortcuts.js";
12
12
  export * from "./spans.js";
13
13
  export * from "./status-line.js";
14
+ export * from "./submitted-prompt.js";
14
15
  export * from "./stepper.js";
16
+ export * from "./transcript-viewport.js";
15
17
  export * from "./surface.js";
16
18
  export * from "./text.js";
19
+ export * from "./text-selection.js";
17
20
  export * from "./theme.js";
18
21
  export * from "./value-menu.js";
@@ -11,8 +11,11 @@ export * from "./scrollbar.js";
11
11
  export * from "./shortcuts.js";
12
12
  export * from "./spans.js";
13
13
  export * from "./status-line.js";
14
+ export * from "./submitted-prompt.js";
14
15
  export * from "./stepper.js";
16
+ export * from "./transcript-viewport.js";
15
17
  export * from "./surface.js";
16
18
  export * from "./text.js";
19
+ export * from "./text-selection.js";
17
20
  export * from "./theme.js";
18
21
  export * from "./value-menu.js";
@@ -13,3 +13,13 @@ export interface ParsedMouseInput {
13
13
  }
14
14
  /** Extracts SGR mouse reports, leaving any keyboard input untouched. */
15
15
  export declare function parseMouseInput(data: string): ParsedMouseInput;
16
+ export interface RoutedMouseInput {
17
+ /** Input left for the TUI after only claimed reports were removed. */
18
+ readonly data: string;
19
+ readonly consumed: boolean;
20
+ }
21
+ /**
22
+ * Routes every SGR report independently, preserving keyboard bytes and mouse
23
+ * reports the caller does not claim even when they share one physical chunk.
24
+ */
25
+ export declare function routeMouseInput(data: string, claim: (event: PaneMouseEvent) => boolean): RoutedMouseInput;
@@ -30,6 +30,29 @@ export function parseMouseInput(data) {
30
30
  rest += data.slice(index);
31
31
  return { events: Object.freeze(events), rest };
32
32
  }
33
+ /**
34
+ * Routes every SGR report independently, preserving keyboard bytes and mouse
35
+ * reports the caller does not claim even when they share one physical chunk.
36
+ */
37
+ export function routeMouseInput(data, claim) {
38
+ if (!data.includes("\u001b[<"))
39
+ return { data, consumed: false };
40
+ let output = "";
41
+ let index = 0;
42
+ let consumed = false;
43
+ SGR_PATTERN.lastIndex = 0;
44
+ for (let match = SGR_PATTERN.exec(data); match !== null; match = SGR_PATTERN.exec(data)) {
45
+ output += data.slice(index, match.index);
46
+ index = match.index + match[0].length;
47
+ const event = toEvent(Number.parseInt(match[1] ?? "", 10), Number.parseInt(match[2] ?? "", 10), Number.parseInt(match[3] ?? "", 10), match[4] === "m");
48
+ if (event !== null && claim(event))
49
+ consumed = true;
50
+ else
51
+ output += match[0];
52
+ }
53
+ output += data.slice(index);
54
+ return { data: output, consumed };
55
+ }
33
56
  function toEvent(code, column, row, released) {
34
57
  if ((code & 64) !== 0) {
35
58
  const kind = (code & 1) === 0 ? "wheel-up" : "wheel-down";
@@ -0,0 +1,14 @@
1
+ export interface TimestampedPromptRowsInput {
2
+ readonly width: number;
3
+ readonly sourceTimestamp: number | Date;
4
+ readonly render: (contentWidth: number) => readonly string[];
5
+ /** Styles the reserved suffix while keeping policy independent of a vendor theme. */
6
+ readonly decorateSuffix?: (text: string, firstRow: boolean) => string;
7
+ readonly minimumWidth?: number;
8
+ }
9
+ export declare function formatPromptTimestamp(source: number | Date): string | null;
10
+ /**
11
+ * Reserves timestamp columns before wrapping, so the clock never overwrites
12
+ * prompt content. At narrow widths the same renderer is used without a clock.
13
+ */
14
+ export declare function composeTimestampedPromptRows(input: TimestampedPromptRowsInput): readonly string[];
@@ -0,0 +1,20 @@
1
+ import { overlaySpan } from "./spans.js";
2
+ export function formatPromptTimestamp(source) {
3
+ const date = source instanceof Date ? source : new Date(source);
4
+ if (Number.isNaN(date.getTime()))
5
+ return null;
6
+ return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
7
+ }
8
+ /**
9
+ * Reserves timestamp columns before wrapping, so the clock never overwrites
10
+ * prompt content. At narrow widths the same renderer is used without a clock.
11
+ */
12
+ export function composeTimestampedPromptRows(input) {
13
+ const timestamp = formatPromptTimestamp(input.sourceTimestamp);
14
+ if (timestamp === null || input.width < (input.minimumWidth ?? 20))
15
+ return input.render(input.width);
16
+ const suffixWidth = timestamp.length + 2;
17
+ const contentWidth = Math.max(1, input.width - suffixWidth);
18
+ const decorate = input.decorateSuffix ?? ((text) => text);
19
+ return input.render(contentWidth).map((row, index) => overlaySpan(row, contentWidth, input.width, decorate(index === 0 ? ` ${timestamp}` : " ".repeat(suffixWidth), index === 0)));
20
+ }