@timurproko/a1 0.2.6-dev.717 → 0.2.6-dev.734

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 (213) hide show
  1. package/bin/cli.js +13 -2
  2. package/bin/ui.js +13 -0
  3. package/dist/app/session-shell/clipboard-services.d.ts +19 -0
  4. package/dist/app/session-shell/clipboard-services.js +7 -0
  5. package/dist/app/session-shell/exit-notice.d.ts +9 -5
  6. package/dist/app/session-shell/exit-notice.js +5 -5
  7. package/dist/app/session-shell/index.d.ts +3 -1
  8. package/dist/app/session-shell/index.js +1 -0
  9. package/dist/app/session-shell/paste-preparation-client.d.ts +3 -0
  10. package/dist/app/session-shell/paste-preparation-client.js +5 -2
  11. package/dist/app/session-shell/path-chip-presentation.d.ts +10 -3
  12. package/dist/app/session-shell/path-chip-presentation.js +48 -6
  13. package/dist/app/session-shell/prompt-chips.js +30 -4
  14. package/dist/app/session-shell/prompt-suggestion-controller.d.ts +5 -4
  15. package/dist/app/session-shell/prompt-suggestion-controller.js +148 -58
  16. package/dist/app/session-shell/quit-outro.d.ts +3 -1
  17. package/dist/app/session-shell/quit-outro.js +23 -8
  18. package/dist/app/session-shell/response-copy-transport.d.ts +6 -1
  19. package/dist/app/session-shell/response-copy-transport.js +8 -2
  20. package/dist/app/session-shell/session-shell-root.d.ts +22 -11
  21. package/dist/app/session-shell/session-shell-root.js +19 -12
  22. package/dist/app/session-shell/session-shell.d.ts +19 -21
  23. package/dist/app/session-shell/session-shell.js +678 -840
  24. package/dist/app/session-shell/session-viewport-controller.js +33 -21
  25. package/dist/cli/dispatch.js +5 -3
  26. package/dist/cli/session-context.d.ts +5 -1
  27. package/dist/cli/session-context.js +25 -8
  28. package/dist/composition/owned-ui.d.ts +23 -2
  29. package/dist/composition/owned-ui.js +124 -33
  30. package/dist/composition/session-info-reference.d.ts +2 -2
  31. package/dist/composition/session-info-reference.js +2 -2
  32. package/dist/composition/settings-route-host.js +2 -1
  33. package/dist/contracts/agent-engine/capability-ports.d.ts +1 -85
  34. package/dist/contracts/agent-engine/capability-ports.js +1 -35
  35. package/dist/contracts/agent-engine/index.d.ts +1 -7
  36. package/dist/contracts/agent-engine/index.js +0 -4
  37. package/dist/contracts/owned-ui/index.d.ts +9 -3
  38. package/dist/contracts/owned-ui/index.js +2 -1
  39. package/dist/contracts/owned-ui/model.d.ts +10 -0
  40. package/dist/contracts/owned-ui/session-backend.d.ts +119 -0
  41. package/dist/contracts/owned-ui/session-presenters.d.ts +55 -0
  42. package/dist/contracts/owned-ui/session-presenters.js +1 -0
  43. package/dist/contracts/owned-ui/session-workflows.d.ts +342 -0
  44. package/dist/contracts/owned-ui/session-workflows.js +30 -0
  45. package/dist/contracts/owned-ui/suggestion-diagnostics.d.ts +5 -1
  46. package/dist/contracts/owned-ui/suggestion-diagnostics.js +3 -1
  47. package/dist/contracts/owned-ui/validation.js +7 -0
  48. package/dist/features/launch/index.d.ts +1 -1
  49. package/dist/features/launch/session-context.d.ts +17 -2
  50. package/dist/features/launch/session-context.js +9 -5
  51. package/dist/features/owned-ui/settings-app.js +56 -54
  52. package/dist/features/prompt-suggestions/diagnostics.js +4 -1
  53. package/dist/foundation/lifecycle/index.d.ts +2 -2
  54. package/dist/foundation/lifecycle/index.js +1 -1
  55. package/dist/foundation/lifecycle/session-repository-context.d.ts +19 -2
  56. package/dist/foundation/lifecycle/session-repository-context.js +434 -66
  57. package/dist/foundation/process-containment/index.d.ts +1 -0
  58. package/dist/foundation/process-containment/index.js +1 -0
  59. package/dist/foundation/process-containment/native-process-inspector.d.ts +3 -0
  60. package/dist/foundation/process-containment/native-process-inspector.js +21 -0
  61. package/dist/foundation/release/bootstrap.js +1 -2
  62. package/dist/foundation/release/progress-palette.generated.d.ts +2 -2
  63. package/dist/foundation/release/progress-palette.generated.js +2 -2
  64. package/dist/foundation/release/release-gc.js +1 -1
  65. package/dist/foundation/release/update.js +0 -4
  66. package/dist/foundation/startup/startup-descriptor.js +2 -2
  67. package/dist/foundation/storage/control-store.js +0 -12
  68. package/dist/integrations/pi/components/components.js +2 -18
  69. package/dist/integrations/pi/components/index.d.ts +4 -4
  70. package/dist/integrations/pi/components/index.js +2 -2
  71. package/dist/integrations/pi/components/modal-frame.js +3 -4
  72. package/dist/integrations/pi/components/models-dialog.js +6 -35
  73. package/dist/integrations/pi/components/prompt-input-port.d.ts +1 -0
  74. package/dist/integrations/pi/components/resources/builtin-themes.d.ts +2 -2
  75. package/dist/integrations/pi/components/resources/builtin-themes.js +2 -2
  76. package/dist/integrations/pi/components/share-operation-dialog.js +3 -3
  77. package/dist/integrations/pi/components/shell-editor-autocomplete.js +19 -16
  78. package/dist/integrations/pi/components/shell-extension-ui.d.ts +5 -3
  79. package/dist/integrations/pi/components/shell-extension-ui.js +26 -12
  80. package/dist/integrations/pi/components/shell-hotkey-sections.js +2 -2
  81. package/dist/integrations/pi/components/shell-presenters-info.js +10 -10
  82. package/dist/integrations/pi/components/shell-presenters-transcript.js +13 -13
  83. package/dist/integrations/pi/components/shell-selectors-dialogs.js +5 -6
  84. package/dist/integrations/pi/components/shell-shared-facade.d.ts +21 -1
  85. package/dist/integrations/pi/components/shell-shared-facade.js +19 -3
  86. package/dist/integrations/pi/components/skills-dialog.js +3 -3
  87. package/dist/integrations/pi/components/submitted-prompt-adapter.js +2 -3
  88. package/dist/integrations/pi/components/theme.d.ts +9 -0
  89. package/dist/integrations/pi/components/theme.js +30 -0
  90. package/dist/integrations/pi/components/tool-result-adapter.js +6 -1
  91. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -1
  92. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +1 -1
  93. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +2 -2
  94. package/dist/integrations/pi/components/upstream/components/countdown-timer.d.ts +1 -1
  95. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +1 -1
  96. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +1 -1
  97. package/dist/integrations/pi/components/upstream/components/extension-editor.js +6 -6
  98. package/dist/integrations/pi/components/upstream/components/extension-input.js +4 -4
  99. package/dist/integrations/pi/components/upstream/components/extension-selector.js +4 -4
  100. package/dist/integrations/pi/components/upstream/components/mermaid.js +1 -1
  101. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +5 -5
  102. package/dist/integrations/pi/components/upstream/components/owned-editor.js +8 -5
  103. package/dist/integrations/pi/components/upstream/components/pi-logo.js +1 -1
  104. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
  105. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +3 -3
  106. package/dist/integrations/pi/components/upstream/components/session-footer.js +1 -1
  107. package/dist/integrations/pi/components/upstream/components/session-selector-search.js +1 -1
  108. package/dist/integrations/pi/components/upstream/components/session-selector.js +9 -9
  109. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.d.ts +3 -2
  110. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.js +6 -3
  111. package/dist/integrations/pi/components/upstream/components/status-indicator.d.ts +1 -1
  112. package/dist/integrations/pi/components/upstream/components/status-indicator.js +1 -1
  113. package/dist/integrations/pi/components/upstream/components/themed-text.d.ts +1 -1
  114. package/dist/integrations/pi/components/upstream/components/themed-text.js +1 -1
  115. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +1 -1
  116. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +2 -2
  117. package/dist/integrations/pi/components/upstream/components/tool-execution.d.ts +4 -0
  118. package/dist/integrations/pi/components/upstream/components/tool-execution.js +11 -1
  119. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +2 -2
  120. package/dist/integrations/pi/components/upstream/components/tree-selector.js +27 -18
  121. package/dist/integrations/pi/components/upstream/components/trust-selector.js +1 -1
  122. package/dist/integrations/pi/components/upstream/external-editor.js +1 -1
  123. package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +4 -4
  124. package/dist/integrations/pi/components/upstream/history/editor-core.js +11 -7
  125. package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +1 -1
  126. package/dist/integrations/pi/components/upstream/history/kill-ring.js +1 -1
  127. package/dist/integrations/pi/components/upstream/history/printable-key.js +1 -1
  128. package/dist/integrations/pi/components/upstream/history/text-helpers.js +1 -1
  129. package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +1 -1
  130. package/dist/integrations/pi/components/upstream/history/undo-stack.js +1 -1
  131. package/dist/integrations/pi/components/upstream/history/word-navigation.js +1 -1
  132. package/dist/integrations/pi/components/upstream/model-search.d.ts +1 -1
  133. package/dist/integrations/pi/components/upstream/program-status-reporter.d.ts +31 -0
  134. package/dist/integrations/pi/components/upstream/program-status-reporter.js +89 -0
  135. package/dist/integrations/pi/components/upstream/theme/theme-controller.d.ts +1 -1
  136. package/dist/integrations/pi/components/upstream/theme/theme.d.ts +22 -1
  137. package/dist/integrations/pi/components/upstream/theme/theme.js +225 -10
  138. package/dist/integrations/pi/engine/adapter.d.ts +44 -24
  139. package/dist/integrations/pi/engine/adapter.js +130 -22
  140. package/dist/integrations/pi/engine/command-dispatch.d.ts +2 -5
  141. package/dist/integrations/pi/engine/extension-ui-binding.d.ts +2 -11
  142. package/dist/integrations/pi/engine/host-ports.d.ts +20 -0
  143. package/dist/integrations/pi/engine/host-ports.js +13 -0
  144. package/dist/integrations/pi/engine/host.d.ts +70 -0
  145. package/dist/integrations/pi/engine/host.js +147 -0
  146. package/dist/integrations/pi/engine/index.d.ts +8 -4
  147. package/dist/integrations/pi/engine/index.js +3 -1
  148. package/dist/integrations/pi/engine/pi-settings-metadata.json +1 -1
  149. package/dist/integrations/pi/engine/resource-catalog.d.ts +4 -23
  150. package/dist/integrations/pi/engine/resources/changelog.json +4 -4
  151. package/dist/integrations/pi/engine/runtime-integration.d.ts +8 -2
  152. package/dist/integrations/pi/engine/runtime-integration.js +12 -7
  153. package/dist/integrations/pi/engine/session-events.js +10 -2
  154. package/dist/integrations/pi/engine/session-integration.d.ts +3 -8
  155. package/dist/integrations/pi/engine/session-integration.js +0 -49
  156. package/dist/integrations/pi/engine/session-runtime.d.ts +18 -3
  157. package/dist/integrations/pi/engine/session-runtime.js +18 -7
  158. package/dist/integrations/pi/engine/settings-bridge.d.ts +2 -2
  159. package/dist/integrations/pi/engine/settings-bridge.js +0 -2
  160. package/dist/integrations/pi/engine/settings-port.d.ts +2 -0
  161. package/dist/integrations/pi/engine/settings-port.js +3 -5
  162. package/dist/integrations/pi/engine/transcript-projection.js +2 -1
  163. package/dist/integrations/pi/engine/workflows.d.ts +33 -260
  164. package/dist/integrations/pi/engine/workflows.js +6 -28
  165. package/dist/integrations/pi/session-presenters/index.d.ts +2 -0
  166. package/dist/integrations/pi/session-presenters/index.js +1 -0
  167. package/dist/integrations/pi/session-presenters/presenters.d.ts +16 -0
  168. package/dist/integrations/pi/session-presenters/presenters.js +341 -0
  169. package/dist/integrations/pi/startup-public.js +306 -290
  170. package/dist/integrations/pi/startup-public.manifest.json +289 -265
  171. package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -1
  172. package/dist/integrations/pi/tui-runtime/adapter.js +7 -2
  173. package/dist/integrations/pi/tui-runtime/contracts.d.ts +3 -1
  174. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +8 -1
  175. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +67 -2
  176. package/dist/integrations/pi/tui-runtime/index.d.ts +1 -1
  177. package/dist/integrations/pi/tui-runtime/index.js +1 -1
  178. package/dist/native/darwin-arm64/manifest.json +1 -1
  179. package/dist/native/linux-x64/manifest.json +1 -1
  180. package/dist/native/win32-x64/manifest.json +2 -2
  181. package/dist/native/win32-x64/process-guardian.exe +0 -0
  182. package/dist/product-identity.d.ts +2 -2
  183. package/dist/product-identity.js +2 -2
  184. package/dist/product-identity.json +3 -1
  185. package/dist/runtime-payload-inventory.json +3092 -2878
  186. package/dist/ui/components/prompt-input.d.ts +2 -0
  187. package/dist/ui/components/prompt-input.js +5 -1
  188. package/dist/ui/components/text-selection.d.ts +1 -1
  189. package/dist/ui/components/text-selection.js +3 -1
  190. package/dist/ui/components/theme.d.ts +2 -0
  191. package/dist/ui/components/transcript-viewport.d.ts +6 -4
  192. package/dist/ui/components/transcript-viewport.js +94 -16
  193. package/dist/ui/components/value-menu.d.ts +2 -0
  194. package/dist/ui/components/value-menu.js +13 -8
  195. package/dist/ui/settings/declarations.d.ts +38 -14
  196. package/dist/ui/settings/declarations.js +35 -11
  197. package/dist/ui/settings/migrations.js +23 -0
  198. package/docs/architecture/boundaries.md +9 -1
  199. package/docs/architecture/project-structure.md +7 -5
  200. package/docs/architecture/prompt-suggestions.md +14 -10
  201. package/docs/local-worktree-cleanup.md +11 -5
  202. package/docs/openspec-archive-automation.md +6 -5
  203. package/package.json +3 -3
  204. package/dist/contracts/agent-engine/model.d.ts +0 -78
  205. package/dist/contracts/agent-engine/model.js +0 -1
  206. package/dist/contracts/agent-engine/ports.d.ts +0 -18
  207. package/dist/contracts/agent-engine/serialization.d.ts +0 -7
  208. package/dist/contracts/agent-engine/serialization.js +0 -58
  209. package/dist/contracts/agent-engine/validation.d.ts +0 -5
  210. package/dist/contracts/agent-engine/validation.js +0 -128
  211. package/dist/integrations/pi/components/upstream/theme/system-theme.d.ts +0 -61
  212. package/dist/integrations/pi/components/upstream/theme/system-theme.js +0 -709
  213. /package/dist/contracts/{agent-engine/ports.js → owned-ui/session-backend.js} +0 -0
