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

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 (65) hide show
  1. package/README.md +8 -6
  2. package/bin/ui.js +1 -0
  3. package/dist/app/session-shell/dock-chrome.js +1 -1
  4. package/dist/app/session-shell/exit-notice.d.ts +2 -2
  5. package/dist/app/session-shell/exit-notice.js +4 -4
  6. package/dist/app/session-shell/session-lifecycle.d.ts +2 -1
  7. package/dist/app/session-shell/session-lifecycle.js +4 -2
  8. package/dist/app/session-shell/session-shell-root.d.ts +2 -0
  9. package/dist/app/session-shell/session-shell-root.js +3 -2
  10. package/dist/app/session-shell/session-shell.js +9 -1
  11. package/dist/app/session-shell/session-viewport-controller.d.ts +2 -1
  12. package/dist/app/session-shell/session-viewport-controller.js +26 -37
  13. package/dist/app/session-shell/shell-editor-factory.js +4 -1
  14. package/dist/cli/dispatch.js +15 -2
  15. package/dist/composition/owned-ui.d.ts +2 -0
  16. package/dist/composition/owned-ui.js +43 -11
  17. package/dist/composition/reference-selection-options.d.ts +3 -0
  18. package/dist/composition/reference-selection-options.js +16 -0
  19. package/dist/composition/settings-route-host.d.ts +1 -2
  20. package/dist/composition/settings-route-host.js +19 -13
  21. package/dist/composition/settings-search-input.js +4 -1
  22. package/dist/composition/utility-tab.js +4 -2
  23. package/dist/features/launch/intent.js +2 -2
  24. package/dist/features/owned-ui/plus-menu-dialog.js +28 -20
  25. package/dist/features/owned-ui/reference-document-selection.d.ts +30 -0
  26. package/dist/features/owned-ui/reference-document-selection.js +179 -0
  27. package/dist/features/owned-ui/reference-screen-app.d.ts +4 -1
  28. package/dist/features/owned-ui/reference-screen-app.js +39 -8
  29. package/dist/features/owned-ui/settings-app.d.ts +1 -0
  30. package/dist/features/owned-ui/settings-app.js +23 -19
  31. package/dist/foundation/release/automatic-update.js +90 -22
  32. package/dist/foundation/release/bootstrap.js +2 -2
  33. package/dist/foundation/release/latest-release.d.ts +5 -1
  34. package/dist/foundation/release/latest-release.js +74 -31
  35. package/dist/integrations/pi/components/owned-editor-ux.js +48 -5
  36. package/dist/integrations/pi/engine/workflow-runner.js +1 -1
  37. package/dist/native/darwin-arm64/manifest.json +1 -1
  38. package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
  39. package/dist/native/darwin-arm64/terminal-host.node +0 -0
  40. package/dist/native/darwin-x64/manifest.json +1 -1
  41. package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
  42. package/dist/native/darwin-x64/terminal-host.node +0 -0
  43. package/dist/native/linux-x64/manifest.json +1 -1
  44. package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
  45. package/dist/native/linux-x64/terminal-host.node +0 -0
  46. package/dist/native/win32-x64/manifest.json +2 -2
  47. package/dist/native/win32-x64/process-guardian.node +0 -0
  48. package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
  49. package/dist/native/win32-x64/terminal-host.node +0 -0
  50. package/dist/ui/components/dialog-panel.d.ts +17 -8
  51. package/dist/ui/components/dialog-panel.js +97 -26
  52. package/dist/ui/components/index.d.ts +3 -3
  53. package/dist/ui/components/index.js +3 -3
  54. package/dist/ui/components/scrollbar.d.ts +1 -1
  55. package/dist/ui/components/scrollbar.js +1 -1
  56. package/dist/ui/components/selection-copy.d.ts +2 -4
  57. package/dist/ui/components/selection-copy.js +4 -2
  58. package/dist/ui/components/text-selection.d.ts +1 -2
  59. package/dist/ui/components/text-selection.js +5 -2
  60. package/dist/ui/components/transcript-viewport.js +3 -3
  61. package/dist/ui/settings/declarations.d.ts +1 -1
  62. package/dist/ui/settings/declarations.js +1 -1
  63. package/docs/architecture/resident-tabs.md +5 -4
  64. package/docs/features/launch-profiles.md +7 -4
  65. package/package.json +3 -3
