@timurproko/a1 0.2.7-dev.1221 → 0.2.7-dev.1222

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 (48) hide show
  1. package/dist/app/session-shell/dock-chrome.js +1 -1
  2. package/dist/app/session-shell/session-shell-root.js +3 -2
  3. package/dist/app/session-shell/session-shell.js +4 -1
  4. package/dist/app/session-shell/session-viewport-controller.d.ts +2 -1
  5. package/dist/app/session-shell/session-viewport-controller.js +26 -37
  6. package/dist/app/session-shell/shell-editor-factory.js +4 -1
  7. package/dist/composition/owned-ui.js +4 -2
  8. package/dist/composition/reference-selection-options.d.ts +3 -0
  9. package/dist/composition/reference-selection-options.js +16 -0
  10. package/dist/composition/settings-route-host.d.ts +1 -2
  11. package/dist/composition/settings-route-host.js +19 -13
  12. package/dist/composition/settings-search-input.js +4 -1
  13. package/dist/composition/utility-tab.js +4 -2
  14. package/dist/features/owned-ui/plus-menu-dialog.js +28 -20
  15. package/dist/features/owned-ui/reference-document-selection.d.ts +30 -0
  16. package/dist/features/owned-ui/reference-document-selection.js +179 -0
  17. package/dist/features/owned-ui/reference-screen-app.d.ts +4 -1
  18. package/dist/features/owned-ui/reference-screen-app.js +39 -8
  19. package/dist/features/owned-ui/settings-app.d.ts +1 -0
  20. package/dist/features/owned-ui/settings-app.js +23 -19
  21. package/dist/integrations/pi/components/owned-editor-ux.js +48 -5
  22. package/dist/integrations/pi/engine/workflow-runner.js +1 -1
  23. package/dist/native/darwin-arm64/manifest.json +1 -1
  24. package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
  25. package/dist/native/darwin-arm64/terminal-host.node +0 -0
  26. package/dist/native/darwin-x64/manifest.json +1 -1
  27. package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
  28. package/dist/native/darwin-x64/terminal-host.node +0 -0
  29. package/dist/native/linux-x64/manifest.json +1 -1
  30. package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
  31. package/dist/native/linux-x64/terminal-host.node +0 -0
  32. package/dist/native/win32-x64/manifest.json +2 -2
  33. package/dist/native/win32-x64/process-guardian.node +0 -0
  34. package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
  35. package/dist/native/win32-x64/terminal-host.node +0 -0
  36. package/dist/ui/components/dialog-panel.d.ts +17 -8
  37. package/dist/ui/components/dialog-panel.js +97 -26
  38. package/dist/ui/components/index.d.ts +3 -3
  39. package/dist/ui/components/index.js +3 -3
  40. package/dist/ui/components/scrollbar.d.ts +1 -1
  41. package/dist/ui/components/scrollbar.js +1 -1
  42. package/dist/ui/components/selection-copy.d.ts +2 -4
  43. package/dist/ui/components/selection-copy.js +4 -2
  44. package/dist/ui/components/text-selection.d.ts +1 -2
  45. package/dist/ui/components/text-selection.js +5 -2
  46. package/dist/ui/components/transcript-viewport.js +3 -3
  47. package/docs/architecture/resident-tabs.md +5 -4
  48. package/package.json +3 -3
@@ -1,8 +1,8 @@
1
+ import { COPY_ACKNOWLEDGEMENT_DURATION_MS } from "../../ui/components/selection-copy.js";
1
2
  import { renderPiShellCommandMessage, renderPiShellReleaseUpdateBanner, renderPiShellStatusText, } from "../../integrations/pi/components/shell-presenters-info.js";
2
3
  import { PINNED_PI_LAYOUT, piTheme } from "../../integrations/pi/components/upstream/theme/theme.js";
3
4
  import { piShellTruncateToWidth } from "../../integrations/pi/components/shell-shared-facade.js";
4
5
  import { parseReleaseUpdateDiagnostic } from "./transcript-document.js";
