@timurproko/a1 0.1.8-dev.182 → 0.1.8-dev.214

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 (142) hide show
  1. package/bin/cli.js +2 -2
  2. package/bin/sync-pi-tui-proxy.js +1 -1
  3. package/bin/ui.js +1 -1
  4. package/dist/cli/dispatch.js +3 -3
  5. package/dist/cli/packages.js +1 -1
  6. package/dist/cli/version-stats.js +3 -3
  7. package/dist/composition/owned-ui.js +1 -1
  8. package/dist/composition/settings-route-host.js +6 -4
  9. package/dist/features/launch/development-launch.js +1 -1
  10. package/dist/features/owned-ui/customization.d.ts +2 -0
  11. package/dist/features/owned-ui/customization.js +2 -0
  12. package/dist/features/owned-ui/diagnostics.d.ts +1 -0
  13. package/dist/features/owned-ui/diagnostics.js +1 -0
  14. package/dist/features/owned-ui/project-trust-prompt.d.ts +11 -6
  15. package/dist/features/owned-ui/project-trust-prompt.js +134 -17
  16. package/dist/features/owned-ui/settings-app.js +47 -50
  17. package/dist/features/workspace/reducer.d.ts +1 -0
  18. package/dist/features/workspace/reducer.js +1 -0
  19. package/dist/features/workspace/router.d.ts +1 -0
  20. package/dist/features/workspace/router.js +1 -0
  21. package/dist/features/workspace/store.d.ts +1 -0
  22. package/dist/features/workspace/store.js +1 -0
  23. package/dist/foundation/launch-guardian/main.js +1 -1
  24. package/dist/foundation/native-host-protocol/codec.d.ts +2 -0
  25. package/dist/foundation/native-host-protocol/codec.js +2 -0
  26. package/dist/foundation/process-containment/linux-process-inspector.d.ts +1 -0
  27. package/dist/foundation/process-containment/linux-process-inspector.js +1 -0
  28. package/dist/foundation/process-containment/native-guardian-containment.d.ts +1 -0
  29. package/dist/foundation/process-containment/native-guardian-containment.js +1 -0
  30. package/dist/foundation/process-containment/windows-process-inspector.d.ts +1 -0
  31. package/dist/foundation/process-containment/windows-process-inspector.js +1 -0
  32. package/dist/foundation/protocol/client.d.ts +1 -0
  33. package/dist/foundation/protocol/client.js +1 -0
  34. package/dist/foundation/protocol/messages.d.ts +1 -0
  35. package/dist/foundation/protocol/messages.js +1 -0
  36. package/dist/foundation/release/bootstrap.js +6 -6
  37. package/dist/foundation/release/cohort-state.d.ts +1 -0
  38. package/dist/foundation/release/cohort-state.js +1 -0
  39. package/dist/foundation/release/release-gc.js +1 -1
  40. package/dist/foundation/release/release.js +1 -1
  41. package/dist/foundation/release/update-transaction.d.ts +1 -0
  42. package/dist/foundation/release/update-transaction.js +1 -0
  43. package/dist/foundation/release/update.js +12 -12
  44. package/dist/foundation/storage/control-store.d.ts +1 -0
  45. package/dist/foundation/storage/control-store.js +1 -0
  46. package/dist/foundation/structured-agent-runtime/backpressure.d.ts +1 -0
  47. package/dist/foundation/structured-agent-runtime/backpressure.js +1 -0
  48. package/dist/foundation/structured-agent-runtime/commands.d.ts +1 -0
  49. package/dist/foundation/structured-agent-runtime/commands.js +1 -0
  50. package/dist/foundation/structured-agent-runtime/reconnection.d.ts +1 -0
  51. package/dist/foundation/structured-agent-runtime/reconnection.js +1 -0
  52. package/dist/foundation/structured-agent-runtime/state.d.ts +1 -0
  53. package/dist/foundation/structured-agent-runtime/state.js +1 -0
  54. package/dist/foundation/supervision/main.js +1 -1
  55. package/dist/foundation/supervision/server.d.ts +1 -0
  56. package/dist/foundation/supervision/server.js +4 -6
  57. package/dist/integrations/pi/components/conformance.d.ts +1 -0
  58. package/dist/integrations/pi/components/conformance.js +1 -0
  59. package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
  60. package/dist/integrations/pi/components/shell-footer-status.js +8 -7
  61. package/dist/integrations/pi/components/shell-shared-facade.js +1 -1
  62. package/dist/integrations/pi/engine/adapter.d.ts +7 -0
  63. package/dist/integrations/pi/engine/adapter.js +37 -35
  64. package/dist/integrations/pi/engine/conformance.d.ts +2 -0
  65. package/dist/integrations/pi/engine/conformance.js +2 -0
  66. package/dist/integrations/pi/engine/http-dispatcher.js +1 -1
  67. package/dist/integrations/pi/engine/model-auth-integration.d.ts +1 -0
  68. package/dist/integrations/pi/engine/model-auth-integration.js +1 -0
  69. package/dist/integrations/pi/engine/package-integration.js +1 -1
  70. package/dist/integrations/pi/engine/project-trust-preflight.js +1 -1
  71. package/dist/integrations/pi/engine/resource-extension-integration.d.ts +1 -0
  72. package/dist/integrations/pi/engine/resource-extension-integration.js +1 -0
  73. package/dist/integrations/pi/engine/session-integration.d.ts +1 -0
  74. package/dist/integrations/pi/engine/session-integration.js +2 -1
  75. package/dist/integrations/pi/engine/settings-effects.d.ts +13 -2
  76. package/dist/integrations/pi/engine/settings-effects.js +62 -42
  77. package/dist/integrations/pi/engine/settings-integration.js +4 -4
  78. package/dist/integrations/pi/engine/workflow-controllers.d.ts +1 -0
  79. package/dist/integrations/pi/engine/workflow-controllers.js +1 -0
  80. package/dist/integrations/pi/session-ui/index.d.ts +1 -0
  81. package/dist/integrations/pi/session-ui/index.js +1 -0
  82. package/dist/integrations/pi/session-ui/prompt-chips.js +1 -1
  83. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +30 -2
  84. package/dist/integrations/pi/session-ui/session-shell-root.js +133 -72
  85. package/dist/integrations/pi/session-ui/session-shell.d.ts +9 -2
  86. package/dist/integrations/pi/session-ui/session-shell.js +112 -37
  87. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +5 -0
  88. package/dist/integrations/pi/session-ui/session-viewport-controller.js +44 -11
  89. package/dist/integrations/pi/session-ui/stream-presentation-coalescer.d.ts +29 -0
  90. package/dist/integrations/pi/session-ui/stream-presentation-coalescer.js +79 -0
  91. package/dist/integrations/pi/session-ui/system-clipboard.js +6 -6
  92. package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -0
  93. package/dist/integrations/pi/tui-runtime/adapter.js +138 -8
  94. package/dist/integrations/pi/tui-runtime/contracts.d.ts +25 -0
  95. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +63 -0
  96. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +240 -0
  97. package/dist/integrations/pi/tui-runtime/index.d.ts +2 -0
  98. package/dist/integrations/pi/tui-runtime/index.js +2 -0
  99. package/dist/integrations/pi/tui-runtime/input-presentation-coordinator.d.ts +39 -0
  100. package/dist/integrations/pi/tui-runtime/input-presentation-coordinator.js +139 -0
  101. package/dist/native/darwin-arm64/manifest.json +1 -1
  102. package/dist/native/linux-x64/manifest.json +1 -1
  103. package/dist/native/win32-x64/manifest.json +2 -2
  104. package/dist/native/win32-x64/process-guardian.exe +0 -0
  105. package/dist/ui/apps/host.js +5 -9
  106. package/dist/ui/apps/registry.d.ts +1 -0
  107. package/dist/ui/apps/registry.js +1 -0
  108. package/dist/ui/components/dialog-panel.js +5 -3
  109. package/dist/ui/components/frame.d.ts +1 -0
  110. package/dist/ui/components/frame.js +1 -0
  111. package/dist/ui/components/index.d.ts +1 -0
  112. package/dist/ui/components/index.js +1 -0
  113. package/dist/ui/components/line-input.d.ts +1 -6
  114. package/dist/ui/components/line-input.js +4 -9
  115. package/dist/ui/components/list-view.d.ts +0 -6
  116. package/dist/ui/components/list-view.js +14 -7
  117. package/dist/ui/components/progress-status.d.ts +2 -0
  118. package/dist/ui/components/progress-status.js +4 -0
  119. package/dist/ui/components/scrollbar.js +1 -1
  120. package/dist/ui/components/shortcuts.d.ts +1 -0
  121. package/dist/ui/components/shortcuts.js +1 -0
  122. package/dist/ui/components/spans.d.ts +0 -5
  123. package/dist/ui/components/spans.js +3 -8
  124. package/dist/ui/components/surface.d.ts +0 -5
  125. package/dist/ui/components/surface.js +1 -5
  126. package/dist/ui/components/text-selection.d.ts +10 -1
  127. package/dist/ui/components/text-selection.js +96 -29
  128. package/dist/ui/components/text.js +3 -3
  129. package/dist/ui/components/transcript-viewport.d.ts +42 -0
  130. package/dist/ui/components/transcript-viewport.js +287 -39
  131. package/dist/ui/settings/sections.d.ts +0 -2
  132. package/dist/ui/settings/sections.js +10 -16
  133. package/dist/ui/settings/session.js +5 -4
  134. package/dist/ui/settings/store.d.ts +1 -0
  135. package/dist/ui/settings/store.js +2 -1
  136. package/docs/architecture/code-documentation.md +77 -0
  137. package/docs/architecture/project-structure.md +1 -1
  138. package/docs/architecture/ui-reference-provenance.md +23 -3
  139. package/docs/ci-release-runbook.md +30 -4
  140. package/docs/manual-owned-ui-checkpoint.md +28 -1
  141. package/docs/manual-terminal-colour-check.md +4 -3
  142. package/package.json +7 -2
