@timurproko/a1 0.2.7-dev.820 → 0.2.7-dev.834

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 (124) hide show
  1. package/README.md +7 -1
  2. package/bin/automatic-update.js +7 -0
  3. package/bin/ui.js +1 -0
  4. package/dist/app/session-shell/session-shell-root.d.ts +8 -1
  5. package/dist/app/session-shell/session-shell.js +68 -18
  6. package/dist/app/session-shell/shell-host.js +6 -3
  7. package/dist/app/session-shell/transcript-document.js +1 -1
  8. package/dist/composition/owned-ui.d.ts +3 -11
  9. package/dist/composition/owned-ui.js +93 -42
  10. package/dist/composition/resident-tab-bridge.d.ts +10 -1
  11. package/dist/composition/resident-tab-bridge.js +21 -0
  12. package/dist/composition/settings-route-host.d.ts +12 -2
  13. package/dist/composition/settings-route-host.js +27 -9
  14. package/dist/composition/settings-search-input.d.ts +9 -0
  15. package/dist/composition/settings-search-input.js +58 -0
  16. package/dist/contracts/owned-ui/session-backend.d.ts +2 -0
  17. package/dist/contracts/owned-ui/session-workflows.d.ts +7 -3
  18. package/dist/contracts/presentation/index.d.ts +11 -0
  19. package/dist/contracts/presentation/index.js +12 -0
  20. package/dist/features/launch/development-launch.js +6 -0
  21. package/dist/features/launch/profile-paths.js +4 -0
  22. package/dist/features/owned-ui/plus-menu-dialog.d.ts +22 -0
  23. package/dist/features/owned-ui/plus-menu-dialog.js +76 -0
  24. package/dist/features/owned-ui/reference-screen-app.d.ts +2 -0
  25. package/dist/features/owned-ui/reference-screen-app.js +12 -2
  26. package/dist/features/owned-ui/settings-app.d.ts +10 -3
  27. package/dist/features/owned-ui/settings-app.js +98 -45
  28. package/dist/features/owned-ui/settings-search-input.d.ts +29 -0
  29. package/dist/features/owned-ui/settings-search-input.js +1 -0
  30. package/dist/features/prompt-history/index.d.ts +1 -1
  31. package/dist/features/prompt-history/paths.d.ts +2 -1
  32. package/dist/features/prompt-history/paths.js +10 -2
  33. package/dist/features/prompt-history/service.d.ts +5 -0
  34. package/dist/features/prompt-history/service.js +1 -1
  35. package/dist/features/resident-tab/shortcut-declarations.d.ts +1 -1
  36. package/dist/features/resident-tab/shortcut-declarations.js +2 -0
  37. package/dist/foundation/launch-context/index.d.ts +2 -0
  38. package/dist/foundation/launch-context/index.js +1 -0
  39. package/dist/foundation/release/automatic-update.d.ts +37 -0
  40. package/dist/foundation/release/automatic-update.js +265 -0
  41. package/dist/foundation/release/bootstrap.d.ts +1 -9
  42. package/dist/foundation/release/bootstrap.js +7 -3
  43. package/dist/foundation/release/index.d.ts +1 -1
  44. package/dist/foundation/release/index.js +1 -1
  45. package/dist/foundation/release/latest-release.d.ts +3 -2
  46. package/dist/foundation/release/latest-release.js +1 -1
  47. package/dist/foundation/release/update-execution-lease.d.ts +36 -0
  48. package/dist/foundation/release/update-execution-lease.js +145 -0
  49. package/dist/foundation/release/update.d.ts +8 -6
  50. package/dist/foundation/release/update.js +232 -167
  51. package/dist/foundation/resident-tabs/bridge-client.d.ts +8 -2
  52. package/dist/foundation/resident-tabs/bridge-client.js +13 -6
  53. package/dist/foundation/resident-tabs/progress-observation-schema.d.ts +34 -0
  54. package/dist/foundation/resident-tabs/progress-observation-schema.js +1 -0
  55. package/dist/foundation/resident-tabs/progress-observation.d.ts +3 -6
  56. package/dist/foundation/resident-tabs/progress-observation.js +42 -12
  57. package/dist/foundation/resident-tabs/routes.d.ts +5 -0
  58. package/dist/foundation/resident-tabs/routes.js +5 -0
  59. package/dist/integrations/pi/components/editor-interaction.d.ts +2 -0
  60. package/dist/integrations/pi/components/modal-frame.d.ts +17 -1
  61. package/dist/integrations/pi/components/modal-frame.js +48 -9
  62. package/dist/integrations/pi/components/models-dialog.js +4 -52
  63. package/dist/integrations/pi/components/owned-editor-ux.d.ts +3 -0
  64. package/dist/integrations/pi/components/owned-editor-ux.js +75 -4
  65. package/dist/integrations/pi/components/shell-editor-autocomplete.js +57 -37
  66. package/dist/integrations/pi/components/shell-footer-status.js +19 -3
  67. package/dist/integrations/pi/components/shell-presenters-transcript.js +4 -1
  68. package/dist/integrations/pi/components/shell-selectors-dialogs.js +4 -1
  69. package/dist/integrations/pi/components/shell-shared-facade.d.ts +9 -2
  70. package/dist/integrations/pi/components/skills-dialog.js +2 -2
  71. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +7 -5
  72. package/dist/integrations/pi/components/upstream/components/owned-editor.js +15 -8
  73. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +3 -3
  74. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +2 -2
  75. package/dist/integrations/pi/components/upstream/components/session-footer.js +26 -23
  76. package/dist/integrations/pi/components/upstream/components/session-selector.js +7 -6
  77. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +7 -6
  78. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +9 -8
  79. package/dist/integrations/pi/components/upstream/components/tool-execution.js +6 -4
  80. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +8 -8
  81. package/dist/integrations/pi/components/upstream/components/tree-selector.js +10 -10
  82. package/dist/integrations/pi/components/upstream/history/editor-core.js +2 -0
  83. package/dist/integrations/pi/engine/adapter.js +7 -2
  84. package/dist/integrations/pi/engine/ports.d.ts +1 -1
  85. package/dist/integrations/pi/engine/ports.js +4 -0
  86. package/dist/integrations/pi/engine/workflow-runner.js +9 -2
  87. package/dist/native/darwin-arm64/manifest.json +1 -1
  88. package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
  89. package/dist/native/darwin-arm64/terminal-host.node +0 -0
  90. package/dist/native/darwin-x64/manifest.json +1 -1
  91. package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
  92. package/dist/native/darwin-x64/terminal-host.node +0 -0
  93. package/dist/native/linux-x64/manifest.json +1 -1
  94. package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
  95. package/dist/native/linux-x64/terminal-host.node +0 -0
  96. package/dist/native/win32-x64/manifest.json +2 -2
  97. package/dist/native/win32-x64/process-guardian.node +0 -0
  98. package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
  99. package/dist/native/win32-x64/terminal-host.node +0 -0
  100. package/dist/product-identity.d.ts +1 -1
  101. package/dist/product-identity.js +1 -1
  102. package/dist/product-identity.json +1 -0
  103. package/dist/ui/apps/host.d.ts +2 -0
  104. package/dist/ui/apps/host.js +7 -1
  105. package/dist/ui/components/dialog-panel.js +35 -30
  106. package/dist/ui/components/line-input.d.ts +3 -0
  107. package/dist/ui/components/line-input.js +3 -1
  108. package/dist/ui/components/prompt-input.d.ts +3 -2
  109. package/dist/ui/components/prompt-input.js +6 -4
  110. package/dist/ui/components/spans.js +2 -2
  111. package/dist/ui/components/surface.js +4 -2
  112. package/dist/ui/components/text.d.ts +3 -6
  113. package/dist/ui/components/text.js +6 -8
  114. package/dist/ui/settings/declarations.d.ts +54 -8
  115. package/dist/ui/settings/declarations.js +36 -7
  116. package/dist/ui/settings/manager.js +7 -3
  117. package/dist/ui/settings/migrations.js +15 -0
  118. package/dist/ui/settings/sections.d.ts +4 -0
  119. package/dist/ui/settings/sections.js +9 -3
  120. package/docs/architecture/resident-tabs.md +36 -8
  121. package/docs/architecture/tool-image-presentation.md +2 -1
  122. package/docs/features/prompt-history.md +41 -20
  123. package/docs/validation.md +25 -5
  124. package/package.json +1 -1