5
- const COPY_ACKNOWLEDGEMENT_DURATION_MS = 1_000;
6
6
  /**
7
7
  * The transient chrome around bare A1's dock: the one command notice above the editor, the release
8
8
  * banner above Working with its close control, and the short copy acknowledgement.
@@ -1,7 +1,8 @@
1
1
  import { ImageAttachmentError } from "../../contracts/owned-ui/index.js";
2
2
  import { promptArrow } from "../../ui/components/prompt-input.js";
3
3
  import { composeSubmittedPromptRows, submittedPromptLayout, } from "../../ui/components/submitted-prompt.js";
4
- import { backgroundSgrSpan, nativeHyperlinkStyle, } from "../../ui/components/spans.js";
4
+ import { nativeHyperlinkStyle } from "../../ui/components/spans.js";
5
+ import { paintTextSelection } from "../../ui/components/text-selection.js";
5
6
  import { progressStatusFrame, progressStatusText } from "../../ui/components/progress-status.js";
6
7
  import { createPiQueuedInputStatus, createPiShellFooter, createPiShellHeader, createPiShellLoadedResources, createPiShellStatus, } from "../../integrations/pi/components/shell-footer-status.js";
7
8
  import { createPiShellTranscriptComponent, } from "../../integrations/pi/components/shell-presenters-transcript.js";
@@ -847,5 +848,5 @@ const VIEWPORT_THEME = {
847
848
  sticky: (text, hovered) => piTheme().bg(hovered ? "selectedBg" : "toolPendingBg", piTheme().fg("text", withoutTerminalBackground(text))),
848
849
  quietSticky: text => `\u001b[2m${text.replace(/\u001b\[(?:0|22)m/g, "$&\u001b[2m")}\u001b[22m`,
849
850
  bottomControl: (text, hovered) => piTheme().bg(hovered ? "selectedBg" : "toolPendingBg", piTheme().fg("text", text)),
850
- selection: (line, from, to) => backgroundSgrSpan(line, from, to, "\u001b[48;2;38;79;120m", "\u001b[49m"),
851
+ selection: paintTextSelection,
851
852
  };
@@ -14,6 +14,7 @@ import { promptSuggestionSurface, routeResponseToSuggestions, suggestionModelKey
14
14
  import { CLEAR_EXIT_INTERVAL_MS, boundedCleanup } from "../../foundation/terminal-cleanup/terminal-reset.js";
15
15
  import { OWNED_UI_HIDDEN_COMMAND_NAMES } from "../../contracts/owned-ui/index.js";
16
16
  import { ImageAttachmentError } from "../../contracts/owned-ui/index.js";
17
+ import { copyAcknowledgementText } from "../../ui/components/selection-copy.js";
17
18
  import { ContextualPromptSuggestionController } from "./prompt-suggestion-controller.js";
18
19
  import { workflowCommandNames } from "../../integrations/pi/engine/workflows.js";
19
20
  import { createPiExtensionUiBridge } from "../../integrations/pi/components/shell-extension-ui.js";
@@ -499,6 +500,8 @@ export class OwnedUiSessionPresenter {
499
500
  const snapshot = routed.copySelection;
500
501
  this.#trackCopy(this.#responseCopy?.submit(snapshot, this.#pendingClipboardWrite), copyCharacters(snapshot));
501
502
  }
503
+ if (routed.copyLastMessage)
504
+ void this.runWorkflow({ command: "copy", argument: "" });
502
505
  if (!routed.consumed)
503
506
  return routed.data === data ? undefined : { data: routed.data };
504
507
  return routed.data.length === 0 ? { consume: true } : { data: routed.data };
@@ -509,7 +512,7 @@ export class OwnedUiSessionPresenter {
509
512
  if (count === undefined || this.#disposed || !this.#terminal.running || intent !== this.#copyIntentSequence
510
513
  || outcome !== "delivered" && outcome !== "submitted-unverified")
511
514
  return;
512
- this.root.showCopyAcknowledgement(`copied ${count} chars to clipboard`);
515
+ this.root.showCopyAcknowledgement(copyAcknowledgementText(count));
513
516
  });
514
517
  }
515
518
  #promptSuggestionPresentationBlockReason(identity) {
@@ -1,7 +1,7 @@
1
1
  import type { PiTuiPointerSurface } from "../../integrations/pi/tui-runtime/contracts.js";
2
2
  import type { OwnedUiViewportSettings } from "../../contracts/owned-ui/index.js";
3
3
  import { type TranscriptViewportFrame, type TranscriptViewportFrameInput } from "../../ui/components/transcript-viewport.js";
4
- import { type SelectionCopySnapshot } from "../../ui/components/selection-copy.js";
4
+ import type { SelectionCopySnapshot } from "../../ui/components/selection-copy.js";
5
5
  import type { PiShellEditorPort } from "../../integrations/pi/components/shell-shared-facade.js";
6
6
  import type { PasteEvent } from "./paste-protocol.js";
7
7
  export interface SessionViewportControllerOptions {
@@ -26,6 +26,7 @@ export interface SessionViewportInputResult {
26
26
  readonly data: string;
27
27
  readonly consumed: boolean;
28
28
  readonly copySelection?: SelectionCopySnapshot;
29
+ readonly copyLastMessage?: true;
29
30
  }
30
31
  /**
31
32
  * Owns the stateful interaction policy of the bare-A1 session viewport. The
@@ -3,7 +3,6 @@ import { TranscriptViewport } from "../../ui/components/transcript-viewport.js";
3
3
  import { readVisibleHyperlinks } from "../../ui/components/visible-hyperlinks.js";
4
4
  import { routeMouseInput } from "../../ui/components/mouse.js";
5
5
  import { scrollForTrackPage, scrollForThumbRow, scrollbarSelectionRows, scrollbarWheelRows } from "../../ui/components/scrollbar.js";
6
- import { literalSelectionCopySnapshot } from "../../ui/components/selection-copy.js";
7
6
  function withinTailControl(region, column, row) {
8
7
  return row >= region.rowStart && row <= region.rowEnd && column >= region.columnStart && column <= region.columnEnd;
9
8
  }
@@ -254,15 +253,13 @@ export class SessionViewportController {
254
253
  pasteRequest = this.#nextPasteDiagnosticRequest();
255
254
  this.#emitPasteRoute(pasteRequest, "shortcut-received");
256
255
  }
257
- // Compatibility: Pi components share one keybinding manager. Restore bare A1's aliases
258
- // before the focused vanilla editor handles this input.
256
+ // Invariant: activate A1 aliases before editor input.
259
257
  if (editorActive)
260
258
  this.#editor.activateKeybindings();
261
259
  // Protocol: bracketed paste is opaque input, even if its text resembles mouse reports.
262
260
  if (data.startsWith("\u001b[200~"))
263
261
  return { data, consumed: false };
264
- // Platform: handle the physical paste chord at the pre-input boundary. Windows
265
- // terminals vary between forwarding Ctrl+V and terminal-owned bracketed paste.
262
+ // Platform: Windows terminals may forward Ctrl+V or terminal-owned paste.
266
263
  if (editorActive && this.#editor.matchesTerminalKey(data, "ctrl+v")) {
267
264
  pasteRequest ??= this.#nextPasteDiagnosticRequest();
268
265
  if (data !== "\u0016")
@@ -278,8 +275,7 @@ export class SessionViewportController {
278
275
  this.#requestHyperlinkCleanup();
279
276
  this.#requestRender();
280
277
  }
281
- // Invariant: content shortcuts never reach the prompt, even before the first frame
282
- // or when scrolling is a no-op. Plain Home/End remain editor line navigation.
278
+ // Invariant: viewport shortcuts skip the prompt; Home/End do not.
283
279
  if (editorActive && allowWheel && this.#editor.matchesTerminalKey(data, "ctrl+home")) {
284
280
  if (this.#viewport.scrollTo(0, now)) {
285
281
  this.#scheduleActivityExpiry();
@@ -295,6 +291,9 @@ export class SessionViewportController {
295
291
  }
296
292
  return { data: "", consumed: true };
297
293
  }
294
+ if (data === "\u0018" && editorActive && !this.#viewport.hasSelection && !this.#editor.hasSelection()) {
295
+ return { data: "", consumed: true, copyLastMessage: true };
296
+ }
298
297
  if (this.#viewport.frame === null) {
299
298
  // Invariant: a not-yet-painted A1 screen is not Pi's selection surface.
300
299
  return routeMouseInput(data, event => {
@@ -305,22 +304,15 @@ export class SessionViewportController {
305
304
  return event.kind === "motion" || event.kind === "release" || (event.kind === "press" && event.button === 0);
306
305
  });
307
306
  }
308
- if (data === "\u0003") {
309
- // Invariant: the newest complete-frame gesture owns explicit copy even if an
310
- // older editor word/line selection remains painted beneath it.
311
- if (this.#viewport.hasSelection) {
312
- const copySelection = this.#viewport.captureSelectedText();
313
- this.#viewport.clearSelection();
314
- this.#stopSelectionAutoScroll();
315
- this.#requestRender();
316
- return copySelection === null
317
- ? { data: "", consumed: true }
318
- : { data: "", consumed: true, copySelection };
319
- }
320
- if (editorActive && this.#editor.hasSelection()) {
321
- if (this.#viewport.clearSelection())
322
- this.#requestRender();
323
- }
307
+ if ((data === "\u0003" || data === "\u0018") && this.#viewport.hasSelection) {
308
+ // Invariant: the newest frame selection wins over an older editor selection.
309
+ const copySelection = this.#viewport.captureSelectedText();
310
+ this.#viewport.clearSelection();
311
+ this.#stopSelectionAutoScroll();
312
+ this.#requestRender();
313
+ return copySelection === null
314
+ ? { data: "", consumed: true }
315
+ : { data: "", consumed: true, copySelection };
324
316
  }
325
317
  const previousPrompt = PROMPT_UP_INPUTS.has(data) || this.#editor.matchesTerminalKey(data, "alt+home");
326
318
  if (editorActive && allowWheel && (previousPrompt || PROMPT_DOWN_INPUTS.has(data))) {
@@ -345,8 +337,7 @@ export class SessionViewportController {
345
337
  const hits = frame.hits;
346
338
  const overModal = this.#modalAt(event.column, event.row);
347
339
  const wheel = event.kind === "wheel-up" || event.kind === "wheel-down";
348
- // Invariant: ownership is latched for a complete gesture. Modal reports bypass the outer
349
- // fullscreen selection layer without ever invoking the hidden ordinary editor.
340
+ // Invariant: latch each gesture to one surface.
350
341
  if (this.#pointerOwner === "drain" || !this.inputGeometryReady) {
351
342
  if (event.kind === "release")
352
343
  this.#pointerOwner = undefined;
@@ -387,8 +378,7 @@ export class SessionViewportController {
387
378
  const wasOverBottom = hits.bottom !== null && previousPointer !== undefined
388
379
  && previousPointer.row === hits.bottom.row
389
380
  && previousPointer.column >= hits.bottom.columnStart && previousPointer.column <= hits.bottom.columnEnd;
390
- // Invariant: every report locates the pointer, independently of which surface owns the action.
391
- // Keep this location while the control is hidden; composition resolves its next hit region.
381
+ // Invariant: every report updates the pointer position.
392
382
  this.#pointerPosition = { column: event.column, row: event.row };
393
383
  repaint ||= wasOverBottom !== overBottom;
394
384
  const wasOverTailControl = tailControl !== undefined && previousPointer !== undefined
@@ -407,7 +397,7 @@ export class SessionViewportController {
407
397
  this.#viewport.setRailHovered(overRail);
408
398
  this.#viewport.setStickyHovered(overSticky);
409
399
  repaint = true;
410
- if (editorActive && this.#editor.ownsPointer() && this.#editorPointerFrame !== undefined) {
400
+ if (editorActive && this.#editorPointerSelecting && this.#editorPointerFrame !== undefined) {
411
401
  this.#editor.handlePointer({
412
402
  kind: "motion",
413
403
  button: event.button,
@@ -423,6 +413,12 @@ export class SessionViewportController {
423
413
  if (event.column === pendingEditorClick.column && event.row === pendingEditorClick.row)
424
414
  return true;
425
415
  this.#pendingEditorClick = undefined;
416
+ const editorFrame = editorActive ? this.#editorPointerFrame : undefined;
417
+ if (editorFrame !== undefined && this.#editor.handlePointer({ kind: "press", button: 0, column: pendingEditorClick.column, row: pendingEditorClick.row - editorFrame.rowStart + 1 })) {
418
+ this.#editorPointerSelecting = true;
419
+ this.#editor.handlePointer({ kind: "motion", button: event.button, column: event.column, row: event.row - editorFrame.rowStart + 1 });
420
+ return true;
421
+ }
426
422
  this.#viewport.pressSelection(pendingEditorClick.column, pendingEditorClick.row, now, { continueMultiClick: false });
427
423
  this.#viewport.extendSelection(event.column, event.row, now, false);
428
424
  this.#updateSelectionAutoScroll(event.column, event.row, hits.viewportHeight, frame.rows.length);
@@ -513,8 +509,7 @@ export class SessionViewportController {
513
509
  }
514
510
  if (event.row >= 1 && event.row <= frame.rows.length && event.column <= frame.descriptor.width) {
515
511
  if (editorFrame !== undefined && event.row >= editorFrame.rowStart && event.row <= editorFrame.rowEnd) {
516
- // Invariant: no-drag prompt clicks remain visually editor-owned. Distinct
517
- // motion promotes from this exact origin into complete-frame selection.
512
+ // Invariant: no-drag clicks remain editor-owned; first motion latches an editor drag.
518
513
  this.#pendingEditorClick = { column: event.column, row: event.row };
519
514
  }
520
515
  else {
@@ -527,7 +522,7 @@ export class SessionViewportController {
527
522
  return true;
528
523
  }
529
524
  if (event.kind === "release") {
530
- if (editorActive && this.#editor.ownsPointer() && this.#editorPointerFrame !== undefined) {
525
+ if (editorActive && this.#editorPointerSelecting && this.#editorPointerFrame !== undefined) {
531
526
  const wasEditorSelecting = this.#editorPointerSelecting;
532
527
  this.#editor.handlePointer({
533
528
  kind: "release",
@@ -547,12 +542,6 @@ export class SessionViewportController {
547
542
  const editorRow = pendingEditorClick.row - this.#editorPointerFrame.rowStart + 1;
548
543
  this.#editor.handlePointer({ kind: "press", button: 0, column: pendingEditorClick.column, row: editorRow });
549
544
  this.#editor.handlePointer({ kind: "release", button: 0, column: pendingEditorClick.column, row: editorRow });
550
- if (this.#copyOnSelect) {
551
- const selected = this.#editor.selectedText?.();
552
- if (selected !== undefined) {
553
- completedCopy = literalSelectionCopySnapshot(selected, this.#viewport.selectionRevision) ?? undefined;
554
- }
555
- }
556
545
  }
557
546
  forceRepaint = true;
558
547
  repaint = true;
@@ -2,6 +2,7 @@ import { caretCell } from "../../ui/components/line-input.js";
2
2
  import { PromptInput } from "../../ui/components/prompt-input.js";
3
3
  import { backgroundSgrSpan, hyperlinkSgrSpan } from "../../ui/components/spans.js";
4
4
  import { faint, stripAnsi } from "../../ui/components/text.js";
5
+ import { paintTextSelection } from "../../ui/components/text-selection.js";
5
6
  import { createPiShellEditor } from "../../integrations/pi/components/shell-editor-autocomplete.js";
6
7
  import { piTheme } from "../../integrations/pi/components/upstream/theme/theme.js";
7
8
  import { piShellTruncateToWidth, piShellVisibleWidth, } from "../../integrations/pi/components/shell-shared-facade.js";
@@ -18,7 +19,9 @@ export function createShellEditor(ports) {
18
19
  ...handlers,
19
20
  keybindingProfile: customViewport ? "a1" : "pi",
20
21
  ...(handlers.keybindings === undefined ? {} : { keybindings: handlers.keybindings }),
21
- paintEditorSelection: (line, from, to, atomic) => backgroundSgrSpan(line, from, to, atomic ? "\u001b[7m" : "\u001b[27m\u001b[48;2;38;79;120m", atomic ? "\u001b[27m" : "\u001b[49m", piShellVisibleWidth),
22
+ paintEditorSelection: (line, from, to, atomic) => atomic
23
+ ? backgroundSgrSpan(line, from, to, "\u001b[7m", "\u001b[27m", piShellVisibleWidth)
24
+ : paintTextSelection(line, from, to, piShellVisibleWidth, true),
22
25
  transformPastedContent: content => {
23
26
  try {
24
27
  return promptChips.transformPastedContent(content, editor.getText());
@@ -383,8 +383,10 @@ function settingsRouteOptions(tabBridge, keybindings, agentDir, cwd) {
383
383
  agentDir,
384
384
  cwd,
385
385
  copyText: text => {
386
- if (Buffer.byteLength(text) <= MAX_CLIPBOARD_TEXT_BYTES)
387
- void writeSystemClipboardText(text).catch(() => { });
386
+ if (Buffer.byteLength(text) > MAX_CLIPBOARD_TEXT_BYTES)
387
+ return false;
388
+ void writeSystemClipboardText(text).catch(() => { });
389
+ return true;
388
390
  },
389
391
  };
390
392
  }
@@ -0,0 +1,3 @@
1
+ import type { ReferenceDocumentSelectionOptions } from "../features/owned-ui/reference-document-selection.js";
2
+ /** Builds the Changelog selection clipboard policy without loading Pi's engine in a utility tab. */
3
+ export declare function createReferenceSelectionOptions(settingsFile: string, copyText: ((text: string) => boolean | void) | undefined): ReferenceDocumentSelectionOptions;
@@ -0,0 +1,16 @@
1
+ import { readFileSync } from "node:fs";
2
+ /** Builds the Changelog selection clipboard policy without loading Pi's engine in a utility tab. */
3
+ export function createReferenceSelectionOptions(settingsFile, copyText) {
4
+ return {
5
+ copyOnSelect: () => {
6
+ try {
7
+ const document = JSON.parse(readFileSync(settingsFile, "utf8"));
8
+ return typeof document.fullscreenCopyOnSelect === "boolean" ? document.fullscreenCopyOnSelect : true;
9
+ }
10
+ catch {
11
+ return true;
12
+ }
13
+ },
14
+ copyText: copyText ?? (() => false),
15
+ };
16
+ }
@@ -2,7 +2,6 @@ import type { ReferenceDocumentProvider } from "../features/owned-ui/reference-s
2
2
  import type { OwnedSettingsManager } from "../ui/settings/manager.js";