@@ -10,6 +10,8 @@ export interface PromptInputBody {
10
10
  readonly topRule?: string | undefined;
11
11
  readonly bottomRule?: string | undefined;
12
12
  readonly after?: readonly string[];
13
+ /** Outer indentation for after-body rows; defaults to the prompt prefix width. */
14
+ readonly afterIndent?: number;
13
15
  }
14
16
  export interface PromptInputMetrics {
15
17
  readonly measure: (text: string) => number;
@@ -31,6 +31,10 @@ export class PromptInput {
31
31
  return [];
32
32
  const { prefixWidth, innerWidth } = this.geometry(width, padding, promptGlyph);
33
33
  const body = renderBody(innerWidth);
34
+ const requestedAfterIndent = body.afterIndent ?? prefixWidth;
35
+ const afterIndent = Number.isFinite(requestedAfterIndent)
36
+ ? Math.min(width, Math.max(0, Math.floor(requestedAfterIndent)))
37
+ : prefixWidth;
34
38
  const fit = (row) => {
35
39
  const clipped = this.metrics.truncate(row, width);
36
40
  return clipped + " ".repeat(Math.max(0, width - this.metrics.measure(clipped)));
@@ -45,7 +49,7 @@ export class PromptInput {
45
49
  ...(ruled ? [rule(body.topRule)] : []),
46
50
  ...rows,
47
51
  ...(ruled ? [rule(body.bottomRule)] : []),
48
- ...(body.after ?? []).map(row => fit(`${" ".repeat(prefixWidth)}${row}`)),
52
+ ...(body.after ?? []).map(row => fit(`${" ".repeat(afterIndent)}${row}`)),
49
53
  ];
50
54
  }
51
55
  }
@@ -56,7 +56,7 @@ export declare function releaseTextSelection(selection: TextSelection | undefine
56
56
  export declare function textSelectionLineExtendColumn(selection: TextSelection, line: number, contentWidth: number): number;
57
57
  /** Resolves a reported one-based pointer cell to the complete grapheme it intersects. */
58
58
  export declare function textSelectionPointAt(line: number, column: number, lineText: string): TextSelectionPoint;
59
- export declare function textSelectionText(selection: OrderedTextSelection, rows: readonly string[], lineContent?: (line: number) => TextSelectionLineContent | undefined): string;
59
+ export declare function textSelectionText(selection: OrderedTextSelection, rows: readonly string[], lineContent?: (line: number) => TextSelectionLineContent | undefined, includeLine?: (line: number) => boolean): string;
60
60
  /** Returns the plain cells covered by [from,to), expanding at grapheme edges. */
61
61
  export declare function plainTextBetweenColumns(line: string, from: number, to: number): string;
62
62
  export declare function usefulTextLineContent(line: string, from?: number): TextSelectionLineContent;
@@ -111,9 +111,11 @@ export function textSelectionPointAt(line, column, lineText) {
111
111
  }
112
112
  return { line, column, from: target, to: target + 1 };
113
113
  }
114
- export function textSelectionText(selection, rows, lineContent) {
114
+ export function textSelectionText(selection, rows, lineContent, includeLine = () => true) {
115
115
  const parts = [];
116
116
  for (let line = selection.start.line; line <= selection.end.line && line < rows.length; line += 1) {
117
+ if (!includeLine(line))
118
+ continue;
117
119
  const plain = stripAnsi(rows[line] ?? "");
118
120
  const content = lineContent?.(line);
119
121
  if (content?.selectable === false) {
@@ -8,6 +8,8 @@ export interface UiTheme {
8
8
  * into a dark terminal, which is how the engine leaves its lists too.
9
9
  */
10
10
  plain(text: string): string;
11
+ /** Optional palette preview used by controls that choose the active accent. */
12
+ accentPreview?(color: string, text: string): string | null;
11
13
  /** Selection background for an active list item or floating-surface row. */
12
14
  highlight(text: string): string;
13
15
  /** A control that is present but cannot act: quieter than quiet text. */
@@ -25,7 +25,7 @@ export interface TranscriptViewportFrameInput {
25
25
  readonly documentRows: readonly string[];
26
26
  /** Optional paint-only transform; semantic selection and copying keep documentRows exact. */
27
27
  readonly paintDocumentRow?: (row: string) => string;
28
- /** Leading document rows that participate in pointer selection and copying. */
28
+ /** Leading persistent document rows; later transient rows remain selectable visible-frame content. */
29
29
  readonly selectableDocumentRowCount?: number;
30
30
  /**
31
31
  * First document row owned by the block that is currently streaming, if any. Rows from
@@ -84,7 +84,7 @@ export interface TranscriptViewportHitRegions {
84
84
  readonly columnStart: number;
85
85
  readonly columnEnd: number;
86
86
  } | null;
87
- /** Transient non-selectable document-tail rows currently visible in the viewport. */
87
+ /** Transient non-persistent document-tail rows currently visible in the viewport. */
88
88
  readonly transientTail: readonly number[];
89
89
  }
90
90
  export interface TranscriptViewportFrameDescriptor {
@@ -109,7 +109,7 @@ export interface TranscriptViewportFrameDescriptor {
109
109
  };
110
110
  readonly previousFollowingEnd: boolean | null;
111
111
  readonly followingEnd: boolean;
112
- /** Complete non-selectable suffix, including pending steering, alignment, and status rows. */
112
+ /** Complete non-persistent suffix, including pending steering, alignment, and status rows. */
113
113
  readonly transientRowCount: number;
114
114
  /** Flexible rows inserted before the bottom-aligned live status while content fits. */
115
115
  readonly transientAlignmentGapRows: number;
@@ -164,7 +164,9 @@ export declare class TranscriptViewport {
164
164
  get hasSelection(): boolean;
165
165
  /** True when the active gesture began on transcript rows rather than the dock. */
166
166
  get selectionFromContent(): boolean;
167
- pressSelection(column: number, frameRow: number, now?: number): boolean;
167
+ pressSelection(column: number, frameRow: number, now?: number, options?: {
168
+ readonly continueMultiClick?: boolean;
169
+ }): boolean;
168
170
  extendSelection(column: number, frameRow: number, now?: number, autoScroll?: boolean): boolean;
169
171
  releaseSelection(): boolean;
170
172
  clearSelection(): boolean;
@@ -1,4 +1,4 @@
1
- import { MAX_COPY_SOURCE_UNITS } from "./selection-copy.js";
1
+ import { MAX_COPY_SOURCE_UNITS, selectionCopyLineContent } from "./selection-copy.js";
2
2
  import { isThumbRow, scrollbarGeometry, scrollbarPresentation, } from "./scrollbar.js";
3
3
  import { backgroundSgrSpan, overlaySpan } from "./spans.js";
4
4
  import { faint, truncateToWidth, displayWidth, stripAnsi } from "./text.js";
@@ -8,8 +8,13 @@ const CONTROL_STYLE_RESET = "\u001b]8;;\u001b\\\u001b[0m";
8
8
  // the row surface painted beneath its dedicated, non-semantic gutter.
9
9
  const GUTTER_DECORATION_RESET = "\u001b]8;;\u001b\\\u001b[22;23;24;25;27;28;29;39;54;55m";
10
10
  const IDENTITY_ROW = (row) => row;
11
- function documentSelectionAnchor(row, source) {
12
- return { kind: "document", row, source, identity: stripAnsi(source).trimEnd() };
11
+ function documentSelectionAnchor(row, source, liveFromBottom) {
12
+ return { kind: "document", row, source, identity: stripAnsi(source).trimEnd(), liveFromBottom };
13
+ }
14
+ function viewportSelectionAnchor(row, source, selectableDocumentRowCount, documentRowCount) {
15
+ return row < selectableDocumentRowCount
16
+ ? documentSelectionAnchor(row, source)
17
+ : { kind: "transient", fromBottom: documentRowCount - row - 1 };
13
18
  }
14
19
  const DEFAULT_CONFIG = {
15
20
  scrollbarAppearance: "auto",
@@ -53,6 +58,7 @@ export class TranscriptViewport {
53
58
  #previousVisibleStates = [];
54
59
  #documentRows = [];
55
60
  #selectableDocumentRowCount = 0;
61
+ #liveTailStartRow;
56
62
  #promptAnchors = [];
57
63
  #contentWidth = 0;
58
64
  #selectionWidth = 0;
@@ -91,7 +97,7 @@ export class TranscriptViewport {
91
97
  get hasSelection() { return orderedTextSelection(this.#selection) !== undefined; }
92
98
  /** True when the active gesture began on transcript rows rather than the dock. */
93
99
  get selectionFromContent() { return this.#selectionAnchors !== undefined && this.#selectionAnchors.anchor.kind !== "dock"; }
94
- pressSelection(column, frameRow, now = Date.now()) {
100
+ pressSelection(column, frameRow, now = Date.now(), options = {}) {
95
101
  const frameHeight = this.#frame?.rows.length ?? 0;
96
102
  const selectionWidth = frameRow <= (this.#frame?.hits.viewportHeight ?? 0)
97
103
  ? this.#contentWidth
@@ -105,7 +111,7 @@ export class TranscriptViewport {
105
111
  contentWidth: selectionWidth,
106
112
  lineText: this.#selectionRows[line] ?? "",
107
113
  lineContent: usefulTextLineContent(this.#selectionRows[line] ?? ""),
108
- ...(this.#selectionClick === undefined ? {} : { previousClick: this.#selectionClick }),
114
+ ...(options.continueMultiClick !== false && this.#selectionClick !== undefined ? { previousClick: this.#selectionClick } : {}),
109
115
  now,
110
116
  });
111
117
  this.#selection = pressed.selection;
@@ -176,7 +182,29 @@ export class TranscriptViewport {
176
182
  const selection = this.#visibleSelection();
177
183
  if (selection === undefined)
178
184
  return null;
179
- const text = textSelectionText(selection, this.#selectionRows, line => usefulTextLineContent(this.#selectionRows[line] ?? ""));
185
+ // Invariant: the blue range spans the complete frame, but automatic and Ctrl+C frame copy
186
+ // admit only persistent transcript rows. Transient status, suggestions, input, and footer
187
+ // chrome remain visually selectable without becoming clipboard payload.
188
+ const copiedLines = Array.from({ length: Math.max(0, selection.end.line - selection.start.line + 1) }, (_value, index) => selection.start.line + index).filter(line => {
189
+ const anchor = this.#selectionRowAnchors[line];
190
+ return anchor?.kind === "document" && anchor.row < this.#selectableDocumentRowCount;
191
+ });
192
+ if (copiedLines.length === 0)
193
+ return null;
194
+ // Rationale: a prompt selected by itself uses semantic prompt text. Once surrounding transcript text
195
+ // participates, preserve the prompt's visible prefix, alignment, and timestamp in the bulk range.
196
+ const prompt = this.#promptAnchors.find(candidate => copiedLines.every(line => {
197
+ const anchor = this.#selectionRowAnchors[line];
198
+ return anchor?.kind === "document" && anchor.row >= candidate.firstRow && anchor.row <= candidate.lastRow;
199
+ }));
200
+ const included = new Set(copiedLines);
201
+ const text = textSelectionText(selection, this.#selectionRows, line => {
202
+ const row = this.#selectionRows[line] ?? "";
203
+ const anchor = this.#selectionRowAnchors[line];
204
+ return prompt !== undefined && anchor?.kind === "document"
205
+ ? selectionCopyLineContent(row, anchor.row === prompt.firstRow ? "first" : "continuation")
206
+ : usefulTextLineContent(row);
207
+ }, line => included.has(line));
180
208
  return text.length === 0 ? null : text;
181
209
  }
182
210
  /** Captures visible selected text, not off-screen history. */
@@ -188,6 +216,8 @@ export class TranscriptViewport {
188
216
  if (text === null)
189
217
  return null;
190
218
  text = text.trim();
219
+ if (text.length === 0)
220
+ return null;
191
221
  const normalized = {
192
222
  start: { line: 0, column: 0 },
193
223
  end: { line: 0, column: Number.MAX_SAFE_INTEGER },
@@ -280,6 +310,7 @@ export class TranscriptViewport {
280
310
  this.#previousVisibleStates = [];
281
311
  this.#documentRows = [];
282
312
  this.#selectableDocumentRowCount = 0;
313
+ this.#liveTailStartRow = undefined;
283
314
  this.#promptAnchors = [];
284
315
  this.#contentWidth = 0;
285
316
  this.#selectionWidth = 0;
@@ -340,6 +371,9 @@ export class TranscriptViewport {
340
371
  const contentWidth = presentation.reservesSpace ? Math.max(1, width - 1) : width;
341
372
  this.#documentRows = documentRows;
342
373
  this.#selectableDocumentRowCount = clamp(input.selectableDocumentRowCount ?? input.documentRows.length, 0, input.documentRows.length);
374
+ this.#liveTailStartRow = input.liveTailStartRow === undefined || !Number.isSafeInteger(input.liveTailStartRow)
375
+ ? undefined
376
+ : clamp(input.liveTailStartRow, 0, this.#selectableDocumentRowCount);
343
377
  this.#promptAnchors = input.promptAnchors;
344
378
  this.#contentWidth = contentWidth;
345
379
  // Invariant: dock rows remain selectable at full width; scrollable source rows
@@ -389,7 +423,7 @@ export class TranscriptViewport {
389
423
  const visibleAnchors = Array.from({ length: viewportHeight }, (_value, row) => {
390
424
  const documentRow = this.#scrollTop + row;
391
425
  return documentRow < documentRows.length
392
- ? documentSelectionAnchor(documentRow, documentRows[documentRow] ?? "")
426
+ ? viewportSelectionAnchor(documentRow, documentRows[documentRow] ?? "", this.#selectableDocumentRowCount, documentRows.length)
393
427
  : { kind: "screen", row, source: this.#selectionRows[row] ?? "" };
394
428
  });
395
429
  // Invariant: a pinned prompt is chrome over its hidden source row, which clips like off-screen rows.
@@ -486,7 +520,7 @@ export class TranscriptViewport {
486
520
  // Invariant: floating control styling is applied only after the row and
487
521
  // gutter surfaces are resolved, so its inline background cannot leak.
488
522
  const withControl = rowControl === null ? withRail : overlaySpan(withRail, rowControl.left, rowControl.right, rowControl.text);
489
- return overlayWidth === 0 ? withControl : overlaySpan(padRowPreservingBackground(withControl, width), overlayLimit - overlayWidth, overlayLimit, `${CONTROL_STYLE_RESET}${overlayText}`);
523
+ return overlayWidth === 0 ? withControl : overlaySpan(padRowPreservingBackground(withControl, width), overlayLimit - overlayWidth, overlayLimit, `${GUTTER_DECORATION_RESET}${overlayText}`, { inheritStartStyle: true });
490
524
  });
491
525
  rowRecomputed ||= !final.reused;
492
526
  frameRows[row] = final.value;
@@ -643,17 +677,30 @@ export class TranscriptViewport {
643
677
  return undefined;
644
678
  if (line < this.#viewportHeight) {
645
679
  const existing = this.#selectionRowAnchors[line];
646
- if (this.#frame?.scrollTop === this.#scrollTop)
647
- return existing;
680
+ if (this.#frame?.scrollTop === this.#scrollTop) {
681
+ return existing?.kind === "document"
682
+ ? this.#interactionDocumentAnchor(existing.row, existing.source)
683
+ : existing;
684
+ }
648
685
  // Concurrency: selection edge auto-scroll updates scrollTop before the next frame is
649
686
  // composed. Resolve ordinary viewport rows from that latest position instead of stale paint.
650
687
  const documentRow = this.#scrollTop + line;
651
688
  if (documentRow >= 0 && documentRow < this.#documentRows.length) {
652
- return documentSelectionAnchor(documentRow, this.#documentRows[documentRow] ?? "");
689
+ return documentRow < this.#selectableDocumentRowCount
690
+ ? this.#interactionDocumentAnchor(documentRow, this.#documentRows[documentRow] ?? "")
691
+ : viewportSelectionAnchor(documentRow, this.#documentRows[documentRow] ?? "", this.#selectableDocumentRowCount, this.#documentRows.length);
653
692
  }
654
693
  }
655
694
  return this.#selectionRowAnchors[line];
656
695
  }
696
+ #interactionDocumentAnchor(row, source) {
697
+ const liveFromBottom = this.#liveTailStartRow !== undefined
698
+ && row >= this.#liveTailStartRow
699
+ && row < this.#selectableDocumentRowCount
700
+ ? this.#selectableDocumentRowCount - row - 1
701
+ : undefined;
702
+ return documentSelectionAnchor(row, source, liveFromBottom);
703
+ }
657
704
  #projectSelection(documentRows, dockRows, height) {
658
705
  const selection = this.#selection;
659
706
  const anchors = this.#selectionAnchors;
@@ -672,9 +719,16 @@ export class TranscriptViewport {
672
719
  ? undefined
673
720
  : { line: anchor.row, anchor };
674
721
  }
722
+ if (anchor.kind === "transient") {
723
+ const documentRow = documentRows.length - anchor.fromBottom - 1;
724
+ if (documentRow < this.#selectableDocumentRowCount || documentRow >= documentRows.length)
725
+ return undefined;
726
+ const visible = this.#selectionRowAnchors.findIndex(candidate => candidate?.kind === "transient" && candidate.fromBottom === anchor.fromBottom);
727
+ return visible < 0 ? undefined : { line: visible, anchor };
728
+ }
675
729
  if (anchor.row >= 0 && anchor.row < documentRows.length
676
730
  && stripAnsi(documentRows[anchor.row] ?? "").trimEnd() === anchor.identity) {
677
- const current = documentSelectionAnchor(anchor.row, documentRows[anchor.row] ?? "");
731
+ const current = documentSelectionAnchor(anchor.row, documentRows[anchor.row] ?? "", anchor.liveFromBottom);
678
732
  const visible = this.#selectionRowAnchors.findIndex(candidate => candidate?.kind === "document"
679
733
  && candidate.row === current.row && candidate.identity === current.identity);
680
734
  return { line: visible >= 0 ? visible : current.row - this.#scrollTop, anchor: current };
@@ -684,9 +738,29 @@ export class TranscriptViewport {
684
738
  const visibleMatches = this.#selectionRowAnchors
685
739
  .map((candidate, line) => ({ candidate, line }))
686
740
  .filter((entry) => entry.candidate?.kind === "document" && entry.candidate.identity === anchor.identity);
687
- return visibleMatches.length === 1
688
- ? { line: visibleMatches[0].line, anchor: visibleMatches[0].candidate }
689
- : undefined;
741
+ if (visibleMatches.length === 1) {
742
+ const match = visibleMatches[0];
743
+ return {
744
+ line: match.line,
745
+ anchor: documentSelectionAnchor(match.candidate.row, match.candidate.source, anchor.liveFromBottom),
746
+ };
747
+ }
748
+ // Concurrency: a streaming chunk may replace or reflow the pointer's current live-tail
749
+ // row before the next motion report. During the held gesture only, retain its distance
750
+ // from the persistent tail; release restores identity-only projection.
751
+ if (selection.selecting && anchor.liveFromBottom !== undefined && this.#liveTailStartRow !== undefined) {
752
+ const documentRow = this.#selectableDocumentRowCount - anchor.liveFromBottom - 1;
753
+ if (documentRow >= this.#liveTailStartRow && documentRow < this.#selectableDocumentRowCount) {
754
+ const visible = this.#selectionRowAnchors.findIndex(candidate => candidate?.kind === "document" && candidate.row === documentRow);
755
+ if (visible >= 0) {
756
+ return {
757
+ line: visible,
758
+ anchor: documentSelectionAnchor(documentRow, documentRows[documentRow] ?? "", anchor.liveFromBottom),
759
+ };
760
+ }
761
+ }
762
+ }
763
+ return undefined;
690
764
  };
691
765
  const projectedAnchor = project(anchors.anchor);
692
766
  const projectedHead = project(anchors.head);
@@ -704,7 +778,11 @@ export class TranscriptViewport {
704
778
  };
705
779
  }
706
780
  #visibleSelection() {
707
- const rows = this.#selectionAnchors?.anchor.kind === "document" ? this.#viewportHeight : this.#selectionRows.length;
781
+ const anchors = this.#selectionAnchors;
782
+ const crossesDock = anchors?.anchor.kind === "dock" || anchors?.head.kind === "dock";
783
+ // Invariant: mixed viewport/dock ranges project across the same complete frame regardless
784
+ // of origin. Document-only ranges remain clipped to the scrollable viewport when off-screen.
785
+ const rows = crossesDock ? this.#selectionRows.length : this.#viewportHeight;
708
786
  return visibleTextSelection(orderedTextSelection(this.#selection), rows, this.#firstSelectableRow);
709
787
  }
710
788
  #updateCopyableSelection() {
@@ -7,6 +7,8 @@ import type { UiTheme } from "./theme.js";
7
7
  export interface ValueMenuState {
8
8
  /** Values offered, as they should read. */
9
9
  readonly choices: readonly string[];
10
+ /** Optional already-painted square previews, aligned before each choice label. */
11
+ readonly previews?: readonly string[];
10
12
  /** The value in effect, marked rather than highlighted. */
11
13
  readonly current: string | null;
12
14
  /** The entry picked by a key or the pointer, or -1 while none is. */
@@ -8,10 +8,12 @@ export function valueMenuFrame(state, anchor, layout) {
8
8
  const top = below + state.choices.length <= bodyBottom
9
9
  ? below
10
10
  : Math.max(bodyTop, anchor.screenRow - state.choices.length);
11
- const width = Math.max(...state.choices.map(choice => displayWidth(choice) + 4), 6);
12
- // Invariant: each row reserves two cells for its effective-value mark. Place those
13
- // cells before the anchor so visible choice text starts in the source value column.
14
- const column = Math.min(Math.max(0, anchor.valueColumn - 2), Math.max(0, layout.surfaceWidth - width - layout.reservedRight));
11
+ const previewWidth = Math.max(0, ...(state.previews ?? []).map(displayWidth));
12
+ const previewColumns = previewWidth === 0 ? 0 : previewWidth + 1;
13
+ const width = Math.max(...state.choices.map(choice => displayWidth(choice) + previewColumns + 4), 6);
14
+ // Invariant: rows reserve two cells for the effective-value mark and, when present,
15
+ // a preview plus one gap. Choice text remains aligned with the source value column.
16
+ const column = Math.min(Math.max(0, anchor.valueColumn - 2 - previewColumns), Math.max(0, layout.surfaceWidth - width - layout.reservedRight));
15
17
  return { top, column, width, rows: state.choices.length };
16
18
  }
17
19
  /** Draws the menu over the rows behind it, leaving them otherwise untouched. */
@@ -24,11 +26,14 @@ export function renderValueMenu(lines, state, frame, theme) {
24
26
  const active = index === state.index;
25
27
  const paint = active ? theme.highlight : theme.panel;
26
28
  const current = choice === state.current;
27
- const tail = padToWidth(`${current ? " " : " "}${choice} `, frame.width - (current ? 1 : 0));
28
- // Invariant: the effective-value check uses the same accent as modal marks, while
29
- // the rest of an active row keeps its highlighted foreground and background.
29
+ const previews = state.previews ?? [];
30
+ const previewWidth = Math.max(0, ...previews.map(displayWidth));
31
+ const preview = previewWidth === 0 ? "" : `${padToWidth(previews[index] ?? "", previewWidth)} `;
32
+ // Invariant: the current-value check stays on ordinary text while the optional
33
+ // square carries the palette color; the row treatment supplies only its surface.
34
+ const tail = padToWidth(`${current ? " " : " "}${preview}${choice} `, frame.width - (current ? 1 : 0));
30
35
  const painted = current
31
- ? `${paint(theme.fg("accent", "✓"))}${paint(tail)}`
36
+ ? `${paint(theme.fg("text", "✓"))}${paint(tail)}`
32
37
  : paint(tail);
33
38
  output[target] = overlaySpan(output[target] ?? "", frame.column, frame.column + frame.width, painted);
34
39
  });
@@ -1,4 +1,4 @@
1
- export declare const OWNED_UI_SETTINGS_VERSION = 10;
1
+ export declare const OWNED_UI_SETTINGS_VERSION = 13;
2
2
  export type OwnedUiSettingValue = string | number | boolean;
3
3
  export type OwnedUiSettingApplication = "live" | "restart";
4
4
  export interface OwnedUiSettingDeclaration {
@@ -20,18 +20,6 @@ export interface OwnedUiSettingDeclaration {
20
20
  * values are read from it, and `OwnedSettingValueOf` derives each getter's type from it.
21
21
  */
22
22
  export declare const OWNED_SETTING_DECLARATIONS: Readonly<{
23
- readonly quitAnimation: Readonly<{
24
- id: "quitAnimation";
25
- label: "Quit animation";
26
- section: Readonly<{
27
- id: "generic";
28
- title: "Generic";
29
- }>;
30
- description: "Play the fall effect over the last screen when the session quits. Off returns to the terminal immediately.";
31
- application: "live";
32
- defaultValue: true;
33
- allowedValues: readonly [true, false];
34
- }>;
35
23
  readonly updateCheck: Readonly<{
36
24
  id: "updateCheck";
37
25
  label: "Update check";
@@ -44,6 +32,42 @@ export declare const OWNED_SETTING_DECLARATIONS: Readonly<{
44
32
  defaultValue: true;
45
33
  allowedValues: readonly [true, false];
46
34
  }>;
35
+ readonly accentColor: Readonly<{
36
+ id: "accentColor";
37
+ label: "Accent color";
38
+ section: Readonly<{
39
+ id: "appearance";
40
+ title: "Appearance";
41
+ }>;
42
+ description: "Color used by highlighted interface controls, list markers, and tonal interface surfaces.";
43
+ application: "live";
44
+ defaultValue: "purple";
45
+ allowedValues: readonly ["purple", "blue", "cyan", "green", "orange", "pink"];
46
+ }>;
47
+ readonly backgroundStyle: Readonly<{
48
+ id: "backgroundStyle";
49
+ label: "Background";
50
+ section: Readonly<{
51
+ id: "appearance";
52
+ title: "Appearance";
53
+ }>;
54
+ description: "Fullscreen canvas background: terminal transparency, a dark accent tint, or fixed neutral dark.";
55
+ application: "live";
56
+ defaultValue: "transparent";
57
+ allowedValues: readonly ["transparent", "accent", "dark"];
58
+ }>;
59
+ readonly quitAnimation: Readonly<{
60
+ id: "quitAnimation";
61
+ label: "Quit animation";
62
+ section: Readonly<{
63
+ id: "appearance";
64
+ title: "Appearance";
65
+ }>;
66
+ description: "Play the fall effect over the last screen when the session quits. Off returns to the terminal immediately.";
67
+ application: "live";
68
+ defaultValue: true;
69
+ allowedValues: readonly [true, false];
70
+ }>;
47
71
  readonly scrollbarAppearance: Readonly<{
48
72
  id: "scrollbarAppearance";
49
73
  label: "Scrollbar mode";
@@ -111,7 +135,7 @@ export declare const OWNED_SETTING_DECLARATIONS: Readonly<{
111
135
  id: "agent";
112
136
  title: "Agent";
113
137
  }>;
114
- description: "Predict likely next prompts with one additional background request using the selected model.";
138
+ description: "Predict likely next prompts with one background request, plus one bounded recovery request when needed, using the selected model.";
115
139
  application: "live";
116
140
  defaultValue: true;
117
141
  allowedValues: readonly [true, false];
@@ -1,8 +1,12 @@
1
- export const OWNED_UI_SETTINGS_VERSION = 10;
1
+ export const OWNED_UI_SETTINGS_VERSION = 13;
2
+ const UI_ACCENT_COLOR_CHOICES = Object.freeze([
3
+ "purple", "blue", "cyan", "green", "orange", "pink",
4
+ ]);
2
5
  const MAX_ID_LENGTH = 64;
3
6
  const ID_PATTERN = /^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
4
7
  /** Declared first so the settings screen opens on it; sections follow first-declaration order. */
5
8
  const GENERIC_SECTION = Object.freeze({ id: "generic", title: "Generic" });
9
+ const APPEARANCE_SECTION = Object.freeze({ id: "appearance", title: "Appearance" });
6
10
  const SCROLL_SECTION = Object.freeze({ id: "scroll", title: "Scroll" });
7
11
  const AGENT_SECTION = Object.freeze({ id: "agent", title: "Agent" });
8
12
  /**
@@ -10,15 +14,6 @@ const AGENT_SECTION = Object.freeze({ id: "agent", title: "Agent" });
10
14
  * values are read from it, and `OwnedSettingValueOf` derives each getter's type from it.
11
15
  */
12
16
  export const OWNED_SETTING_DECLARATIONS = Object.freeze({
13
- quitAnimation: Object.freeze({
14
- id: "quitAnimation",
15
- label: "Quit animation",
16
- section: GENERIC_SECTION,
17
- description: "Play the fall effect over the last screen when the session quits. Off returns to the terminal immediately.",
18
- application: "live",
19
- defaultValue: true,
20
- allowedValues: Object.freeze([true, false]),
21
- }),
22
17
  updateCheck: Object.freeze({
23
18
  id: "updateCheck",
24
19
  label: "Update check",
@@ -28,6 +23,35 @@ export const OWNED_SETTING_DECLARATIONS = Object.freeze({
28
23
  defaultValue: true,
29
24
  allowedValues: Object.freeze([true, false]),
30
25
  }),
26
+ accentColor: Object.freeze({
27
+ id: "accentColor",
28
+ label: "Accent color",
29
+ section: APPEARANCE_SECTION,
30
+ description: "Color used by highlighted interface controls, list markers, and tonal interface surfaces.",
31
+ application: "live",
32
+ defaultValue: "purple",
33
+ allowedValues: UI_ACCENT_COLOR_CHOICES,
34
+ }),
35
+ backgroundStyle: Object.freeze({
36
+ id: "backgroundStyle",
37
+ label: "Background",
38
+ section: APPEARANCE_SECTION,
39
+ description: "Fullscreen canvas background: terminal transparency, a dark accent tint, or fixed neutral dark.",
40
+ application: "live",
41
+ defaultValue: "transparent",
42
+ allowedValues: Object.freeze([
43
+ "transparent", "accent", "dark",
44
+ ]),
45
+ }),
46
+ quitAnimation: Object.freeze({
47
+ id: "quitAnimation",
48
+ label: "Quit animation",
49
+ section: APPEARANCE_SECTION,
50
+ description: "Play the fall effect over the last screen when the session quits. Off returns to the terminal immediately.",
51
+ application: "live",
52
+ defaultValue: true,
53
+ allowedValues: Object.freeze([true, false]),
54
+ }),
31
55
  scrollbarAppearance: Object.freeze({
32
56
  id: "scrollbarAppearance",
33
57
  label: "Scrollbar mode",
@@ -77,7 +101,7 @@ export const OWNED_SETTING_DECLARATIONS = Object.freeze({
77
101
  id: "promptSuggestions",
78
102
  label: "Prompt suggestions",
79
103
  section: AGENT_SECTION,
80
- description: "Predict likely next prompts with one additional background request using the selected model.",
104
+ description: "Predict likely next prompts with one background request, plus one bounded recovery request when needed, using the selected model.",
81
105
  application: "live",
82
106
  defaultValue: true,
83
107
  allowedValues: Object.freeze([true, false]),
@@ -81,6 +81,29 @@ export const OWNED_UI_SETTINGS_MIGRATIONS = Object.freeze([
81
81
  return { ...values };
82
82
  },
83
83
  }),
84
+ Object.freeze({
85
+ to: 11,
86
+ description: "Introduce semantic accent selection with the active-theme default.",
87
+ migrate(values) {
88
+ return { ...values };
89
+ },
90
+ }),
91
+ Object.freeze({
92
+ to: 12,
93
+ description: "Rename the inherited default accent choice to explicit purple.",
94
+ migrate(values) {
95
+ return values.accentColor === "default"
96
+ ? { ...values, accentColor: "purple" }
97
+ : { ...values };
98
+ },
99
+ }),
100
+ Object.freeze({
101
+ to: 13,
102
+ description: "Introduce the transparent fullscreen background default.",
103
+ migrate(values) {
104
+ return { ...values };
105
+ },
106
+ }),
84
107
  ]);
85
108
  export function assertOwnedUiSettingsMigrations(migrations, currentVersion = OWNED_UI_SETTINGS_VERSION) {
86
109
  const firstProduced = currentVersion - migrations.length + 1;
@@ -30,7 +30,7 @@ The owned Pi-backed surface is not an arbitrary-CLI terminal multiplexer. A feat
30
30
  - `prompt-suggestions`: opt-in bounded diagnostic snapshots of neutral suggestion lifecycle metadata. Composition supplies an explicit local destination; this feature never imports Pi, generates suggestions, stores conversation text, or owns the editor. See [prompt suggestion diagnostics](prompt-suggestions.md).
31
31
  - `storage`: SQLite migrations, prior-boot reconciliation, and plural launch-instance persistence. Legacy foreground rows are historical migration input and never authorize current ownership.
32
32
  - `supervisor`: endpoint identity, plural cohort ownership, per-instance reconciliation, and aggregate release shutdown coordination. It owns no terminal surface.
33
- - `pi-engine-adapter`, `pi-component-adapter`, and `pi-tui-runtime-adapter`: isolate pinned Pi engine and presentation knowledge behind neutral contracts; product features do not import them directly.
33
+ - `pi-engine-adapter`, `pi-component-adapter`, and `pi-tui-runtime-adapter`: isolate pinned Pi engine and presentation knowledge behind neutral contracts; product features do not import them directly. Inside the engine adapter, one process-level engine host owns what is process-wide (the HTTP dispatcher, the theme singletons, the one-time changelog and package-update announcements, the startup trace) and is the only place sessions are created, so every session id is unique per process.
34
34
  - `session-shell` (`src/app/session-shell`): the application layer that composes the feature owners, the UI foundations, and the three Pi adapters into the interactive session; only composition imports it. Its render root assembles semantic document and dock rows, while its focused viewport controller owns follow state, pointer routing, selection, and interaction timers. Owned-app route lifecycle belongs to the neutral `ui-apps` owner and is only hosted by the session shell.
35
35
  - release/update/bootstrap: package-derived immutable release identity, process-guardian integrity, cohort selection, durable update transactions, rollback, and dependency-light command entry. After an unsuccessful bare-A1 child exit, the bootstrap restores owned terminal modes; neither guardian emits terminal controls.
36
36
  - `terminal-cleanup`: bounded, idempotent emergency restoration and fatal-exit diagnostics for an explicitly owned terminal. It does not inspect commands, relay terminal input/output, or emulate terminal state. The owned UI installs its fatal boundary before terminal activation; packaged bootstrap and checkout launch provide the surviving-owner fallback. Fatal records retain at most ten 16 KiB records with trusted classifications and code locations, never prompts, attachments, raw input, credentials, or arbitrary exception messages.
@@ -43,6 +43,14 @@ The JavaScript product path has no PTY or terminal-emulator dependency. Reintrod
43
43
 
44
44
  `scripts/governance/check-architecture.mjs` enforces the structural parts of these boundaries. Cross-cutting rationale belongs here; implementation history belongs in Git and archived OpenSpec changes.
45
45
 
46
+ ## Multi-session limits of the pinned engine
47
+
48
+ The engine host removes A1's own obstacles to several Pi sessions in one process, but three remain in the pinned Pi SDK and must be planned around, not discovered:
49
+
50
+ - The extension cache is keyed by one working directory and cleared on reload (`core/extensions/loader.js`), so sessions in different directories reload each other's extensions.
51
+ - `SettingsManager` writes under a lockfile but never re-reads the file, so a second in-memory copy goes stale after another session writes a setting (`core/settings-manager.js`). The host keeps the one process-wide effect, the HTTP dispatcher, consistent itself.
52
+ - `AgentSessionRuntime.dispose()` releases the session but not the model runtime or the resource loader it was created with (`core/agent-session-runtime.js`), so a session's services outlive it until the process ends.
53
+
46
54
  ## Planned multi-agent boundaries
47
55
 
48
56
  The multi-agent workspace feature, its dependency-free contracts, the structured-agent runtime, and the Node side of the terminal-host protocol were removed from `develop` while the multi-agent plan is on hold; their last state is preserved on the `archive/multi-agent-workspace` branch. A future implementation starts from the current codebase rather than from that copy. The boundary rules below remain the design intent for that work.
@@ -18,13 +18,14 @@ src/
18
18
  prompt-history/ profile-isolated prompt retention and bounded SQLite worker lifecycle
19
19
  prompt-suggestions/ opt-in bounded metadata capture and local diagnostic snapshots
20
20
  contracts/
21
- agent-engine/ dependency-free agent engine, session, package, and capability ports
21
+ agent-engine/ dependency-free agent settings and package contracts
22
22
  owned-ui/ dependency-free owned-session and extension UI contracts
23
23
  presentation/ dependency-free component, terminal, and runtime ports
24
24
  integrations/
25
25
  pi/
26
26
  components/ pinned Pi component and theme adaptation
27
27
  engine/ pinned Pi engine, settings, resource, package, and workflow integration
28
+ session-presenters/ pinned Pi selectors and transcript renderers built from engine objects
28
29
  tui-runtime/ neutral presentation runtime over pinned Pi TUI
29
30
  foundation/
30
31
  launch-guardian/ authenticated launch-instance coordination
@@ -42,7 +43,7 @@ src/
42
43
 
43
44
  Each directory owner exposes `index.ts`, which lists its named exports; `export *` is rejected there so a public contract is readable in one place. Imports within one owner may use private files. Imports crossing owners must use the provider's public entry and follow the dependency DAG declared by `PROJECT_OWNERS`. Two exceptions exist: `src/composition` is the dependency-injection root and may reach past any public entry, and a module on the eager startup path may import a leaf of a provider whose public entry is a prohibited startup entry (`PROHIBITED_STARTUP_ENTRIES` in `startup-graph-policy.mjs`), because loading that barrel would pull the provider's whole graph into startup. `product-identity` is the sole exception to the directory-entry convention because its public entry is `src/product-identity.ts`.
44
45
 
45
- Three layer boundaries hold regardless of the DAG: `src/contracts/*` import nothing outside their own contract; `src/ui/components` imports only contracts; only `src/integrations/pi/*` and the shipped `bin/` entries import the pinned Pi packages. The session shell under `src/app` is the application layer: it may import the contracts, the UI foundations, the three Pi adapters, and the feature owners, and only composition imports it.
46
+ Three layer boundaries hold regardless of the DAG: `src/contracts/*` import nothing outside their own contract; `src/ui/components` imports only contracts; only `src/integrations/pi/*` and the shipped `bin/` entries import the pinned Pi packages. The session shell under `src/app` is the application layer: it may import the contracts, the UI foundations, the three Pi adapters, and the feature owners, and only composition imports it. The shell never holds a Pi engine object: selectors and transcript renderers built from one live in `src/integrations/pi/session-presenters`, the only owner besides composition that may import both the engine and the component adapters, and the shell opens them through the `OwnedUiSessionPresenters` contract.
46
47
 
47
48
  `src/cli` contains command policy but delegates runtime work. `src/composition` is the concrete dependency-injection boundary: it may know both neutral contracts and Pi implementations, while product features receive vendor-neutral ports. Foundation modules never import product features. Pi package knowledge remains inside the Pi adapter owners.
48
49
 
@@ -92,18 +93,19 @@ Build output mirrors the production namespaces directly under ignored `dist/`, w
92
93
 
93
94
  Every task worktree must be created at `{working-dir}/.worktrees/<task-id>`, where `{working-dir}` is the agent session's initial working directory. The `.worktrees` directory is inside that working directory, not beside it. For working directory `D:/Git/a1`, `D:/Git/a1/.worktrees/<task-id>` is correct and `D:/Git/a1-<task-id>` is forbidden.
94
95
 
95
- From the initial working directory, create the task branch and worktree, run `a1 session link-worktree <absolute-worktree>` from the owning A1 session, and continue to address it explicitly:
96
+ From the initial working directory, run `a1 session worktrees` to inspect cooperative live ownership before selecting an existing checkout. A `busy` or `unverifiable` worktree is unavailable. An `available` checkout may be reused only when its exact branch, OpenSpec change, and pull request match the requested stream and atomic `a1 session link-worktree <absolute-worktree>` succeeds; names, similar files, recency, cleanliness, and ancestry are not ownership evidence. Otherwise create a fresh task branch/worktree, link it from the owning A1 session, and continue to address it explicitly:
96
97
 
97
98
  ```sh
99
+ a1 session worktrees
98
100
  git fetch origin develop
99
101
  git worktree add -b <type>/<short-description> .worktrees/<task-id> origin/develop
100
102
  a1 session link-worktree <absolute-worktree>
101
103
  git -C .worktrees/<task-id> status
102
104
  ```
103
105
 
104
- A successful link response confirming the exact canonical worktree is required before planning, implementation, test, or delivery-documentation edits. If linking fails or confirms another path, stop task edits and report the blocker instead of continuing with primary-checkout footer metadata. A session resuming an existing delivery or switching streams links the exact owned worktree before editing it.
106
+ Inventory is read-only and advisory because another runtime may win a later race. A successful link response confirming the exact canonical worktree is required before planning, implementation, test, or delivery-documentation edits. If inventory or linking fails, reports a live/unverifiable owner, loses a race, or confirms another path, stop task edits and report the blocker; use a separate fresh worktree rather than overriding, unlinking, or recovering that owner. A session resuming an existing delivery or switching streams atomically claims and links the exact worktree before editing it.
105
107
 
106
- The association switches bare A1's footer repository context and pull-request discovery; it does not change process or tool cwd and does not register, claim, release, or authorize cleanup of the worktree. Repository commands therefore keep an explicit worktree path. The primary worktree stays on `develop` for integration and must not be used for task edits. Do not edit, adopt, move, or remove another session's worktree.
108
+ The link acquires a cooperative live-session editing claim and switches bare A1's footer repository context and pull-request discovery. It does not change process or tool cwd, create a Git worktree lock, or register, release, or authorize cleanup. Clean unlink/disposal releases live editing authority; a crashed owner becomes reusable only after exact process death is verified. Repository commands therefore keep an explicit worktree path. The primary worktree stays on `develop` for integration and must not be used for task edits. Do not edit, adopt, move, or remove another session's worktree.
107
109
 
108
110
  An agent that directly chooses a transient file path for a PR/comment body, command payload, captured output, temporary patch/diff, or ad hoc log must place it beneath the linked worktree's exact `.artifacts/` root, never the primary checkout, another worktree, OS temp, home/desktop, or a sibling path. Such scratch remains ignored, unstaged, uncommitted, disposable, non-authoritative, and secret-free. This repository path rule does not relocate storage internally selected by tools, product runtime code, or hermetic tests.
109
111