@@ -1,8 +1,9 @@
1
1
  import { isThumbRow, scrollbarGeometry, scrollbarPresentation, } from "./scrollbar.js";
2
2
  import { backgroundSgrSpan, overlaySpan } from "./spans.js";
3
3
  import { faint, truncateToWidth, displayWidth, stripAnsi } from "./text.js";
4
- import { extendTextSelection, orderedTextSelection, pressTextSelection, releaseTextSelection, textSelectionLineExtendColumn, textSelectionText, usefulTextLineContent, } from "./text-selection.js";
4
+ import { extendTextSelection, orderedTextSelection, pressTextSelection, releaseTextSelection, textSelectionLineExtendColumn, textSelectionPointAt, textSelectionText, usefulTextLineContent, } from "./text-selection.js";
5
5
  const CONTROL_STYLE_RESET = "\u001b]8;;\u001b\\\u001b[0m";
6
+ const IDENTITY_ROW = (row) => row;
6
7
  const DEFAULT_CONFIG = {
7
8
  scrollbarAppearance: "auto",
8
9
  scrollbarStyle: "thin",
@@ -32,16 +33,26 @@ export class TranscriptViewport {
32
33
  #bottomHovered = false;
33
34
  #selection;
34
35
  #selectionClick;
36
+ #selectionRevision = 0;
37
+ #paintedRowCache = new Map();
38
+ #baseRowCache = new Map();
39
+ #selectedRowCache = new Map();
40
+ #finalRowCache = new Map();
41
+ #functionIds = new WeakMap();
42
+ #nextFunctionId = 1;
43
+ #previousVisibleStates = [];
35
44
  #documentRows = [];
36
45
  #selectableDocumentRowCount = 0;
37
46
  #promptAnchors = [];
38
47
  #contentWidth = 0;
48
+ #frameId = 0;
39
49
  #frame = null;
40
50
  get scrollTop() { return this.#scrollTop; }
41
51
  get maxScroll() { return this.#maxScroll; }
42
52
  get followingEnd() { return this.#followingEnd; }
43
53
  get newMessages() { return this.#newMessages; }
44
54
  get frame() { return this.#frame; }
55
+ get selectionRevision() { return this.#selectionRevision; }
45
56
  /** Counts one completed assistant reply while detached; tool rows never call this. */
46
57
  noteNewMessage() {
47
58
  if (this.#followingEnd)
@@ -83,6 +94,7 @@ export class TranscriptViewport {
83
94
  });
84
95
  this.#selection = pressed.selection;
85
96
  this.#selectionClick = pressed.click;
97
+ this.#selectionRevision += 1;
86
98
  return true;
87
99
  }
88
100
  extendSelection(column, viewportRow, now = Date.now(), autoScroll = true) {
@@ -90,7 +102,7 @@ export class TranscriptViewport {
90
102
  const viewportHeight = this.#frame?.hits.viewportHeight ?? 0;
91
103
  if (selection?.selecting !== true || viewportHeight <= 0 || this.#documentRows.length === 0)
92
104
  return false;
93
- // Pointer motion updates only the endpoint; the shell's fixed-cadence timer
105
+ // Performance: pointer motion updates only the endpoint; the shell's fixed-cadence timer
94
106
  // owns edge scrolling. This option prevents high-rate motion reports from
95
107
  // adding irregular extra rows between timer ticks.
96
108
  if (autoScroll && viewportRow > viewportHeight)
@@ -105,19 +117,28 @@ export class TranscriptViewport {
105
117
  const targetColumn = selection.fullRow
106
118
  ? textSelectionLineExtendColumn(selection, line, this.#contentWidth)
107
119
  : clamp(column, 1, this.#contentWidth);
108
- this.#selection = extendTextSelection(selection, { line, column: targetColumn });
120
+ const point = selection.fullRow
121
+ ? { line, column: targetColumn }
122
+ : textSelectionPointAt(line, targetColumn, this.#documentRows[line] ?? "");
123
+ const next = extendTextSelection(selection, point);
124
+ if (next !== selection) {
125
+ this.#selection = next;
126
+ this.#selectionRevision += 1;
127
+ }
109
128
  return true;
110
129
  }
111
130
  releaseSelection() {
112
131
  if (this.#selection?.selecting !== true)
113
132
  return false;
114
133
  this.#selection = releaseTextSelection(this.#selection);
134
+ this.#selectionRevision += 1;
115
135
  return true;
116
136
  }
117
137
  clearSelection() {
118
138
  if (this.#selection === undefined)
119
139
  return false;
120
140
  this.#selection = undefined;
141
+ this.#selectionRevision += 1;
121
142
  return true;
122
143
  }
123
144
  selectedText() {
@@ -142,7 +163,7 @@ export class TranscriptViewport {
142
163
  const next = clamp(position, 0, this.#maxScroll);
143
164
  const changed = next !== this.#scrollTop || this.#followingEnd;
144
165
  this.#scrollTop = next;
145
- // Every scrolling path that reaches the final legal top row resumes follow.
166
+ // Invariant: every scrolling path that reaches the final legal top row resumes follow.
146
167
  this.#followingEnd = next >= this.#maxScroll;
147
168
  if (this.#followingEnd)
148
169
  this.#newMessages = 0;
@@ -166,7 +187,7 @@ export class TranscriptViewport {
166
187
  if (anchor.firstRow < this.#scrollTop && anchor.firstRow > target)
167
188
  target = anchor.firstRow;
168
189
  }
169
- // The first prompt owns the document's opening breathing row. At that final
190
+ // Invariant: the first prompt owns the document's opening breathing row. At that final
170
191
  // navigation stop, reveal the spacer too rather than pinning the prompt to
171
192
  // terminal row one as later prompt jumps do.
172
193
  const destination = target === earliest ? 0 : target;
@@ -177,7 +198,7 @@ export class TranscriptViewport {
177
198
  if (this.#promptAnchors.length === 0)
178
199
  return false;
179
200
  const earliest = Math.min(...this.#promptAnchors.map(anchor => anchor.firstRow));
180
- // Scroll position zero is the first-prompt stop because it includes that
201
+ // Invariant: scroll position zero is the first-prompt stop because it includes that
181
202
  // prompt's opening spacer. Do not spend an extra keypress moving one row.
182
203
  const after = this.#scrollTop === 0 ? earliest : this.#scrollTop;
183
204
  let target = Number.POSITIVE_INFINITY;
@@ -192,8 +213,15 @@ export class TranscriptViewport {
192
213
  this.#maxScroll = 0;
193
214
  this.#followingEnd = true;
194
215
  this.#newMessages = 0;
216
+ if (this.#selection !== undefined)
217
+ this.#selectionRevision += 1;
195
218
  this.#selection = undefined;
196
219
  this.#selectionClick = undefined;
220
+ this.#paintedRowCache.clear();
221
+ this.#baseRowCache.clear();
222
+ this.#selectedRowCache.clear();
223
+ this.#finalRowCache.clear();
224
+ this.#previousVisibleStates = [];
197
225
  this.#documentRows = [];
198
226
  this.#selectableDocumentRowCount = 0;
199
227
  this.#promptAnchors = [];
@@ -212,6 +240,7 @@ export class TranscriptViewport {
212
240
  const width = Math.max(1, input.width);
213
241
  const height = Math.max(0, input.height);
214
242
  const theme = input.theme ?? PLAIN_THEME;
243
+ const previousDescriptor = this.#frame?.descriptor ?? null;
215
244
  const dock = input.dockRows.length > height ? input.dockRows.slice(-height) : [...input.dockRows];
216
245
  const viewportHeight = Math.max(0, height - dock.length);
217
246
  this.#maxScroll = Math.max(0, input.documentRows.length - viewportHeight);
@@ -227,7 +256,7 @@ export class TranscriptViewport {
227
256
  contentLength: input.documentRows.length,
228
257
  viewportHeight,
229
258
  scroll: this.#scrollTop,
230
- // The session rail deliberately starts one line below the viewport top.
259
+ // Compatibility: the session rail deliberately starts one line below the viewport top.
231
260
  trackHeight: Math.max(0, viewportHeight - 1),
232
261
  });
233
262
  const presentation = scrollbarPresentation({
@@ -244,62 +273,82 @@ export class TranscriptViewport {
244
273
  this.#selectableDocumentRowCount = clamp(input.selectableDocumentRowCount ?? input.documentRows.length, 0, input.documentRows.length);
245
274
  this.#promptAnchors = input.promptAnchors;
246
275
  this.#contentWidth = contentWidth;
247
- const paintDocumentRow = input.paintDocumentRow ?? (row => row);
276
+ const paintDocumentRow = input.paintDocumentRow ?? IDENTITY_ROW;
277
+ const cacheLimit = Math.max(32, viewportHeight * 6);
278
+ trimCache(this.#paintedRowCache, cacheLimit);
279
+ trimCache(this.#baseRowCache, cacheLimit);
280
+ trimCache(this.#selectedRowCache, cacheLimit);
281
+ trimCache(this.#finalRowCache, cacheLimit);
282
+ const paintId = this.#functionId(paintDocumentRow);
283
+ const paintRecomputedRows = new Set();
248
284
  const visible = input.documentRows
249
285
  .slice(this.#scrollTop, this.#scrollTop + viewportHeight)
250
- .map(paintDocumentRow);
286
+ .map((row, index) => {
287
+ const painted = cachedString(this.#paintedRowCache, `${paintId}\u0000${row}`, cacheLimit, () => paintDocumentRow(row));
288
+ if (!painted.reused)
289
+ paintRecomputedRows.add(index);
290
+ return painted.value;
291
+ });
251
292
  while (visible.length < viewportHeight)
252
293
  visible.push("");
253
294
  const governing = governingPrompt(input.promptAnchors, this.#scrollTop);
254
295
  const stickyActive = governing !== null && governing.firstRow < this.#scrollTop;
255
296
  if (stickyActive && visible.length > 0) {
256
297
  const quiet = this.#scrollTop > governing.lastRow;
257
- // Sticky prompts use the same normal/hover surface roles as the bottom
298
+ // Compatibility: sticky prompts use the same normal/hover surface roles as the bottom
258
299
  // control. Hover always starts from the full source row, so a quiet prompt
259
300
  // becomes prominent again with its timestamp intact.
260
- const sticky = theme.sticky(paintDocumentRow(governing.sourceRow), this.#stickyHovered);
301
+ const source = cachedString(this.#paintedRowCache, `${paintId}\u0000${governing.sourceRow}`, cacheLimit, () => paintDocumentRow(governing.sourceRow));
302
+ if (!source.reused)
303
+ paintRecomputedRows.add(0);
304
+ const sticky = theme.sticky(source.value, this.#stickyHovered);
261
305
  visible[0] = quiet && !this.#stickyHovered ? theme.quietSticky(sticky) : sticky;
262
306
  }
307
+ const selectionPainterId = this.#functionId(theme.selection);
308
+ // Concurrency: a painter may synchronously route input in tests or host integrations.
309
+ // Label this frame with the exact selection snapshot it began composing.
310
+ const composingSelectionRevision = this.#selectionRevision;
263
311
  const orderedSelection = orderedTextSelection(this.#selection);
312
+ const recomputedRows = [];
313
+ const reusedRows = [];
314
+ const selectionDamagedRows = [];
315
+ const visibleStates = [];
264
316
  for (let row = 0; row < visible.length; row += 1) {
265
- let line = padRowPreservingBackground(visible[row] ?? "", width);
266
- if (orderedSelection !== undefined && !(stickyActive && row === 0)) {
267
- const documentLine = this.#scrollTop + row;
268
- if (documentLine < this.#selectableDocumentRowCount
269
- && documentLine >= orderedSelection.start.line
270
- && documentLine <= orderedSelection.end.line) {
271
- const fullVisualRow = orderedSelection.fullRow === true
272
- || (orderedSelection.start.line !== orderedSelection.end.line
273
- && documentLine !== orderedSelection.start.line
274
- && documentLine !== orderedSelection.end.line);
275
- const from = fullVisualRow || documentLine !== orderedSelection.start.line
276
- ? 0
277
- : orderedSelection.start.column - 1;
278
- const throughFinalColumn = documentLine === orderedSelection.end.line
279
- && orderedSelection.throughFinalColumn === true;
280
- const to = fullVisualRow || documentLine !== orderedSelection.end.line || throughFinalColumn
281
- ? width
282
- : orderedSelection.end.column;
283
- if (to > from)
284
- line = theme.selection(line, from, Math.min(width, to));
285
- }
286
- }
287
- // Paint the rail after selection. Full-row selection reaches the terminal
317
+ const documentLine = this.#scrollTop + row;
318
+ const painted = visible[row] ?? "";
319
+ const base = cachedString(this.#baseRowCache, `${width}\u0000${painted}`, cacheLimit, () => padRowPreservingBackground(painted, width));
320
+ let rowRecomputed = paintRecomputedRows.has(row) || !base.reused;
321
+ const range = stickyActive && row === 0
322
+ ? null
323
+ : selectionRangeForLine(orderedSelection, documentLine, this.#selectableDocumentRowCount, width);
324
+ const rangeKey = range === null ? "-" : `${range.from}:${range.to}`;
325
+ const selected = range === null
326
+ ? { value: base.value, reused: true }
327
+ : cachedString(this.#selectedRowCache, `${selectionPainterId}\u0000${rangeKey}\u0000${base.value}`, cacheLimit, () => theme.selection(base.value, range.from, range.to));
328
+ rowRecomputed ||= !selected.reused;
329
+ // Invariant: paint the rail after selection. Full-row selection reaches the terminal
288
330
  // edge, while the foreground thumb/track remains visible above that
289
331
  // background instead of disappearing into it.
290
- // Row zero is the intentional one-line breathing room above the rail.
332
+ let railCell = "";
291
333
  if (presentation.visible && geometry !== null && row > 0) {
292
334
  const trackRow = row - 1;
293
335
  const thumb = isThumbRow(geometry, trackRow);
294
336
  const glyph = thumb ? presentation.thumbGlyph : presentation.trackGlyph;
295
- const cell = thumb ? theme.thumb(glyph, this.#railHovered || this.#railDragging) : theme.track(glyph);
296
- line = overlaySpan(line, width - 1, width, cell);
337
+ railCell = thumb ? theme.thumb(glyph, this.#railHovered || this.#railDragging) : theme.track(glyph);
297
338
  }
298
- visible[row] = line;
339
+ const final = cachedString(this.#finalRowCache, `${width}\u0000${railCell}\u0000${selected.value}`, cacheLimit, () => railCell.length === 0 ? selected.value : overlaySpan(selected.value, width - 1, width, railCell));
340
+ rowRecomputed ||= !final.reused;
341
+ visible[row] = final.value;
342
+ (rowRecomputed ? recomputedRows : reusedRows).push(row + 1);
343
+ const state = `${width}\u0000${documentLine}\u0000${painted}\u0000${rangeKey}\u0000${range === null ? "" : selectionPainterId}\u0000${railCell}`;
344
+ visibleStates.push(state);
345
+ if (this.#previousVisibleStates[row] !== state)
346
+ selectionDamagedRows.push(row + 1);
299
347
  }
348
+ this.#previousVisibleStates = visibleStates;
300
349
  const frameRows = [...visible, ...dock].slice(0, height);
301
350
  let bottomHit = null;
302
- if (geometry !== null && !this.#followingEnd && frameRows.length > 0) {
351
+ if (this.#maxScroll > 0 && !this.#followingEnd && frameRows.length > 0) {
303
352
  const genericLabel = " Jump to bottom (End) ";
304
353
  const countedLabel = this.#newMessages > 0
305
354
  ? ` ${this.#newMessages} new message${this.#newMessages === 1 ? "" : "s"} (End) `
@@ -314,6 +363,48 @@ export class TranscriptViewport {
314
363
  bottomHit = { row: row + 1, columnStart: left + 1, columnEnd: left + labelWidth };
315
364
  }
316
365
  }
366
+ const nextDocumentRange = {
367
+ start: this.#scrollTop,
368
+ end: Math.min(input.documentRows.length, this.#scrollTop + viewportHeight),
369
+ };
370
+ const previousDocumentRange = previousDescriptor?.nextDocumentRange ?? null;
371
+ const sameGeometry = previousDescriptor !== null
372
+ && previousDescriptor.width === width
373
+ && previousDescriptor.height === height
374
+ && sameRectangle(previousDescriptor.transcript, viewportHeight === 0 ? null : { rowStart: 1, rowEnd: viewportHeight })
375
+ && sameRectangle(previousDescriptor.dock, dock.length === 0 ? null : { rowStart: viewportHeight + 1, rowEnd: height });
376
+ const verticalShiftRows = previousDocumentRange === null ? 0 : nextDocumentRange.start - previousDocumentRange.start;
377
+ const safeVerticalShift = sameGeometry
378
+ && previousDescriptor?.followingEnd === true
379
+ && this.#followingEnd
380
+ && this.#selection === undefined
381
+ && verticalShiftRows > 0
382
+ && verticalShiftRows < viewportHeight;
383
+ const descriptor = {
384
+ frameId: ++this.#frameId,
385
+ width,
386
+ height,
387
+ transcript: viewportHeight === 0 ? null : { rowStart: 1, rowEnd: viewportHeight },
388
+ dock: dock.length === 0 ? null : { rowStart: viewportHeight + 1, rowEnd: height },
389
+ previousDocumentRange,
390
+ nextDocumentRange,
391
+ previousFollowingEnd: previousDescriptor?.followingEnd ?? null,
392
+ followingEnd: this.#followingEnd,
393
+ verticalShiftRows,
394
+ safeVerticalShift,
395
+ selectionRevision: composingSelectionRevision,
396
+ selectionDamagedRows,
397
+ cause: previousDescriptor === null
398
+ ? "initial"
399
+ : !sameGeometry
400
+ ? "geometry-change"
401
+ : !this.#followingEnd
402
+ ? "detached"
403
+ : safeVerticalShift
404
+ ? "follow-shift"
405
+ : "steady",
406
+ };
407
+ assertTranscriptViewportFrameDescriptor(descriptor);
317
408
  const hits = {
318
409
  viewportHeight,
319
410
  rail: geometry === null || this.#config.scrollbarAppearance === "hidden" ? null : {
@@ -332,10 +423,77 @@ export class TranscriptViewport {
332
423
  maxScroll: this.#maxScroll,
333
424
  followingEnd: this.#followingEnd,
334
425
  hits,
426
+ descriptor,
427
+ selectionDamage: {
428
+ revision: composingSelectionRevision,
429
+ recomputedRows,
430
+ reusedRows,
431
+ cacheEntries: this.#paintedRowCache.size + this.#baseRowCache.size
432
+ + this.#selectedRowCache.size + this.#finalRowCache.size,
433
+ },
335
434
  };
336
435
  this.#frame = frame;
337
436
  return frame;
338
437
  }
438
+ /** Reuses an established transcript frame when only same-height dock rows changed. */
439
+ composeDockOnly(dockRows, width, height) {
440
+ const previous = this.#frame;
441
+ const boundedWidth = Math.max(1, width);
442
+ const boundedHeight = Math.max(0, height);
443
+ if (previous === null || previous.descriptor.width !== boundedWidth || previous.descriptor.height !== boundedHeight
444
+ || previous.descriptor.selectionRevision !== this.#selectionRevision)
445
+ return null;
446
+ const dock = dockRows.length > boundedHeight ? dockRows.slice(-boundedHeight) : [...dockRows];
447
+ const viewportHeight = Math.max(0, boundedHeight - dock.length);
448
+ const expectedDock = dock.length === 0 ? null : { rowStart: viewportHeight + 1, rowEnd: boundedHeight };
449
+ const expectedTranscript = viewportHeight === 0 ? null : { rowStart: 1, rowEnd: viewportHeight };
450
+ if (!sameRectangle(previous.descriptor.dock, expectedDock)
451
+ || !sameRectangle(previous.descriptor.transcript, expectedTranscript))
452
+ return null;
453
+ const descriptor = {
454
+ frameId: ++this.#frameId,
455
+ width: boundedWidth,
456
+ height: boundedHeight,
457
+ transcript: expectedTranscript,
458
+ dock: expectedDock,
459
+ previousDocumentRange: previous.descriptor.nextDocumentRange,
460
+ nextDocumentRange: previous.descriptor.nextDocumentRange,
461
+ previousFollowingEnd: previous.followingEnd,
462
+ followingEnd: previous.followingEnd,
463
+ verticalShiftRows: 0,
464
+ safeVerticalShift: false,
465
+ selectionRevision: this.#selectionRevision,
466
+ selectionDamagedRows: [],
467
+ cause: "dock-input",
468
+ };
469
+ assertTranscriptViewportFrameDescriptor(descriptor);
470
+ const frame = {
471
+ rows: [...previous.rows.slice(0, viewportHeight), ...dock].slice(0, boundedHeight),
472
+ contentWidth: previous.contentWidth,
473
+ scrollTop: previous.scrollTop,
474
+ maxScroll: previous.maxScroll,
475
+ followingEnd: previous.followingEnd,
476
+ hits: previous.hits,
477
+ descriptor,
478
+ selectionDamage: {
479
+ revision: this.#selectionRevision,
480
+ recomputedRows: [],
481
+ reusedRows: Array.from({ length: viewportHeight }, (_, index) => index + 1),
482
+ cacheEntries: previous.selectionDamage.cacheEntries,
483
+ },
484
+ };
485
+ this.#frame = frame;
486
+ return frame;
487
+ }
488
+ #functionId(value) {
489
+ const existing = this.#functionIds.get(value);
490
+ if (existing !== undefined)
491
+ return existing;
492
+ const id = this.#nextFunctionId;
493
+ this.#nextFunctionId += 1;
494
+ this.#functionIds.set(value, id);
495
+ return id;
496
+ }
339
497
  #lineContentAt(line) {
340
498
  const plain = stripAnsi(this.#documentRows[line] ?? "");
341
499
  const prompt = this.#promptAnchors.find(anchor => line >= anchor.firstRow && line <= anchor.lastRow);
@@ -354,6 +512,42 @@ export class TranscriptViewport {
354
512
  return content;
355
513
  }
356
514
  }
515
+ function selectionRangeForLine(selection, documentLine, selectableRowCount, width) {
516
+ if (selection === undefined || documentLine >= selectableRowCount
517
+ || documentLine < selection.start.line || documentLine > selection.end.line)
518
+ return null;
519
+ const fullVisualRow = selection.fullRow === true
520
+ || (selection.start.line !== selection.end.line
521
+ && documentLine !== selection.start.line
522
+ && documentLine !== selection.end.line);
523
+ const from = fullVisualRow || documentLine !== selection.start.line
524
+ ? 0
525
+ : selection.start.column;
526
+ const throughFinalColumn = documentLine === selection.end.line && selection.throughFinalColumn === true;
527
+ const to = fullVisualRow || documentLine !== selection.end.line || throughFinalColumn
528
+ ? width
529
+ : selection.end.column;
530
+ const boundedFrom = clamp(from, 0, width);
531
+ const boundedTo = clamp(to, boundedFrom, width);
532
+ return boundedTo > boundedFrom ? { from: boundedFrom, to: boundedTo } : null;
533
+ }
534
+ function cachedString(cache, key, limit, create) {
535
+ const existing = cache.get(key);
536
+ if (existing !== undefined) {
537
+ cache.delete(key);
538
+ cache.set(key, existing);
539
+ return { value: existing, reused: true };
540
+ }
541
+ const value = create();
542
+ cache.set(key, value);
543
+ while (cache.size > limit)
544
+ cache.delete(cache.keys().next().value);
545
+ return { value, reused: false };
546
+ }
547
+ function trimCache(cache, limit) {
548
+ while (cache.size > limit)
549
+ cache.delete(cache.keys().next().value);
550
+ }
357
551
  function governingPrompt(anchors, scrollTop) {
358
552
  let result = null;
359
553
  for (const anchor of anchors) {
@@ -376,6 +570,60 @@ function padRowPreservingBackground(line, width) {
376
570
  return `${shown}${reverse}${" ".repeat(padding)}\u001b[27m`;
377
571
  return shown + " ".repeat(padding);
378
572
  }
573
+ export function assertTranscriptViewportFrameDescriptor(descriptor) {
574
+ if (!Number.isSafeInteger(descriptor.frameId) || descriptor.frameId < 1)
575
+ throw new TypeError("viewport frame descriptor identity is invalid");
576
+ if (!Number.isSafeInteger(descriptor.width) || descriptor.width < 1)
577
+ throw new TypeError("viewport frame descriptor width is invalid");
578
+ if (!Number.isSafeInteger(descriptor.height) || descriptor.height < 0)
579
+ throw new TypeError("viewport frame descriptor height is invalid");
580
+ assertRectangle(descriptor.transcript, descriptor.height, "transcript");
581
+ assertRectangle(descriptor.dock, descriptor.height, "dock");
582
+ if (descriptor.transcript !== null && descriptor.dock !== null && descriptor.transcript.rowEnd >= descriptor.dock.rowStart) {
583
+ throw new TypeError("viewport frame descriptor regions overlap");
584
+ }
585
+ assertDocumentRange(descriptor.nextDocumentRange, "next");
586
+ if (descriptor.previousDocumentRange !== null)
587
+ assertDocumentRange(descriptor.previousDocumentRange, "previous");
588
+ if (!Number.isSafeInteger(descriptor.verticalShiftRows))
589
+ throw new TypeError("viewport frame descriptor shift is invalid");
590
+ if (descriptor.previousDocumentRange === null && descriptor.verticalShiftRows !== 0) {
591
+ throw new TypeError("viewport frame descriptor initial shift is invalid");
592
+ }
593
+ if (descriptor.previousDocumentRange !== null
594
+ && descriptor.verticalShiftRows !== descriptor.nextDocumentRange.start - descriptor.previousDocumentRange.start) {
595
+ throw new TypeError("viewport frame descriptor shift disagrees with document ranges");
596
+ }
597
+ if (descriptor.safeVerticalShift && (descriptor.verticalShiftRows <= 0
598
+ || descriptor.previousFollowingEnd !== true
599
+ || !descriptor.followingEnd
600
+ || descriptor.cause !== "follow-shift")) {
601
+ throw new TypeError("viewport frame descriptor marks an unsafe shift");
602
+ }
603
+ if (!Number.isSafeInteger(descriptor.selectionRevision) || descriptor.selectionRevision < 0) {
604
+ throw new TypeError("viewport frame descriptor selection revision is invalid");
605
+ }
606
+ if (descriptor.selectionDamagedRows.some((row, index) => !Number.isSafeInteger(row)
607
+ || row < 1 || row > descriptor.height || (index > 0 && row <= descriptor.selectionDamagedRows[index - 1]))) {
608
+ throw new TypeError("viewport frame descriptor selection damage is invalid");
609
+ }
610
+ }
611
+ function assertRectangle(rectangle, height, label) {
612
+ if (rectangle === null)
613
+ return;
614
+ if (!Number.isSafeInteger(rectangle.rowStart) || !Number.isSafeInteger(rectangle.rowEnd)
615
+ || rectangle.rowStart < 1 || rectangle.rowEnd < rectangle.rowStart || rectangle.rowEnd > height) {
616
+ throw new TypeError(`viewport frame descriptor ${label} region is invalid`);
617
+ }
618
+ }
619
+ function assertDocumentRange(range, label) {
620
+ if (!Number.isSafeInteger(range.start) || !Number.isSafeInteger(range.end) || range.start < 0 || range.end < range.start) {
621
+ throw new TypeError(`viewport frame descriptor ${label} document range is invalid`);
622
+ }
623
+ }
624
+ function sameRectangle(left, right) {
625
+ return left === null ? right === null : right !== null && left.rowStart === right.rowStart && left.rowEnd === right.rowEnd;
626
+ }
379
627
  function clamp(value, minimum, maximum) {
380
628
  return Math.min(Math.max(value, minimum), maximum);
381
629
  }
@@ -15,8 +15,6 @@ export interface OwnedUiSettingsEntry {
15
15
  readonly storedValue: unknown;
16
16
  readonly effectiveValue: unknown;
17
17
  readonly editable: boolean;
18
- readonly available: boolean;
19
- readonly limitationReason: string | null;
20
18
  readonly choices: readonly OwnedUiSettingValue[] | null;
21
19
  /** True when the value is a structured object edited through its own surface. */
22
20
  readonly structured: boolean;
@@ -15,8 +15,6 @@ export function buildOwnedUiSettingsSections(input) {
15
15
  storedValue: setting.value,
16
16
  effectiveValue: setting.value,
17
17
  editable: true,
18
- available: true,
19
- limitationReason: null,
20
18
  structured: false,
21
19
  minimum: null,
22
20
  maximum: null,
@@ -52,9 +50,12 @@ function agentSection(snapshot) {
52
50
  return frozenSection(`Agent settings are unavailable: ${snapshot.failure}`, null, []);
53
51
  }
54
52
  if (snapshot.descriptors.length === 0) {
55
- return frozenSection("The agent engine reported no settings", null, []);
53
+ return { id: AGENT_SECTION_ID, title: "Agent", entries: Object.freeze([]), unavailableReason: null, readOnlyReason: null };
56
54
  }
57
- const entries = snapshot.descriptors.map(descriptor => {
55
+ const presented = snapshot.writeAdvertised
56
+ ? snapshot.descriptors.filter(descriptor => descriptor.writable && descriptor.available)
57
+ : [];
58
+ const entries = presented.map(descriptor => {
58
59
  const raw = descriptor.storedValue;
59
60
  return {
60
61
  id: descriptor.key,
@@ -65,33 +66,26 @@ function agentSection(snapshot) {
65
66
  rawValue: raw,
66
67
  storedValue: descriptor.storedValue,
67
68
  effectiveValue: descriptor.effectiveValue,
68
- available: descriptor.available,
69
- limitationReason: descriptor.limitationReason,
70
- // A structured value is editable through its own surface rather than a
71
- // value menu, so it stays reachable instead of being reported as fixed.
72
- editable: snapshot.writeAdvertised && descriptor.writable && descriptor.available,
69
+ // Invariant: unavailable and non-writable descriptors are filtered before projection.
70
+ // Every presented structured value remains editable through its own surface.
71
+ editable: true,
73
72
  structured: descriptor.valueType === "json",
74
73
  minimum: descriptor.minimum ?? null,
75
74
  maximum: descriptor.maximum ?? null,
76
75
  flags: descriptor.flags ?? [],
77
- // A boolean is a two-value choice even when the engine names no choices,
76
+ // Protocol: a boolean is a two-value choice even when the engine names no choices,
78
77
  // so it opens the same menu as any other enumerated setting.
79
78
  choices: descriptor.valueType === "boolean" ? BOOLEAN_CHOICES : choicesOf(descriptor.choices),
80
79
  origin: "engine",
81
80
  application: descriptor.application,
82
81
  };
83
82
  });
84
- const readOnlyReason = snapshot.writeAdvertised
85
- ? (entries.some(entry => entry.editable)
86
- ? null
87
- : "The agent engine reports no writable settings")
88
- : "The agent engine does not support changing settings from this surface";
89
83
  return {
90
84
  id: AGENT_SECTION_ID,
91
85
  title: "Agent",
92
86
  entries: Object.freeze(entries),
93
87
  unavailableReason: null,
94
- readOnlyReason,
88
+ readOnlyReason: null,
95
89
  };
96
90
  }
97
91
  function frozenSection(unavailableReason, readOnlyReason, entries) {
@@ -49,7 +49,7 @@ export class OwnedUiSettingsSession {
49
49
  if (entry === null)
50
50
  return failed(`unknown ${backend} setting: ${id}`);
51
51
  if (!entry.editable)
52
- return failed(entry.limitationReason ?? `${id} is not editable from this surface`);
52
+ return failed(`${id} is not editable from this surface`);
53
53
  return backend === "a1" ? this.#changeOwned(id, value) : await this.#changeAgentValue(id, value);
54
54
  }
55
55
  #changeOwned(id, value) {
@@ -78,7 +78,7 @@ export class OwnedUiSettingsSession {
78
78
  }
79
79
  let result;
80
80
  try {
81
- // The coordinator behind the port owns effect installation, persistence,
81
+ // Invariant: the coordinator behind the port owns effect installation, persistence,
82
82
  // flush, and rollback. A second surface-level flush would split authority.
83
83
  result = await agent.writeSetting(id, value);
84
84
  }
@@ -102,10 +102,11 @@ async function snapshotOf(agent, hidden) {
102
102
  const listed = await agent.listSettings();
103
103
  for (const descriptor of listed)
104
104
  assertAgentSettingDescriptor(descriptor);
105
- const descriptors = listed.filter(descriptor => !hidden.has(descriptor.key));
105
+ const writeAdvertised = agent.capabilities.write && typeof agent.writeSetting === "function";
106
+ const descriptors = listed.filter(descriptor => !hidden.has(descriptor.key) && writeAdvertised && descriptor.writable && descriptor.available);
106
107
  return {
107
108
  descriptors,
108
- writeAdvertised: agent.capabilities.write && typeof agent.writeSetting === "function",
109
+ writeAdvertised,
109
110
  failure: null,
110
111
  };
111
112
  }
@@ -13,6 +13,7 @@ export interface OwnedUiSettingsWriteOutcome {
13
13
  /** Present when the write failed; the caller must not report the change as saved. */
14
14
  readonly failure: string | null;
15
15
  }
16
+ /** Resolves and atomically persists one profile's owned settings without discarding unknown keys. */
16
17
  export declare class OwnedUiSettingsStore {
17
18
  #private;
18
19
  constructor(options: OwnedUiSettingsStoreOptions);
@@ -5,6 +5,7 @@ import { OWNED_UI_SETTINGS_MIGRATIONS } from "./migrations.js";
5
5
  import { documentFrom, parseOwnedUiSettingsDocument, resolveOwnedUiSettings, } from "./resolution.js";
6
6
  const PROFILE_ID_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
7
7
  const MAX_DOCUMENT_BYTES = 256 * 1024;
8
+ /** Resolves and atomically persists one profile's owned settings without discarding unknown keys. */
8
9
  export class OwnedUiSettingsStore {
9
10
  #file;
10
11
  #declarations;
@@ -76,7 +77,7 @@ export class OwnedUiSettingsStore {
76
77
  rmSync(temporary, { force: true });
77
78
  }
78
79
  catch {
79
- // A leftover temporary file is inert: read() only ever opens the target path.
80
+ // Security: a leftover temporary file is inert: read() only ever opens the target path.
80
81
  }
81
82
  return { stored: false, failure: `${this.#file} could not be written: ${describe(error)}` };
82
83
  }