3
3
  import type { UiRouteHost, UiRouteInput } from "../ui/apps/contracts.js";
4
4
  import type { PiKeybindingsHost } from "../integrations/pi/components/shell-shared-facade.js";
5
- /** Styled document rows for one content width. */
6
5
  export type OwnedReferenceRows = NonNullable<ReferenceDocumentProvider["rows"]>;
7
6
  /** Documents supplied by composition so the route host never reads shell data itself. */
8
7
  export interface OwnedReferenceProviders {
@@ -22,7 +21,7 @@ export interface OwnedRouteHostOptions {
22
21
  readonly keybindings?: PiKeybindingsHost;
23
22
  readonly agentDir?: string;
24
23
  readonly cwd?: string;
25
- readonly copyText?: (text: string) => void;
24
+ readonly copyText?: (text: string) => boolean | void;
26
25
  }
27
26
  /** Declares settings and lazy document routes. */
28
27
  export declare function createOwnedRouteHost(settings: OwnedSettingsManager, references?: OwnedReferenceProviders, options?: OwnedRouteHostOptions): UiRouteHost;
@@ -18,7 +18,7 @@ export function createOwnedRouteHost(settings, references, options = {}) {
18
18
  const title = session ? "Session Info" : changelog ? input ? "What's New" : "Changelog" : HOTKEYS_TITLE;
19
19
  const document = session ? references.session() : changelog ? references.changelog(input) : references.hotkeys();
20
20
  const pinned = options.tabRoute === route;
21
- return deferredSurface(route, title, () => loadReferenceSurface(settings, route, title, document, pinned));
21
+ return deferredSurface(route, title, () => loadReferenceSurface(settings, route, title, document, pinned, options));
22
22
  },
23
23
  };