package/README.md CHANGED
@@ -63,6 +63,8 @@ Launch A1 in single agent mode:
63
63
  a1 solo
64
64
  ```
65
65
 
66
+ When a solo session exits, its printed `a1 solo --session <id>` command resumes that same conversation in solo mode; a custom session store is included in the printed command. Resident tabs are different: they keep running after detach, and bare `a1` returns to them.
67
+
66
68
  Show all commands:
67
69
 
68
70
  ```text
@@ -92,7 +94,7 @@ a1 tabs
92
94
  a1 tabs stop <id>
93
95
  ```
94
96
 
95
- Stop every tab and the resident host, for example before going back to single agent mode with `a1 solo`:
97
+ Stop every tab and the resident host when you are finished with them (not required to run `a1 solo`):
96
98
 
97
99
  ```text
98
100
  a1 tabs host stop
@@ -145,13 +147,13 @@ Stuck on `0.2.2` and `a1 update` fails? Run the installer once, then use `a1 upd
145
147
  npm x -y -- @timurproko/a1-install
146
148
  ```
147
149
 
148
- The **Update** setting controls the at-most-daily startup check:
150
+ The **Update** setting controls release discovery:
149
151
 
150
- - **Auto** (default) silently installs a verified update for the running channel, then shows `Update installed · Restart to update`.
151
- - **Notify** keeps the `Update Available` notice and leaves installation manual.
152
- - **No** performs no startup update check; updates remain available through the commands above.
152
+ - **Auto** (default) checks the running release's stable or development channel at startup and every 15 minutes while open. It silently installs a verified update, then shows `Update installed · Restart to update`; existing sessions keep running until restarted.
153
+ - **Notify** checks at startup (at most daily), keeps the `Update Available` notice, and leaves installation manual.
154
+ - **No** performs no update check; updates remain available through the commands above.
153
155
 
154
- `A1_SKIP_VERSION_CHECK=1` also disables the startup check. Manual stable, development, and exact-version update commands remain available in every mode.
156
+ `A1_SKIP_VERSION_CHECK=1` disables both Auto and Notify discovery. Manual stable, development, and exact-version update commands remain available in every mode.
155
157
 
156
158
  <p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/assets/readme/separator-dark.svg"><img src="docs/assets/readme/separator.svg" alt="" width="280"></picture></p>
157
159
 