@@ -17,7 +17,10 @@ class ResidentProgressObserver {
17
17
  #engineSequence = 0;
18
18
  #pendingBytes = 0;
19
19
  #lost = 0;
20
- #writing = Promise.resolve();
20
+ #writing = false;
21
+ #pending = [];
22
+ #scheduled = false;
23
+ #directoryReady = false;
21
24
  #failed = false;
22
25
  get tabId() { return this.#tabId; }
23
26
  constructor(directory, runId, tabId, incarnation) {
@@ -36,6 +39,9 @@ class ResidentProgressObserver {
36
39
  count: 1,
37
40
  });
38
41
  }
42
+ inputPhase(phase, revision) {
43
+ this.#observe("child-input-phase", { eventKind: phase, revision, count: 1 });
44
+ }
39
45
  childRenderWrite(bytes, fragments, transaction, frameId, renderCause) {
40
46
  this.#observe("child-render-write", {
41
47
  eventSequence: this.#engineSequence,
@@ -59,7 +65,7 @@ class ResidentProgressObserver {
59
65
  pid: process.pid,
60
66
  processInstance: this.#processInstance,
61
67
  timestampUnixUs: Math.round((performance.timeOrigin + performance.now()) * 1_000),
62
- // Rationale: timeOrigin is calibrated to the Unix epoch; retain a conservative cross-process error.
68
+ // Rationale: epoch-calibrated timeOrigin retains conservative cross-process clock error.
63
69
  clockErrorUs: 1_000,
64
70
  sequence: ++this.#sequence,
65
71
  tabId: this.#tabId,
@@ -75,18 +81,43 @@ class ResidentProgressObserver {
75
81
  }
76
82
  this.#lost = 0;
77
83
  this.#pendingBytes += length;
78
- this.#writing = this.#writing.then(async () => {
79
- await mkdir(this.#directory, { recursive: true });
80
- await appendFile(this.#path, line, "utf8");
81
- }).catch(() => {
84
+ this.#pending.push(line);
85
+ this.#schedule();
86
+ }
87
+ #schedule() {
88
+ if (this.#scheduled || this.#writing || this.#failed)
89
+ return;
90
+ this.#scheduled = true;
91
+ // Performance: check-phase batches yield to input and amortize file opens and promises.
92
+ setImmediate(() => { this.#scheduled = false; void this.#flush(); });
93
+ }
94
+ async #flush() {
95
+ if (this.#failed || this.#pending.length === 0)
96
+ return;
97
+ this.#writing = true;
98
+ const batch = this.#pending.join("");
99
+ this.#pending = [];
100
+ try {
101
+ if (!this.#directoryReady) {
102
+ await mkdir(this.#directory, { recursive: true });
103
+ this.#directoryReady = true;
104
+ }
105
+ await appendFile(this.#path, batch, "utf8");
106
+ }
107
+ catch {
82
108
  this.#failed = true;
83
- }).finally(() => {
84
- this.#pendingBytes -= length;
85
- });
109
+ this.#pending = [];
110
+ }
111
+ finally {
112
+ this.#pendingBytes -= Buffer.byteLength(batch);
113
+ this.#writing = false;
114
+ if (this.#pending.length > 0)
115
+ this.#schedule();
116
+ }
86
117
  }
87
118
  }
88
119
  let observer;
89
- /** Returns the opt-in payload-free observer for a resident child, or null on ordinary launches. */
120
+ /** Opt-in payload-free child observer; ordinary launches return null. */
90
121
  export function residentProgressObserver(environment = process.env) {
91
122
  if (environment === process.env && observer !== undefined)
92
123
  return observer;
@@ -99,8 +130,7 @@ export function residentProgressObserver(environment = process.env) {
99
130
  : null;
100
131
  if (environment === process.env) {
101
132
  observer = value;
102
- // Security: test evidence paths and tab identity are not child/tool configuration. Retain them only in
103
- // this observer instance so extensions and spawned tools cannot append to the fixture trace.
133
+ // Security: keep evidence paths and identity private; erase them before extensions/tools inherit env.
104
134
  for (const name of [
105
135
  OBSERVATION_DIRECTORY_ENV,
106
136
  OBSERVATION_RUN_ENV,
@@ -0,0 +1,5 @@
1
+ /** The owned screens the server opens in a utility tab; each names its tab id stem, `<route>-…`. */
2
+ export type ResidentTabRoute = "settings" | "changelog";
3
+ export declare const RESIDENT_TAB_ROUTES: readonly ResidentTabRoute[];
4
+ /** The utility route whose screen is the resident tab `tabId`, or `null` for an Agent tab. */
5
+ export declare function residentUtilityRoute(tabId: string): ResidentTabRoute | null;
@@ -0,0 +1,5 @@
1
+ export const RESIDENT_TAB_ROUTES = ["settings", "changelog"];
2
+ /** The utility route whose screen is the resident tab `tabId`, or `null` for an Agent tab. */
3
+ export function residentUtilityRoute(tabId) {
4
+ return RESIDENT_TAB_ROUTES.find(route => tabId.startsWith(`${route}-`)) ?? null;
5
+ }
@@ -25,6 +25,8 @@ export interface EditorRecallPort {
25
25
  };
26
26
  observe(listener: () => void): () => void;
27
27
  reset(): void;
28
+ /** Exit a recall browse cycle by restoring the unsent draft rather than keeping the recalled entry. */
29
+ restoreDraft?(): void;
28
30
  }
29
31
  export interface EditorSurface extends Pick<Editor, Exclude<keyof Editor, "setText" | "render" | "handleInput">> {
30
32
  setText(text: string): void;
@@ -1,5 +1,21 @@
1
- import { Container, type Component, type TUI } from "@earendil-works/pi-tui";
1
+ import { Container, Input, type Component, type Focusable, type TUI, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
2
2
  export declare const PI_MODAL_CONTENT_PADDING_X = 1;
3
+ /** Paints the standard Pi Input prompt as a live-themed dialog-search marker. */
4
+ export declare function renderPiModalSearchInput(input: Input, width: number): string[];
5
+ /** Keeps Pi Input behavior and geometry while owning only its standard dialog-search presentation. */
6
+ export declare class PiModalSearchInput implements Component, Focusable {
7
+ #private;
8
+ constructor(input: Input);
9
+ get focused(): boolean;
10
+ set focused(value: boolean);
11
+ invalidate(): void;
12
+ render(width: number): string[];
13
+ handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined;
14
+ }
15
+ /** Replaces one explicitly identified direct Input child with the standard search presentation. */
16
+ export declare function adoptPiModalSearchInput(container: Container, input: Component): PiModalSearchInput;
17
+ /** Adds one standard dialog-search presentation while callers retain the Input controller. */
18
+ export declare function addPiModalSearchInput(container: Container, input: Input): PiModalSearchInput;
3
19
  /** Adds the standard navigation/select/close footer before a dependency component's bottom rule. */
4
20
  export declare function addPiModalSelectFooter(container: Container): void;
5
21
  /** Owned cancellable operation frame with the canonical dialog close guidance. */
@@ -1,7 +1,47 @@
1
- import { CancellableLoader, Container, Spacer, Text } from "@earendil-works/pi-tui";
1
+ import { CancellableLoader, Container, Input, Spacer, Text, } from "@earendil-works/pi-tui";
2
+ import { DIALOG_CONTENT_PADDING_X, dialogFrameGeometry, insetDialogContent } from "../../../contracts/presentation/index.js";
2
3
  import { DynamicBorder } from "../startup-public.js";
3
4
  import { DIALOG_CLOSE_SHORTCUT_HINT, paintPiBorder, piTheme, renderPiModalShortcutHints } from "./theme.js";
4
- export const PI_MODAL_CONTENT_PADDING_X = 1;
5
+ export const PI_MODAL_CONTENT_PADDING_X = DIALOG_CONTENT_PADDING_X;
6
+ /** Paints the standard Pi Input prompt as a live-themed dialog-search marker. */
7
+ export function renderPiModalSearchInput(input, width) {
8
+ return input.render(width).map(row => {
9
+ if (row.length === 0)
10
+ return row;
11
+ if (!row.startsWith(">"))
12
+ throw new TypeError("modal search input does not use the standard prompt");
13
+ return `${piTheme().fg("accent", ">")}${row.slice(1)}`;
14
+ });
15
+ }
16
+ /** Keeps Pi Input behavior and geometry while owning only its standard dialog-search presentation. */
17
+ export class PiModalSearchInput {
18
+ #input;
19
+ constructor(input) {
20
+ this.#input = input;
21
+ }
22
+ get focused() { return this.#input.focused; }
23
+ set focused(value) { this.#input.focused = value; }
24
+ invalidate() { this.#input.invalidate(); }
25
+ render(width) { return renderPiModalSearchInput(this.#input, width); }
26
+ handleMouse(event) { return this.#input.handleMouse(event); }
27
+ }
28
+ /** Replaces one explicitly identified direct Input child with the standard search presentation. */
29
+ export function adoptPiModalSearchInput(container, input) {
30
+ if (!(input instanceof Input))
31
+ throw new TypeError("modal search component is not an Input");
32
+ const index = container.children.indexOf(input);
33
+ if (index === -1)
34
+ throw new TypeError("modal search Input is not a direct child");
35
+ const presentation = new PiModalSearchInput(input);
36
+ container.children.splice(index, 1, presentation);
37
+ return presentation;
38
+ }
39
+ /** Adds one standard dialog-search presentation while callers retain the Input controller. */
40
+ export function addPiModalSearchInput(container, input) {
41
+ const presentation = new PiModalSearchInput(input);
42
+ container.addChild(presentation);
43
+ return presentation;
44
+ }
5
45
  /** Adds the standard navigation/select/close footer before a dependency component's bottom rule. */
6
46
  export function addPiModalSelectFooter(container) {
7
47
  container.children.splice(container.children.length - 1, 0, new Text(renderPiModalShortcutHints([
@@ -67,21 +107,20 @@ export class PiModalFrame {
67
107
  this.#bottomRule.invalidate();
68
108
  }
69
109
  render(width) {
70
- const padding = Math.min(PI_MODAL_CONTENT_PADDING_X, Math.max(0, width));
71
- const contentWidth = Math.max(0, width - padding);
72
- const rows = [...this.#topRule.render(width)];
110
+ const geometry = dialogFrameGeometry(width);
111
+ const rows = [...this.#topRule.render(geometry.frameWidth)];
73
112
  for (const component of this.#content) {
74
113
  if (this.#fullWidthContent.has(component) || this.#preInsetContent.has(component)) {
75
- rows.push(...component.render(width));
114
+ rows.push(...component.render(geometry.frameWidth));
76
115
  }
77
116
  else if (component instanceof Spacer) {
78
- rows.push(...component.render(contentWidth));
117
+ rows.push(...component.render(geometry.contentWidth));
79
118
  }
80
119
  else {
81
- rows.push(...component.render(contentWidth).map(row => `${" ".repeat(padding)}${row}`));
120
+ rows.push(...component.render(geometry.contentWidth).map(row => insetDialogContent(row, geometry)));
82
121
  }
83
122
  }
84
- rows.push(...this.#bottomRule.render(width));
123
+ rows.push(...this.#bottomRule.render(geometry.frameWidth));
85
124
  return rows;
86
125
  }
87
126
  }
@@ -1,6 +1,6 @@
1
1
  import { Input, getKeybindings, matchesKey, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
2
2
  import { DynamicBorder } from "../startup-public.js";
3
- import { PiModalFrame } from "./modal-frame.js";
3
+ import { PiModalFrame, renderPiModalSearchInput } from "./modal-frame.js";
4
4
  import { DIALOG_CLOSE_SHORTCUT_HINT, paintPiBorder, piTheme, renderPiModalListRow, renderPiModalShortcutHints, } from "./theme.js";
5
5
  function renderHintsWithClose(entries, width) {
6
6
  const body = renderPiModalShortcutHints(entries.slice(0, -1));
@@ -13,7 +13,6 @@ function renderHintsWithClose(entries, width) {
13
13
  }
14
14
  const MAX_VISIBLE_ROWS = 10;
15
15
  const MODELS_TITLE = "Models";
16
- const REFRESHING_TITLE_MIN_DURATION_MS = 1_000;
17
16
  class ModalTitleLine {
18
17
  #text = "";
19
18
  setText(text) { this.#text = text; }
@@ -60,9 +59,6 @@ export class ModelsDialogComponent {
60
59
  #selectedIndex = 0;
61
60
  #preferredId;
62
61
  #refreshStatus;
63
- #refreshing = false;
64
- #refreshStartedAt;
65
- #refreshOutcomeTimer;
66
62
  #disposed = false;
67
63
  #focused = false;
68
64
  constructor(config, callbacks) {
@@ -71,10 +67,6 @@ export class ModelsDialogComponent {
71
67
  this.#scopeIds = [...config.scopeIds];
72
68
  this.#savedScopeIds = [...config.savedScopeIds];
73
69
  this.#filter = config.initialFilter ?? "all";
74
- if (config.refreshStatus !== undefined) {
75
- this.#refreshing = true;
76
- this.#refreshStartedAt = Date.now();
77
- }
78
70
  if (config.initialQuery)
79
71
  this.#input.setValue(config.initialQuery);
80
72
  this.#replaceModels(config.models);
@@ -114,38 +106,10 @@ export class ModelsDialogComponent {
114
106
  setRefreshStatus(message, kind) {
115
107
  if (this.#disposed)
116
108
  return;
117
- if (kind === "muted") {
118
- this.#clearRefreshOutcome();
119
- this.#refreshStatus = undefined;
120
- this.#refreshing = true;
121
- this.#refreshStartedAt = Date.now();
122
- return;
123
- }
124
- const visibleMs = this.#refreshStartedAt === undefined ? REFRESHING_TITLE_MIN_DURATION_MS : Date.now() - this.#refreshStartedAt;
125
- const remainingMs = Math.max(0, REFRESHING_TITLE_MIN_DURATION_MS - visibleMs);
126
- if (this.#refreshing && remainingMs > 0) {
127
- this.#clearRefreshOutcome();
128
- const timer = setTimeout(() => {
129
- if (this.#refreshOutcomeTimer !== timer)
130
- return;
131
- this.#refreshOutcomeTimer = undefined;
132
- if (this.#disposed)
133
- return;
134
- this.#applyRefreshOutcome(message, kind);
135
- this.#callbacks.requestRender();
136
- }, remainingMs);
137
- timer.unref?.();
138
- this.#refreshOutcomeTimer = timer;
139
- return;
140
- }
141
- this.#clearRefreshOutcome();
142
- this.#applyRefreshOutcome(message, kind);
109
+ this.#refreshStatus = kind === "warning" ? { message, kind } : undefined;
143
110
  }
144
111
  dispose() {
145
112
  this.#disposed = true;
146
- this.#clearRefreshOutcome();
147
- this.#refreshing = false;
148
- this.#refreshStartedAt = undefined;
149
113
  }
150
114
  invalidate() {
151
115
  this.#input.invalidate();
@@ -235,21 +199,9 @@ export class ModelsDialogComponent {
235
199
  render(width) {
236
200
  const theme = piTheme();
237
201
  this.#title.setText(theme.fg("accent", theme.bold(MODELS_TITLE))
238
- + (this.dirty ? theme.fg("warning", " (unsaved)") : "")
239
- + (this.#refreshing ? theme.fg("muted", " (refreshing)") : ""));
202
+ + (this.dirty ? theme.fg("warning", " (unsaved)") : ""));
240
203
  return this.#frame.render(width);
241
204
  }
242
- #applyRefreshOutcome(message, kind) {
243
- this.#refreshing = false;
244
- this.#refreshStartedAt = undefined;
245
- this.#refreshStatus = kind === "warning" ? { message, kind } : undefined;
246
- }
247
- #clearRefreshOutcome() {
248
- if (this.#refreshOutcomeTimer === undefined)
249
- return;
250
- clearTimeout(this.#refreshOutcomeTimer);
251
- this.#refreshOutcomeTimer = undefined;
252
- }
253
205
  #renderBody(width) {
254
206
  const theme = piTheme();
255
207
  const lines = [];
@@ -261,7 +213,7 @@ export class ModelsDialogComponent {
261
213
  + theme.fg("dim", " | ")
262
214
  + theme.fg(this.#filter === "scoped" ? "mdHeading" : "dim", "scoped"));
263
215
  push();
264
- for (const line of this.#input.render(width))
216
+ for (const line of renderPiModalSearchInput(this.#input, width))
265
217
  push(line);
266
218
  push();
267
219
  if (rows.length === 0) {
@@ -51,6 +51,8 @@ export interface PromptSelectionUxOptions {
51
51
  readonly beginClipboardPaste?: () => PiShellPasteReservation;
52
52
  readonly beginTextPaste?: (text: string) => PiShellPasteReservation;
53
53
  readonly deferTextPaste?: (text: string) => boolean;
54
+ /** False consumes every paste transport without changing editor text. */
55
+ readonly pasteEnabled?: boolean;
54
56
  readonly onPasteRejected?: (error: unknown) => void;
55
57
  readonly onPasteInput?: (bytes: number, phase: "framing" | "inserting" | "settled") => void;
56
58
  readonly transformPastedContent: (content: PiShellClipboardContent) => string;
@@ -72,5 +74,6 @@ export interface PromptSelectionUxOptions {
72
74
  };
73
75
  }
74
76
  export declare function createPromptSelectionInterceptor(editor: Editor, keybindings: KeybindingsManager, options: PromptSelectionUxOptions): OwnedEditorUxInterceptor;
77
+ export declare function editorInsertedText(data: string): string | undefined;
75
78
  /** Shares the existing atomic-aware body layout with prompt/menu composition. */
76
79
  export declare function editorVisualLineCount(editor: Editor, width: number): number | undefined;
@@ -81,7 +81,7 @@ class PromptSelectionInterceptor {
81
81
  installAtomicSegmentation(editor, options.atomicRanges, () => this.#wordDirection);
82
82
  }
83
83
  handleInput(data, next) {
84
- if (this.#handleTerminalPaste(data, next))
84
+ if (this.options.pasteEnabled === false ? discardTerminalPaste(this, data, value => this.handleInput(value, next)) : this.#handleTerminalPaste(data, next))
85
85
  return;
86
86
  if (this.keybindings.matches(data, "owned.editor.selectAll")) {
87
87
  this.#selectAll();
@@ -107,7 +107,8 @@ class PromptSelectionInterceptor {
107
107
  return;
108
108
  }
109
109
  if (this.keybindings.matches(data, "owned.editor.paste")) {
110
- this.pasteClipboard();
110
+ if (this.options.pasteEnabled !== false)
111
+ this.pasteClipboard();
111
112
  return;
112
113
  }
113
114
  if (this.keybindings.matches(data, "owned.editor.redo")) {
@@ -132,7 +133,7 @@ class PromptSelectionInterceptor {
132
133
  this.#deleteSelection();
133
134
  return;
134
135
  }
135
- const inserted = insertedText(data);
136
+ const inserted = editorInsertedText(data);
136
137
  if (inserted !== undefined) {
137
138
  this.#replaceSelection(inserted);
138
139
  return;
@@ -280,11 +281,13 @@ class PromptSelectionInterceptor {
280
281
  this.#pointerSelecting = false;
281
282
  this.#lastClick = undefined;
282
283
  this.#redoStack = [];
284
+ DISABLED_TERMINAL_PASTES.delete(this);
283
285
  this.#selectionRevision += 1;
284
286
  }
285
287
  cancelPendingPastes() {
286
288
  this.#pasteGeneration += 1;
287
289
  this.#terminalPaste = undefined;
290
+ DISABLED_TERMINAL_PASTES.delete(this);
288
291
  }
289
292
  hasSelection() {
290
293
  return this.#activeRange() !== undefined;
@@ -295,6 +298,8 @@ class PromptSelectionInterceptor {
295
298
  : this.options.expandCopiedText(this.#selectedText());
296
299
  }
297
300
  pasteClipboard() {
301
+ if (this.options.pasteEnabled === false)
302
+ return false;
298
303
  this.#pasteFromClipboard();
299
304
  return true;
300
305
  }
@@ -933,7 +938,73 @@ function isEditorSegment(value) {
933
938
  const input = Reflect.get(value, "input");
934
939
  return typeof segment === "string" && typeof index === "number" && typeof input === "string";
935
940
  }
936
- function insertedText(data) {
941
+ const DISABLED_TERMINAL_PASTES = new WeakMap();
942
+ function discardTerminalPaste(owner, data, forward) {
943
+ const startMarker = "\x1b[200~";
944
+ const endMarker = "\x1b[201~";
945
+ const state = DISABLED_TERMINAL_PASTES.get(owner) ?? { startTail: "", bodyTail: null };
946
+ if (state.bodyTail !== null) {
947
+ const framed = state.bodyTail + data;
948
+ const end = framed.indexOf(endMarker);
949
+ if (end < 0) {
950
+ state.bodyTail = delimiterTail(framed, endMarker);
951
+ DISABLED_TERMINAL_PASTES.set(owner, state);
952
+ return true;
953
+ }
954
+ DISABLED_TERMINAL_PASTES.delete(owner);
955
+ const remaining = framed.slice(end + endMarker.length);
956
+ if (remaining.length > 0)
957
+ forward(remaining);
958
+ return true;
959
+ }
960
+ const hadStartTail = state.startTail.length > 0;
961
+ const candidate = state.startTail + data;
962
+ state.startTail = "";
963
+ const start = candidate.indexOf(startMarker);
964
+ if (start >= 0) {
965
+ if (start > 0)
966
+ forward(candidate.slice(0, start));
967
+ const framed = candidate.slice(start + startMarker.length);
968
+ const end = framed.indexOf(endMarker);
969
+ if (end < 0) {
970
+ state.bodyTail = delimiterTail(framed, endMarker);
971
+ DISABLED_TERMINAL_PASTES.set(owner, state);
972
+ }
973
+ else {
974
+ DISABLED_TERMINAL_PASTES.delete(owner);
975
+ const remaining = framed.slice(end + endMarker.length);
976
+ if (remaining.length > 0)
977
+ forward(remaining);
978
+ }
979
+ return true;
980
+ }
981
+ const partial = delimiterTail(candidate, startMarker);
982
+ // Compatibility: standalone Escape still cancels; marker fragments stay pending.
983
+ if (partial.length > 1) {
984
+ const before = candidate.slice(0, candidate.length - partial.length);
985
+ if (before.length > 0)
986
+ forward(before);
987
+ state.startTail = partial;
988
+ DISABLED_TERMINAL_PASTES.set(owner, state);
989
+ return true;
990
+ }
991
+ DISABLED_TERMINAL_PASTES.delete(owner);
992
+ if (hadStartTail) {
993
+ forward(candidate);
994
+ return true;
995
+ }
996
+ return false;
997
+ }
998
+ function delimiterTail(text, delimiter) {
999
+ const limit = Math.min(text.length, delimiter.length - 1);
1000
+ for (let length = limit; length > 0; length -= 1) {
1001
+ const suffix = text.slice(text.length - length);
1002
+ if (delimiter.startsWith(suffix))
1003
+ return suffix;
1004
+ }
1005
+ return "";
1006
+ }
1007
+ export function editorInsertedText(data) {
937
1008
  if (!data || data.includes("\u001b[200~"))
938
1009
  return undefined;
939
1010
  const kitty = decodeKittyPrintable(data);
@@ -5,6 +5,8 @@ import { OwnedEditorUxInterception, createPromptSelectionInterceptor, editorVisu
5
5
  import { PINNED_PI_LAYOUT, getPiSelectListTheme, piTheme, renderPiListSelection, } from "./theme.js";
6
6
  import { createPiKeybindingsHost, createTuiFacade, ensureTheme, isAutocompleteProvider, } from "./shell-shared-facade.js";
7
7
  import { SKILLS_COMMAND_NAME, collapseSkillCommands, createSkillsTunnelProvider, } from "./skills-command.js";
8
+ /** The built-in commands resident A1 serves from its tab bar instead: utility tabs, and the tab rename for `/name`. */
9
+ const RESIDENT_UTILITY_TAB_COMMANDS = new Set(["settings", "changelog", "name"]);
8
10
  /** A selected autocomplete row: its arrow/primary column, then the aligned description. */
9
11
  const SELECTED_DESCRIBED_ROW = /^(→ .*?\S)(\s{2,}.*)$/u;
10
12
  const INPUT_COUNTER_INSET = 3;
@@ -62,6 +64,8 @@ export function createPiShellEditor(options) {
62
64
  ensureTheme();
63
65
  const tui = createTuiFacade(options);
64
66
  const inputPresentation = options.keybindingProfile === "a1" ? options.promptPresentation?.input : undefined;
67
+ const inputCapabilities = options.inputCapabilities ?? {};
68
+ const autocompleteEnabled = inputCapabilities.autocomplete !== false;
65
69
  // Invariant: the process shares one manager; a standalone editor gets its own host for its profile.
66
70
  const host = options.keybindings ?? createPiKeybindingsHost({
67
71
  profile: options.keybindingProfile ?? "pi",
@@ -74,7 +78,7 @@ export function createPiShellEditor(options) {
74
78
  }
75
79
  const scrollInfo = { emitted: false, counter: undefined };
76
80
  const selectListTheme = getPiSelectListTheme();
77
- let tunnelSkills = [];
81
+ const autocomplete = { provider: emptyAutocompleteProvider(), skills: [] };
78
82
  const EditorClass = options.keybindingProfile === "a1" && options.persistentHistory === true ? options.historyEditor : OwnedEditor;
79
83
  const editor = new EditorClass(tui, {
80
84
  borderColor: (value) => inputPresentation === undefined ? piTheme().fg("borderMuted", value) : inputPresentation.styleRule(value),
@@ -100,13 +104,14 @@ export function createPiShellEditor(options) {
100
104
  ...(options.keybindingProfile === "a1" ? {
101
105
  terminalRows: options.getRows,
102
106
  getVisualLineCount: (width) => editorVisualLineCount(editor, width),
103
- clearCommandSearchOnEscape: true,
104
- commandTunnels: () => tunnelSkills.length === 0 ? [] : [SKILLS_COMMAND_NAME],
107
+ clearCommandSearchOnEscape: autocompleteEnabled,
108
+ commandTunnels: () => !autocompleteEnabled || autocomplete.skills.length === 0 ? [] : [SKILLS_COMMAND_NAME],
105
109
  } : {}),
106
110
  ...(options.keybindingProfile === "a1" && options.promptPresentation !== undefined ? {
107
111
  ...(inputPresentation === undefined ? {} : { inputPresentation }),
108
112
  styleSuggestion: options.promptPresentation.styleSuggestion,
109
113
  styleSuggestionCaret: options.promptPresentation.styleSuggestionCaret,
114
+ ...(inputCapabilities.placeholder === undefined ? {} : { placeholder: inputCapabilities.placeholder }),
110
115
  } : {}),
111
116
  });
112
117
  const editorUx = options.keybindingProfile === "a1"
@@ -117,6 +122,7 @@ export function createPiShellEditor(options) {
117
122
  ...(options.beginClipboardPaste === undefined ? {} : { beginClipboardPaste: options.beginClipboardPaste }),
118
123
  ...(options.beginTextPaste === undefined ? {} : { beginTextPaste: options.beginTextPaste }),
119
124
  ...(options.deferTextPaste === undefined ? {} : { deferTextPaste: options.deferTextPaste }),
125
+ pasteEnabled: inputCapabilities.paste !== false,
120
126
  ...(options.onPasteRejected === undefined ? {} : { onPasteRejected: options.onPasteRejected }),
121
127
  ...(options.onPasteInput === undefined ? {} : { onPasteInput: options.onPasteInput }),
122
128
  transformPastedContent: options.transformPastedContent ?? (content => content.kind === "text" ? content.text : ""),
@@ -142,37 +148,7 @@ export function createPiShellEditor(options) {
142
148
  : isBashMode ? piTheme().getBashModeBorderColor() : piTheme().getThinkingBorderColor(thinkingLevel);
143
149
  tui.requestRender();
144
150
  };
145
- let autocompleteProvider;
146
- const builtInCatalog = (options.keybindingProfile === "a1" ? OWNED_BUILTIN_SLASH_COMMANDS : PINNED_PI_BUILTIN_SLASH_COMMANDS)
147
- .filter(command => !options.hideSettingsCommand || command.name !== "settings");
148
- const setAutocompleteCommands = (installed) => {
149
- // Invariant: collapse is a bare-A1 presentation; the comparison profile installs the pinned list.
150
- const collapsed = options.keybindingProfile === "a1" && options.skillsPresentation?.() === "collapse"
151
- ? collapseSkillCommands(installed)
152
- : { commands: installed, skills: [] };
153
- tunnelSkills = collapsed.skills;
154
- const commands = collapsed.commands;
155
- const additions = new Map(commands.map(command => [command.name, command]));
156
- const builtInNames = new Set(builtInCatalog.map(command => command.name));
157
- const builtIns = builtInCatalog.flatMap(command => [
158
- autocompleteCommand(command, additions.get(command.name)),
159
- // Rationale: the collapsed browser is a primary bare-A1 command, not a resource after quit.
160
- ...(command.name === "settings" && collapsed.collapsedCommand !== undefined
161
- ? [autocompleteCommand(collapsed.collapsedCommand)]
162
- : []),
163
- ]);
164
- // Invariant: built-in additions and the promoted collapsed browser never reappear as resources.
165
- const resources = commands
166
- .filter(command => command.name !== collapsed.collapsedCommand?.name && command.source !== "builtin" && !builtInNames.has(command.name))
167
- .map(command => autocompleteCommand(command));
168
- const catalog = [...builtIns, ...resources];
169
- const combined = new CombinedAutocompleteProvider(catalog, options.cwd ?? process.cwd());
170
- const tunnelAware = tunnelSkills.length === 0 ? combined : createSkillsTunnelProvider(combined, tunnelSkills);
171
- autocompleteProvider = options.keybindingProfile === "a1"
172
- ? createDelimiterReadyCommandProvider(tunnelAware, catalog)
173
- : tunnelAware;
174
- editor.setAutocompleteProvider(autocompleteProvider);
175
- };
151
+ const setAutocompleteCommands = autocompleteCommandSetter(options, editor, autocomplete, autocompleteEnabled);
176
152
  setAutocompleteCommands(options.autocompleteCommands ?? []);
177
153
  let submitHandler = options.onSubmit;
178
154
  let interruptHandler = options.onInterrupt ?? (() => { });
@@ -259,6 +235,9 @@ export function createPiShellEditor(options) {
259
235
  reloadKeybindings: () => host.reload(),
260
236
  matchesTerminalKey: (data, key) => matchesKey(data, key),
261
237
  handleInput: data => {
238
+ if (inputCapabilities.singleLine === true
239
+ && (keybindings.matches(data, "tui.input.newLine") || keybindings.matches(data, "tui.input.submit")))
240
+ return;
262
241
  // Compatibility: an unassigned reverse Tab must not clear selection or reach autocomplete fallback.
263
242
  if (editorUx !== undefined && matchesKey(data, "shift+tab")
264
243
  && !Object.values(keybindings.getEffectiveConfig()).some(keys => (Array.isArray(keys) ? keys : [keys]).some(key => key !== undefined && matchesKey(data, key))))
@@ -316,13 +295,15 @@ export function createPiShellEditor(options) {
316
295
  tui.requestRender();
317
296
  },
318
297
  addAutocompleteProvider(factory) {
298
+ if (!autocompleteEnabled)
299
+ return;
319
300
  if (typeof factory !== "function")
320
301
  throw new TypeError("extension autocomplete factory must be a function");
321
- const candidate = Reflect.apply(factory, undefined, [autocompleteProvider]);
302
+ const candidate = Reflect.apply(factory, undefined, [autocomplete.provider]);
322
303
  if (!isAutocompleteProvider(candidate))
323
304
  throw new TypeError("extension autocomplete factory returned a malformed provider");
324
- autocompleteProvider = candidate;
325
- editor.setAutocompleteProvider(autocompleteProvider);
305
+ autocomplete.provider = candidate;
306
+ editor.setAutocompleteProvider(autocomplete.provider);
326
307
  },
327
308
  setThinkingLevel(level) {
328
309
  if (thinkingLevel === level)
@@ -354,6 +335,45 @@ export function createPiShellEditor(options) {
354
335
  cancelPendingPastes: () => editorUx?.cancelPendingPastes(),
355
336
  };
356
337
  }
338
+ function autocompleteCommandSetter(options, editor, state, enabled) {
339
+ const builtInCatalog = (options.keybindingProfile === "a1" ? OWNED_BUILTIN_SLASH_COMMANDS : PINNED_PI_BUILTIN_SLASH_COMMANDS)
340
+ .filter(command => !options.hideUtilityTabCommands || !RESIDENT_UTILITY_TAB_COMMANDS.has(command.name));
341
+ return installed => {
342
+ if (!enabled) {
343
+ state.provider = emptyAutocompleteProvider();
344
+ editor.setAutocompleteProvider(state.provider);
345
+ return;
346
+ }
347
+ // Invariant: collapse is a bare-A1 presentation; the comparison profile installs the pinned list.
348
+ const collapsed = options.keybindingProfile === "a1" && options.skillsPresentation?.() === "collapse"
349
+ ? collapseSkillCommands(installed)
350
+ : { commands: installed, skills: [] };
351
+ state.skills = collapsed.skills;
352
+ const additions = new Map(collapsed.commands.map(command => [command.name, command]));
353
+ const builtInNames = new Set(builtInCatalog.map(command => command.name));
354
+ const builtIns = builtInCatalog.flatMap(command => [
355
+ autocompleteCommand(command, additions.get(command.name)),
356
+ // Rationale: the collapsed browser is a primary bare-A1 command, not a resource after quit.
357
+ ...(command.name === "settings" && collapsed.collapsedCommand !== undefined
358
+ ? [autocompleteCommand(collapsed.collapsedCommand)] : []),
359
+ ]);
360
+ const resources = collapsed.commands
361
+ .filter(command => command.name !== collapsed.collapsedCommand?.name && command.source !== "builtin" && !builtInNames.has(command.name))
362
+ .map(command => autocompleteCommand(command));
363
+ const catalog = [...builtIns, ...resources];
364
+ const combined = new CombinedAutocompleteProvider(catalog, options.cwd ?? process.cwd());
365
+ const tunnelAware = state.skills.length === 0 ? combined : createSkillsTunnelProvider(combined, state.skills);
366
+ state.provider = options.keybindingProfile === "a1" ? createDelimiterReadyCommandProvider(tunnelAware, catalog) : tunnelAware;
367
+ editor.setAutocompleteProvider(state.provider);
368
+ };
369
+ }
370
+ function emptyAutocompleteProvider() {
371
+ return {
372
+ getSuggestions: async () => null,
373
+ applyCompletion: (lines, cursorLine, cursorCol) => ({ lines, cursorLine, cursorCol }),
374
+ shouldTriggerFileCompletion: () => false,
375
+ };
376
+ }
357
377
  /** Bare A1 leaves an applied catalog command ready for `:`, a user-entered space, or submission. */
358
378
  function createDelimiterReadyCommandProvider(base, commands) {
359
379
  const commandNames = new Set(commands.map(command => command.name));