24
24
  }
@@ -63,9 +63,10 @@ function deferredSurface(id, label, load) {
63
63
  };
64
64
  }
65
65
  async function loadSettingsSurface(settings, options) {
66
- const [{ SettingsApp }, { createSettingsSearchInputFactory }] = await Promise.all([
66
+ const [{ SettingsApp }, { createSettingsSearchInputFactory }, { renderFloatingDialogSurface, renderFloatingMenuSurface }] = await Promise.all([
67
67
  import("../features/owned-ui/settings-app.js"),
68
68
  import("./settings-search-input.js"),
69
+ import("../ui/components/dialog-panel.js"),
69
70
  ]);
70
71
  const pinned = options.tabRoute === SETTINGS_ROUTE;
71
72
  const searchInputFactory = createSettingsSearchInputFactory({
@@ -78,14 +79,19 @@ async function loadSettingsSurface(settings, options) {
78
79
  pinned,
79
80
  searchInputFactory,
80
81
  configurePlusMenu: options.configurePlusMenu ?? null,
82
+ dialogPanel: text => renderFloatingDialogSurface(text, piTheme().getColorMode()),
81
83
  ...(options.copyText === undefined ? {} : { copyText: options.copyText }),
82
- }), !pinned, true);
84
+ }), !pinned, true, text => renderFloatingMenuSurface(text, piTheme().getColorMode()));
83
85
  }