package/bin/ui.js CHANGED
@@ -103,6 +103,7 @@ if (utilityRoute !== null) {
103
103
  ownedSurfaces,
104
104
  notificationVersion: launchContext.updateVersion,
105
105
  ...(hidesTabSettings(launchContext) ? { hideTabSettings: true } : {}),
106
+ soloResumeHint: profile === "a1" && launchContext.launchMode === "solo",
106
107
  ...(processInspector === null ? {} : { inspectProcess: pid => processInspector.observe(pid) }),
107
108
  projectTrustPrompt: createConsoleProjectTrustPrompt({ presentation: profile === "a1" ? "bare" : "comparison" }),
108
109
  sessionForkPrompt: createConsoleSessionForkPrompt(),
@@ -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.
@@ -13,6 +13,6 @@ export interface ExitNoticeSource {
13
13
  export interface ArmedExitNotice {
14
14
  clear(): void;
15
15
  }
16
- export declare function uncleanExitNoticeLines(resume: SessionResumeCommandMetadata | null): string[];
16
+ export declare function uncleanExitNoticeLines(resume: SessionResumeCommandMetadata | null, soloResumeHint?: boolean): string[];
17
17
  /** Arms the notice and keeps its resume command following the bound session until cleared. */
18
- export declare function armExitNotice(source: ExitNoticeSource, path: string): ArmedExitNotice;
18
+ export declare function armExitNotice(source: ExitNoticeSource, path: string, soloResumeHint?: boolean): ArmedExitNotice;
@@ -7,14 +7,14 @@
7
7
  import { renameSync, rmSync, writeFileSync } from "node:fs";
8
8
  import { PRODUCT_IDENTITY } from "../../product-identity.js";
9
9
  import { formatSessionResumeCommand } from "./session-lifecycle.js";
10
- export function uncleanExitNoticeLines(resume) {
10
+ export function uncleanExitNoticeLines(resume, soloResumeHint = false) {
11
11
  return [
12
12
  `${PRODUCT_IDENTITY.displayName} was stopped unexpectedly.`,
13
- ...(resume === null ? [] : [`To resume this session: ${formatSessionResumeCommand(resume)}`]),
13
+ ...(resume === null ? [] : [`To resume this session: ${formatSessionResumeCommand(resume, soloResumeHint)}`]),
14
14
  ];
15
15
  }
16
16
  /** Arms the notice and keeps its resume command following the bound session until cleared. */
17
- export function armExitNotice(source, path) {
17
+ export function armExitNotice(source, path, soloResumeHint = false) {
18
18
  let current = null;
19
19
  let cleared = false;
20
20
  let generation = source.identity.sessionGeneration;
@@ -22,7 +22,7 @@ export function armExitNotice(source, path) {
22
22
  if (cleared)
23
23
  return;
24
24
  // Protocol: plain text only; the guardian strips control characters, so styling would be lost.
25
- const text = `${uncleanExitNoticeLines(source.identity.currentSessionResumeMetadata()).join("\n")}\n`;
25
+ const text = `${uncleanExitNoticeLines(source.identity.currentSessionResumeMetadata(), soloResumeHint).join("\n")}\n`;
26
26
  if (text === current)
27
27
  return;
28
28
  try {
@@ -3,13 +3,14 @@ export interface SessionResumeCommandMetadata {
3
3
  readonly sessionDir: string;
4
4
  readonly usesDefaultSessionDir: boolean;
5
5
  }
6
- export declare function formatSessionResumeCommand(metadata: SessionResumeCommandMetadata): string;
6
+ export declare function formatSessionResumeCommand(metadata: SessionResumeCommandMetadata, solo?: boolean): string;
7
7
  export declare function quoteCommandArgument(value: string): string;
8
8
  export interface ExitTextInput {
9
9
  readonly terminalMode: "regular" | "fullscreen";
10
10
  readonly customViewport: boolean;
11
11
  readonly exitMode: "transcript" | "resume-hint";
12
12
  readonly resume: SessionResumeCommandMetadata | null;
13
+ readonly soloResumeHint?: boolean;
13
14
  /** The styled transcript, rendered only when the pinned profile asks for it. */
14
15
  readonly transcript: () => string;
15
16
  }
@@ -1,6 +1,8 @@
1
1
  import { PRODUCT_TEXT } from "../../product-identity.js";
2
- export function formatSessionResumeCommand(metadata) {
2
+ export function formatSessionResumeCommand(metadata, solo = false) {
3
3
  const args = [PRODUCT_TEXT.commandName];
4
+ if (solo)
5
+ args.push("solo");
4
6
  if (!metadata.usesDefaultSessionDir)
5
7
  args.push("--session-dir", quoteCommandArgument(metadata.sessionDir));
6
8
  args.push("--session", metadata.sessionId);
@@ -18,7 +20,7 @@ export function quoteCommandArgument(value) {
18
20
  export function composeExitText(input) {
19
21
  if (input.terminalMode !== "fullscreen")
20
22
  return "";
21
- const resumeHint = input.resume === null ? "" : `${dim("To resume this session:")} ${formatSessionResumeCommand(input.resume)}`;
23
+ const resumeHint = input.resume === null ? "" : `${dim("To resume this session:")} ${formatSessionResumeCommand(input.resume, input.soloResumeHint)}`;
22
24
  // Invariant: bare A1 leaves only the hint behind; the pinned comparison profile still
23
25
  // honors fullscreenExitOutput, including the styled transcript.
24
26
  if (input.customViewport || input.exitMode === "resume-hint")
@@ -73,6 +73,8 @@ export interface OwnedUiShellEngineOptions {
73
73
  /** Terminal, startup, viewport, and presentation-cadence seams; production leaves most at their defaults. */
74
74
  export interface OwnedUiShellPresentationOptions {
75
75
  readonly terminal?: OwnedUiTerminalPort;
76
+ /** Explicit direct solo launch, not a resident fallback or comparison launch. */
77
+ readonly soloResumeHint?: boolean;
76
78
  readonly startup?: OwnedUiStartupOptions;
77
79
  /** Live profile-local settings, supplied only to the bare-A1 composition. */
78
80
  readonly viewportSettings?: OwnedUiViewportSettingsPort;
@@ -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";
@@ -27,6 +28,8 @@ import { writeSystemClipboardText, } from "./system-clipboard.js";
27
28
  import { OwnedUiSessionShellRoot, shellResourceEntries, } from "./session-shell-root.js";
28
29
  // Invariant: a disposed shell leaves the engine a host that holds no reference back to it.
29
30
  const DETACHED_WORKFLOW_INTERACTION = { prompt: async () => null, notify() { } };
31
+ // Invariant: the resume mode belongs to the presenter instance, not the persisted session or profile.
32
+ const SOLO_RESUME_HINTS = new WeakSet();
30
33
  /** Resident Agents refuse the commands their tab bar serves instead, naming where they live. */
31
34
  const RESIDENT_UTILITY_TAB_REFUSALS = Object.freeze({
32
35
  settings: "Settings is available from the global Settings tab.",
@@ -108,6 +111,8 @@ export class OwnedUiSessionPresenter {
108
111
  this.#sessionBindingGeneration = this.backend.identity.sessionBindingGeneration;
109
112
  this.#suggestionModelKey = suggestionModelKey(this.backend.session.view());
110
113
  this.#customViewport = sessionLayout === "custom-viewport";
114
+ if (options.presentation?.soloResumeHint)
115
+ SOLO_RESUME_HINTS.add(this);
111
116
  this.#inputCoordination = inputPresentation?.coordination !== false;
112
117
  this.#host = host;
113
118
  this.#terminal = host.connect(this);
@@ -499,6 +504,8 @@ export class OwnedUiSessionPresenter {
499
504
  const snapshot = routed.copySelection;
500
505
  this.#trackCopy(this.#responseCopy?.submit(snapshot, this.#pendingClipboardWrite), copyCharacters(snapshot));
501
506
  }
507
+ if (routed.copyLastMessage)
508
+ void this.runWorkflow({ command: "copy", argument: "" });
502
509
  if (!routed.consumed)
503
510
  return routed.data === data ? undefined : { data: routed.data };
504
511
  return routed.data.length === 0 ? { consume: true } : { data: routed.data };
@@ -509,7 +516,7 @@ export class OwnedUiSessionPresenter {
509
516
  if (count === undefined || this.#disposed || !this.#terminal.running || intent !== this.#copyIntentSequence
510
517
  || outcome !== "delivered" && outcome !== "submitted-unverified")
511
518
  return;
512
- this.root.showCopyAcknowledgement(`copied ${count} chars to clipboard`);
519
+ this.root.showCopyAcknowledgement(copyAcknowledgementText(count));
513
520
  });
514
521
  }
515
522
  #promptSuggestionPresentationBlockReason(identity) {
@@ -1081,6 +1088,7 @@ export class OwnedUiSessionPresenter {
1081
1088
  customViewport: this.#customViewport,
1082
1089
  exitMode: this.backend.identity.disposed ? this.#fullscreenExitOutput : this.backend.settings.snapshot().fullscreenExitOutput,
1083
1090
  resume: this.backend.identity.currentSessionResumeMetadata(),
1091
+ soloResumeHint: SOLO_RESUME_HINTS.has(this),
1084
1092
  transcript: () => this.root.exitTranscript(terminal.columns),
1085
1093
  });
1086
1094
  });
@@ -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());
@@ -7,6 +7,8 @@ export function cliUsage(capabilities) {
7
7
  "--session <path|id>",
8
8
  "--session-dir <dir> --session <path|id>",
9
9
  "solo",
10
+ "solo --session <path|id>",
11
+ "solo --session-dir <dir> --session <path|id>",
10
12
  ...(capabilities.developmentComparison ? ["pi"] : []),
11
13
  "help",
12
14
  "version",
@@ -36,6 +38,8 @@ export function cliHelp(capabilities) {
36
38
  ` ${command} --session <path|id>`,
37
39
  ` ${command} --session-dir <dir> --session <path|id>`,
38
40
  ` ${command} solo`,
41
+ ` ${command} solo --session <path|id>`,
42
+ ` ${command} solo --session-dir <dir> --session <path|id>`,
39
43
  ...(capabilities.developmentComparison ? [` ${command} pi`] : []),
40
44
  ` ${command} help`,
41
45
  ` ${command} version`,
@@ -145,8 +149,17 @@ export function parseCliCommand(arguments_, capabilities) {
145
149
  return parsePackageCommand(rest, "pi");
146
150
  return capabilities.developmentComparison ? { kind: "launch", profileId: "pi", launchMode: "solo" } : { kind: "noop" };
147
151
  }
148
- if (command === "solo")
149
- return withoutArguments(rest, { kind: "launch", profileId: "a1", launchMode: "solo" });
152
+ if (command === "solo") {
153
+ if (rest.length === 0)
154
+ return { kind: "launch", profileId: "a1", launchMode: "solo" };
155
+ try {
156
+ const sessionSelection = parseSessionSelection(rest);
157
+ return { kind: "launch", profileId: "a1", launchMode: "solo", ...(sessionSelection === undefined ? {} : { sessionSelection }) };
158
+ }
159
+ catch (error) {
160
+ return { kind: "error", message: PRODUCT_TEXT.diagnostic(`could not parse solo session launch: ${error instanceof Error ? error.message : String(error)}`) };
161
+ }
162
+ }
150
163
  if (command === "update")
151
164
  return parseUpdate(rest);
152
165
  if (command === "session")
@@ -24,6 +24,8 @@ export interface OwnedUiCompositionOptions {
24
24
  readonly profileId?: string;
25
25
  /** Hide resident-only tab controls for the explicit direct single-agent launch. */
26
26
  readonly hideTabSettings?: boolean;
27
+ /** The explicit solo launch owns a mode-specific resume hint; resident fallback keeps the tabbed form. */
28
+ readonly soloResumeHint?: boolean;
27
29
  /** `off` withholds A1-specific surfaces from comparison profiles. */
28
30
  readonly ownedSurfaces?: "on" | "off";
29
31
  readonly projectTrustPrompt?: PiProjectTrustPreflightPrompt;
@@ -210,7 +210,7 @@ export async function composeOwnedUi(options = {}) {
210
210
  },
211
211
  presentation: {
212
212
  ...(options.terminal === undefined ? {} : { terminal: createPiTerminalBridge(options.terminal) }),
213
- ...residentStartupPresentation(tabBridge !== null, options.residentFallbackNotice),
213
+ ...residentStartupPresentation(tabBridge !== null, options.residentFallbackNotice), soloResumeHint: options.soloResumeHint === true && tabBridge === null,
214
214
  ...(clipboardDiagnostics === null ? {} : { input: { onEvent: event => clipboardDiagnostics.runtime(event) } }),
215
215
  ...(viewportSettings === null ? {} : { viewportSettings }),
216
216
  ...(backgroundSettings === null ? {} : { backgroundSettings }),
@@ -286,7 +286,7 @@ export async function composeOwnedUi(options = {}) {
286
286
  // Performance: the guardian notice loads beside first paint, never on the startup path.
287
287
  if (exitNoticePath)
288
288
  exitNotice = import("../app/session-shell/exit-notice.js")
289
- .then(module => module.armExitNotice(backend, exitNoticePath), () => null);
289
+ .then(module => module.armExitNotice(backend, exitNoticePath, options.soloResumeHint === true && tabBridge === null), () => null);
290
290
  presenter.start();
291
291
  releaseUpdates.start();
292
292
  },
@@ -345,17 +345,34 @@ function createPromptHistoryConfiguration(settings, backend, ownedSurfaces) {
345
345
  }
346
346
  function createStartupReleaseUpdateObservation(input) {
347
347
  const controller = new AbortController();
348
+ let timer;
349
+ let announcedVersion = null;
350
+ let eligible = input.mode === "auto";
351
+ let cadence = null;
352
+ let nextDelay = null;
353
+ let started = false;
348
354
  const observe = async () => {
349
- const check = input.checkForNewerRelease
350
- ?? (await import("../foundation/release/latest-release.js")).checkForNewerRelease;
355
+ // Performance: registry code and its locks stay outside the first-frame startup graph.
356
+ const releases = await import("../foundation/release/latest-release.js");
357
+ cadence = releases.UPDATE_CHECK_INTERVAL_MS;
358
+ nextDelay = cadence;
359
+ const check = input.checkForNewerRelease ?? releases.checkForNewerRelease;
351
360
  const runningVersion = input.packageVersion ?? await readPackageVersion();
352
361
  const discoveryVersion = input.mode === "notify" ? input.notificationVersion ?? runningVersion : runningVersion;
353
- const release = await check({
362
+ const options = {
354
363
  runningVersion: discoveryVersion,
355
364
  configDir: input.configDir,
356
365
  interactive: process.stdout.isTTY === true,
357
366
  settingMode: input.mode,
358
- });
367
+ signal: controller.signal,
368
+ onNextCheckDelay: milliseconds => { nextDelay = milliseconds; },
369
+ };
370
+ // Invariant: opt-outs never arm a recurring timer, nor read the cache or probe the registry.
371
+ if (input.checkForNewerRelease === undefined && releases.startupReleaseCheckSkipReason(options) !== null) {
372
+ eligible = false;
373
+ return;
374
+ }
375
+ const release = await check(options);
359
376
  if (release === null || controller.signal.aborted)
360
377
  return;
361
378
  if (input.mode === "notify") {
@@ -365,14 +382,27 @@ function createStartupReleaseUpdateObservation(input) {
365
382
  if (input.mode === "auto") {
366
383
  const installed = await import("../foundation/release/automatic-update.js")
367
384
  .then(module => module.requestAutomaticUpdate({ release, runningVersion, signal: controller.signal }));
368
- if (installed !== null && !controller.signal.aborted) {
385
+ if (installed !== null && installed !== announcedVersion && !controller.signal.aborted) {
386
+ announcedVersion = installed;
369
387
  input.backend.extensions.announceReleaseUpdate({ status: "installed", version: installed });
370
388
  }
371
389
  }
372
390
  };
391
+ const run = async () => {
392
+ try {
393
+ await observe();
394
+ }
395
+ catch { /* Rationale: best effort; retry on the next interval. */ }
396
+ if (eligible && cadence !== null && !controller.signal.aborted) {
397
+ timer = setTimeout(() => { void run(); }, nextDelay ?? cadence);
398
+ timer.unref?.();
399
+ }
400
+ };
373
401
  return {
374
- start() { void observe().catch(() => undefined); },
375
- stop() { controller.abort(); },
402
+ start() { if (started || controller.signal.aborted)
403
+ return; started = true; void run(); },
404
+ stop() { controller.abort(); if (timer !== undefined)
405
+ clearTimeout(timer); },
376
406
  };
377
407
  }
378
408
  export { watchSharedSettings } from "./owned-settings.js";
@@ -383,8 +413,10 @@ function settingsRouteOptions(tabBridge, keybindings, agentDir, cwd) {
383
413
  agentDir,
384
414
  cwd,
385
415
  copyText: text => {
386
- if (Buffer.byteLength(text) <= MAX_CLIPBOARD_TEXT_BYTES)
387
- void writeSystemClipboardText(text).catch(() => { });
416
+ if (Buffer.byteLength(text) > MAX_CLIPBOARD_TEXT_BYTES)
417
+ return false;
418
+ void writeSystemClipboardText(text).catch(() => { });
419
+ return true;
388
420
  },
389
421
  };
390
422
  }
@@ -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;