84
- async function loadReferenceSurface(settings, route, title, document, pinned) {
86
+ async function loadReferenceSurface(settings, route, title, document, pinned, options) {
85
87
  const id = route;
86
- // Rationale: the app module and the document load together, so a document that fails
87
- // to render is reported by the placeholder rather than by a screen that never fills.
88
- const [{ ReferenceScreenApp }, content] = await Promise.all([import("../features/owned-ui/reference-screen-app.js"), document]);
88
+ const [{ ReferenceScreenApp }, content, selection] = await Promise.all([
89
+ import("../features/owned-ui/reference-screen-app.js"),
90
+ document,
91
+ route === CHANGELOG_ROUTE
92
+ ? import("./reference-selection-options.js").then(module => module.createReferenceSelectionOptions(settings.file, options.copyText))
93
+ : undefined,
94
+ ]);
89
95
  return hostApp(id, route, () => new ReferenceScreenApp({
90
96
  id,
91
97
  title,
@@ -96,9 +102,10 @@ async function loadReferenceSurface(settings, route, title, document, pinned) {
96
102
  scrollbarSpeed: settings.value("scrollbarSpeed"),
97
103
  }),
98
104
  pinned,
99
- }), !pinned);
105
+ ...(selection === undefined ? {} : { selection }),
106
+ }), !pinned, selection !== undefined);
100
107
  }
101
- async function hostApp(id, route, create, closeOnInterrupt = true, appInputBeforeInterrupt = false) {
108
+ async function hostApp(id, route, create, closeOnInterrupt = true, appInputBeforeInterrupt = false, panel = text => text) {
102
109
  const [{ UiAppHost }, { UiAppRegistry }] = await Promise.all([
103
110
  import("../ui/apps/host.js"),
104
111
  import("../ui/apps/registry.js"),
@@ -114,7 +121,7 @@ async function hostApp(id, route, create, closeOnInterrupt = true, appInputBefor
114
121
  registry,
115
122
  closeOnInterrupt,
116
123
  appInputBeforeInterrupt,
117
- theme: pinnedTheme(),
124
+ theme: pinnedTheme(panel),
118
125
  surface: {
119
126
  size: () => size,
120
127
  requestRender: () => onRender(),
@@ -143,8 +150,7 @@ async function hostApp(id, route, create, closeOnInterrupt = true, appInputBefor
143
150
  onExitRequested: listener => { onExit = listener; },
144
151
  };
145
152
  }
146
- /** Maps A1 UI tokens onto the pinned Pi theme so owned screens match the shell. */
147
- function pinnedTheme() {
153
+ function pinnedTheme(panel) {
148
154
  return {
149
155
  fg: (token, text) => piTheme().fg(token, text),
150
156
  bold: (text) => piTheme().bold(text),
@@ -152,6 +158,6 @@ function pinnedTheme() {
152
158
  accentPreview: (color, text) => renderPiAccentPreview(color, text),
153
159
  disabled: (text) => faint(piTheme().fg("dim", text)),
154
160
  highlight: (text) => piTheme().bg("selectedBg", text),
155
- panel: (text) => `\u001b[48;2;55;55;55m${text}\u001b[49m`,
161
+ panel,
156
162
  };
157
163
  }
@@ -3,6 +3,7 @@ import { createPiShellEditor } from "../integrations/pi/components/shell-editor-
3
3
  import { piShellTruncateToWidth, piShellVisibleWidth, } from "../integrations/pi/components/shell-shared-facade.js";
4
4
  import { backgroundSgrSpan } from "../ui/components/spans.js";
5
5
  import { caretCell } from "../ui/components/line-input.js";
6
+ import { paintTextSelection } from "../ui/components/text-selection.js";
6
7
  import { PromptInput } from "../ui/components/prompt-input.js";
7
8
  /** Builds Settings search from the same configurable owned editor used by the agent prompt. */
8
9
  export function createSettingsSearchInputFactory(options = {}) {
@@ -32,7 +33,9 @@ function createSettingsSearchInput(surface, options) {
32
33
  requestRender: surface.requestRender,
33
34
  onSubmit: () => { },
34
35
  onCopyText: text => surface.copyText(text),
35
- paintEditorSelection: (line, from, to, atomic) => backgroundSgrSpan(line, from, to, atomic ? "\u001b[7m" : "\u001b[27m\u001b[48;2;38;79;120m", atomic ? "\u001b[27m" : "\u001b[49m", piShellVisibleWidth),
36
+ paintEditorSelection: (line, from, to, atomic) => atomic
37
+ ? backgroundSgrSpan(line, from, to, "\u001b[7m", "\u001b[27m", piShellVisibleWidth)
38
+ : paintTextSelection(line, from, to, piShellVisibleWidth, true),
36
39
  promptPresentation: {
37
40
  input: new PromptInput({ fg: (token, text) => surface.theme.fg(token, text) }, {
38
41
  measure: piShellVisibleWidth,
@@ -73,8 +73,10 @@ function createUtilityRouteHost(options, settings, keybindings, cwd, agentDir) {
73
73
  agentDir,
74
74
  cwd,
75
75
  copyText: text => {
76
- if (Buffer.byteLength(text) <= MAX_CLIPBOARD_TEXT_BYTES)
77
- void writeSystemClipboardText(text).catch(() => { });
76
+ if (Buffer.byteLength(text) > MAX_CLIPBOARD_TEXT_BYTES)
77
+ return false;
78
+ void writeSystemClipboardText(text).catch(() => { });
79
+ return true;
78
80
  },
79
81
  }));
80
82
  }
@@ -1,9 +1,15 @@
1
- import { LineInput, displayWidth, handleLineInputKey, renderInputRow, truncateToWidth, } from "../../ui/components/index.js";
1
+ import { LineInput, handleLineInputKey, padToWidth, renderInputRow, renderShortcutHintsWithClose, truncateToWidth, } from "../../ui/components/index.js";
2
2
  /** The A1 setting holding the tab kinds the strip's `+` offers. */
3
3
  export const PLUS_MENU_SETTING_ID = "tabBarPlusMenu";
4
4
  const TITLE = "Plus Menu";
5
5
  const DESCRIPTION = "Choose what the + button in the tab bar can add.";
6
- const HINT = "Type search · ↑↓ navigate · Space toggle · Esc close";
6
+ const HINTS = [
7
+ { key: "Type", action: "search" },
8
+ { key: "↑↓", action: "navigate" },
9
+ { key: "Space", action: "toggle" },
10
+ { key: "esc", action: "close" },
11
+ ];
12
+ const PADDING_X = 2;
7
13
  /**
8
14
  * The plus-menu dialog: every tab kind with `●` when `+` offers it and `○` when not, filtered by typed
9
15
  * text. It owns only its filter and the row in hand; the enabled record and its saving stay with the
@@ -41,36 +47,38 @@ export class PlusMenuDialog {
41
47
  return "handled";
42
48
  }
43
49
  lines(enabled, width, theme) {
44
- const rule = theme.fg("border", "─".repeat(Math.max(0, width)));
45
- const inset = width > 0 ? " " : "";
46
- const contentWidth = Math.max(0, width - inset.length);
47
- const row = (painted, raw) => {
48
- const clipped = truncateToWidth(painted, contentWidth);
49
- return `${inset}${clipped}${" ".repeat(Math.max(0, contentWidth - displayWidth(raw)))}`;
50
+ const frameWidth = Math.max(0, width);
51
+ const left = Math.min(PADDING_X, frameWidth);
52
+ const right = Math.min(PADDING_X, frameWidth - left);
53
+ const contentWidth = Math.max(0, frameWidth - left - right);
54
+ const blank = " ".repeat(frameWidth);
55
+ const row = (painted, selected = false) => {
56
+ const content = padToWidth(truncateToWidth(painted, contentWidth), contentWidth);
57
+ const rightPadding = " ".repeat(right);
58
+ return `${" ".repeat(left)}${selected ? theme.highlight(content) : content}${selected ? theme.panel(rightPadding) : rightPadding}`;
50
59
  };
51
60
  const shown = this.shown();
52
61
  const index = Math.min(this.#index, Math.max(0, shown.length - 1));
53
62
  const rows = shown.length === 0
54
- ? [row(theme.fg("dim", "No tabs match"), "No tabs match")]
63
+ ? [row(theme.fg("dim", "No tabs match"))]
55
64
  : shown.map((kind, position) => {
56
65
  const selected = position === index;
57
66
  const cursor = selected ? "→ " : " ";
58
67
  const marker = enabled[kind.key] === true ? "●" : "○";
59
- const raw = `${cursor}${marker} ${kind.label}`;
60
68
  const content = `${selected ? theme.fg("accent", cursor) : cursor}${theme.fg("text", marker)} ${theme.fg("text", kind.label)}`;
61
- return row(selected ? theme.highlight(content) : content, raw);
69
+ return row(content, selected);
62
70
  });
63
71
  return [
64
- rule,
65
- row(theme.bold(theme.fg("accent", TITLE)), TITLE),
66
- row(theme.fg("dim", DESCRIPTION), DESCRIPTION),
67
- "",
68
- ...renderInputRow(this.#filter, width, { ruled: false, promptGlyph: "> ", theme }).lines,
69
- "",
72
+ blank,
73
+ row(theme.bold(theme.fg("accent", TITLE))),
74
+ row(theme.fg("dim", DESCRIPTION)),
75
+ blank,
76
+ ...renderInputRow(this.#filter, contentWidth, { ruled: false, promptGlyph: "> ", theme }).lines.map(line => row(line)),
77
+ blank,
70
78
  ...rows,
71
- "",
72
- row(theme.fg("dim", HINT), HINT),
73
- rule,
79
+ blank,
80
+ row(renderShortcutHintsWithClose(HINTS, theme, contentWidth)),
81
+ blank,
74
82
  ];
75
83
  }
76
84
  }
@@ -0,0 +1,30 @@
1
+ import { type PaneMouseEvent, type UiTheme } from "../../ui/components/index.js";
2
+ export interface ReferenceDocumentSelectionOptions {
3
+ readonly copyOnSelect: () => boolean;
4
+ readonly copyText: (text: string) => boolean | void;
5
+ }
6
+ interface ReferenceSelectionFrame {
7
+ readonly bodyRowStart: number;
8
+ readonly contentWidth: number;
9
+ readonly documentRows: readonly string[];
10
+ readonly visibleDocumentRows: readonly (number | undefined)[];
11
+ }
12
+ /** Selection state for an opted-in read-only reference document. */
13
+ export declare class ReferenceDocumentSelection {
14
+ #private;
15
+ constructor(options: ReferenceDocumentSelectionOptions);
16
+ get selecting(): boolean;
17
+ get hasSelection(): boolean;
18
+ updateFrame(frame: ReferenceSelectionFrame): void;
19
+ paintVisibleRow(visibleRow: number, line: string, theme: UiTheme): string;
20
+ handleInput(data: string): {
21
+ readonly consumed: boolean;
22
+ readonly cleared: boolean;
23
+ };
24
+ handleMouse(event: PaneMouseEvent, requestRender?: () => void): {
25
+ readonly consumed: boolean;
26
+ readonly changed: boolean;
27
+ };
28
+ clear(): void;
29
+ }
30
+ export {};