@timurproko/a1 0.2.7-dev.1255 → 0.2.7-dev.1264

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 (78) hide show
  1. package/dist/app/session-shell/prompt-submission.d.ts +1 -1
  2. package/dist/app/session-shell/prompt-submission.js +9 -9
  3. package/dist/app/session-shell/resource-labels.d.ts +2 -0
  4. package/dist/app/session-shell/resource-labels.js +158 -0
  5. package/dist/app/session-shell/session-prompt-history.d.ts +29 -0
  6. package/dist/app/session-shell/session-prompt-history.js +80 -0
  7. package/dist/app/session-shell/session-selectors.d.ts +46 -0
  8. package/dist/app/session-shell/session-selectors.js +269 -0
  9. package/dist/app/session-shell/session-shell-root.d.ts +6 -28
  10. package/dist/app/session-shell/session-shell-root.js +26 -220
  11. package/dist/app/session-shell/session-shell.d.ts +3 -23
  12. package/dist/app/session-shell/session-shell.js +54 -365
  13. package/dist/app/session-shell/session-viewport-controller.d.ts +2 -10
  14. package/dist/app/session-shell/session-viewport-controller.js +40 -318
  15. package/dist/app/session-shell/settings-bindings.d.ts +1 -1
  16. package/dist/app/session-shell/settings-bindings.js +2 -2
  17. package/dist/app/session-shell/viewport-pointer.d.ts +83 -0
  18. package/dist/app/session-shell/viewport-pointer.js +361 -0
  19. package/dist/composition/owned-ui.js +63 -59
  20. package/dist/contracts/owned-ui/index.d.ts +1 -1
  21. package/dist/contracts/owned-ui/index.js +1 -1
  22. package/dist/contracts/owned-ui/validation.d.ts +1 -11
  23. package/dist/contracts/owned-ui/validation.js +8 -104
  24. package/dist/contracts/presentation/index.d.ts +1 -1
  25. package/dist/contracts/presentation/terminal.d.ts +0 -6
  26. package/dist/features/owned-ui/settings-app.js +13 -208
  27. package/dist/features/owned-ui/settings-model-dialog.d.ts +41 -0
  28. package/dist/features/owned-ui/settings-model-dialog.js +194 -0
  29. package/dist/foundation/release/automatic-update.js +1 -8
  30. package/dist/foundation/release/dependency-layer.js +3 -3
  31. package/dist/foundation/release/latest-release.js +1 -1
  32. package/dist/foundation/release/managed-path.d.ts +10 -0
  33. package/dist/foundation/release/managed-path.js +26 -0
  34. package/dist/foundation/release/release-gc.js +6 -8
  35. package/dist/foundation/release/restart-certification.js +3 -4
  36. package/dist/foundation/release/update-activation.js +1 -1
  37. package/dist/foundation/release/update-execution-lease.js +1 -7
  38. package/dist/foundation/release/update-installer.d.ts +45 -0
  39. package/dist/foundation/release/update-installer.js +57 -0
  40. package/dist/foundation/release/update-recovery.js +2 -9
  41. package/dist/foundation/release/update.d.ts +4 -23
  42. package/dist/foundation/release/update.js +160 -179
  43. package/dist/integrations/pi/components/bracketed-paste.d.ts +33 -0
  44. package/dist/integrations/pi/components/bracketed-paste.js +123 -0
  45. package/dist/integrations/pi/components/owned-editor-ux.js +46 -140
  46. package/dist/integrations/pi/engine/adapter.d.ts +1 -8
  47. package/dist/integrations/pi/engine/adapter.js +6 -49
  48. package/dist/integrations/pi/engine/host.js +1 -1
  49. package/dist/integrations/pi/engine/repository-metadata.d.ts +49 -0
  50. package/dist/integrations/pi/engine/repository-metadata.js +189 -0
  51. package/dist/integrations/pi/engine/session-runtime.d.ts +6 -12
  52. package/dist/integrations/pi/engine/session-runtime.js +21 -178
  53. package/dist/integrations/pi/engine/view-state.d.ts +21 -2
  54. package/dist/integrations/pi/engine/view-state.js +129 -1
  55. package/dist/integrations/pi/engine/workflow-runner.d.ts +0 -1
  56. package/dist/integrations/pi/engine/workflow-runner.js +254 -365
  57. package/dist/integrations/pi/engine/workflow-share.d.ts +9 -0
  58. package/dist/integrations/pi/engine/workflow-share.js +168 -0
  59. package/dist/integrations/pi/engine/workflow-support.d.ts +0 -14
  60. package/dist/integrations/pi/engine/workflow-support.js +0 -47
  61. package/dist/integrations/pi/tui-runtime/adapter.d.ts +9 -19
  62. package/dist/integrations/pi/tui-runtime/adapter.js +35 -144
  63. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +4 -4
  64. package/dist/integrations/pi/tui-runtime/index.d.ts +0 -1
  65. package/dist/integrations/pi/tui-runtime/overlay-geometry.d.ts +2 -2
  66. package/dist/integrations/pi/tui-runtime/presentation-adapter.d.ts +2 -2
  67. package/dist/ui/components/transcript-viewport-frame.d.ts +120 -0
  68. package/dist/ui/components/transcript-viewport-frame.js +314 -0
  69. package/dist/ui/components/transcript-viewport-types.d.ts +147 -0
  70. package/dist/ui/components/transcript-viewport.d.ts +2 -147
  71. package/dist/ui/components/transcript-viewport.js +87 -315
  72. package/package.json +3 -3
  73. package/dist/integrations/pi/engine/diagnostics-state.d.ts +0 -9
  74. package/dist/integrations/pi/engine/diagnostics-state.js +0 -34
  75. package/dist/integrations/pi/engine/usage-view.d.ts +0 -4
  76. package/dist/integrations/pi/engine/usage-view.js +0 -56
  77. package/dist/integrations/pi/tui-runtime/contracts.d.ts +0 -22
  78. /package/dist/{integrations/pi/tui-runtime/contracts.js → ui/components/transcript-viewport-types.js} +0 -0
@@ -1,6 +1,7 @@
1
1
  import { ImageAttachmentError, MAX_CLIPBOARD_TEXT_BYTES } from "../../../contracts/owned-ui/index.js";
2
2
  import { CURSOR_MARKER, decodeKittyPrintable, visibleWidth, } from "@earendil-works/pi-tui";
3
3
  import { promptPathWordRanges } from "./path-word-ranges.js";
4
+ import { BracketedPasteFramer, PASTE_START } from "./bracketed-paste.js";
4
5
  export class OwnedEditorUxInterception {
5
6
  interceptors;
6
7
  fallback;
@@ -68,7 +69,7 @@ class PromptSelectionInterceptor {
68
69
  #redoStack = [];
69
70
  #selectionRevision = 0;
70
71
  #pasteState = { generation: 0, preview: undefined };
71
- #terminalPaste;
72
+ #paste;
72
73
  #wordDirection;
73
74
  #geometry;
74
75
  editor;
@@ -78,11 +79,12 @@ class PromptSelectionInterceptor {
78
79
  this.editor = editor;
79
80
  this.keybindings = keybindings;
80
81
  this.options = options;
82
+ this.#paste = new BracketedPasteFramer(options.beginTextPaste === undefined ? undefined : MAX_CLIPBOARD_TEXT_BYTES);
81
83
  installAtomicSegmentation(editor, options.atomicRanges, () => this.#wordDirection);
82
84
  }
83
85
  handleInput(data, next) {
84
86
  this.#pasteState.preview = undefined;
85
- if (this.options.pasteEnabled === false ? discardTerminalPaste(this, data, value => this.handleInput(value, next)) : this.#handleTerminalPaste(data, next))
87
+ if (this.options.pasteEnabled === false ? this.#paste.discard(data, value => this.handleInput(value, next)) : this.#handleTerminalPaste(data, next))
86
88
  return;
87
89
  if (this.keybindings.matches(data, "owned.editor.selectAll")) {
88
90
  this.#selectAll();
@@ -166,24 +168,7 @@ class PromptSelectionInterceptor {
166
168
  return;
167
169
  }
168
170
  if (this.keybindings.matches(data, "tui.editor.cursorWordLeft")) {
169
- const before = this.#cursor();
170
- const startedWithAtomicFocus = this.#atomicFocus() !== undefined;
171
- this.#delegateWord(-1, next);
172
- let after = this.#cursor();
173
- const landedAtomic = this.#atomicRangeAt(after);
174
- if (!startedWithAtomicFocus && landedAtomic?.start === after.col && after.col > 0) {
175
- const line = editorState(this.editor).lines[after.line] ?? "";
176
- const previous = [...GRAPHEMES.segment(line.slice(0, after.col))].at(-1);
177
- if (previous !== undefined && !/^\s+$/u.test(previous.segment)) {
178
- this.#delegateWord(-1, next);
179
- after = this.#cursor();
180
- }
181
- }
182
- const line = editorState(this.editor).lines[after.line] ?? "";
183
- const landedPath = promptPathWordRanges(line).some(range => range.start === after.col);
184
- if (!samePosition(before, after) && !landedPath)
185
- this.#moveOntoPreviousSeparator();
186
- this.#requestRender();
171
+ this.#moveWordLeft(next);
187
172
  return;
188
173
  }
189
174
  const beforeText = this.editor.getText();
@@ -302,14 +287,15 @@ class PromptSelectionInterceptor {
302
287
  this.#pointerSelecting = false;
303
288
  this.#lastClick = undefined;
304
289
  this.#redoStack = [];
305
- DISABLED_TERMINAL_PASTES.delete(this);
290
+ // Invariant: reset forgets a paste being dropped, but a paste being inserted still completes.
291
+ if (this.options.pasteEnabled === false)
292
+ this.#paste.clear();
306
293
  this.#selectionRevision += 1;
307
294
  }
308
295
  cancelPendingPastes() {
309
296
  this.#pasteState.generation += 1;
310
297
  this.#pasteState.preview = undefined;
311
- this.#terminalPaste = undefined;
312
- DISABLED_TERMINAL_PASTES.delete(this);
298
+ this.#paste.clear();
313
299
  }
314
300
  hasSelection() {
315
301
  return this.#activeRange() !== undefined;
@@ -485,61 +471,30 @@ class PromptSelectionInterceptor {
485
471
  this.#requestRender();
486
472
  }
487
473
  #handleTerminalPaste(data, next) {
488
- const start = data.indexOf("\x1b[200~");
489
- if (this.#terminalPaste === undefined) {
474
+ if (!this.#paste.open) {
475
+ const start = data.indexOf(PASTE_START);
490
476
  if (start < 0)
491
477
  return false;
492
478
  if (start > 0) {
493
479
  const before = data.slice(0, start);
494
480
  this.handleInput(before, (replacement = before) => next(replacement));
495
481
  }
496
- this.#terminalPaste = { parts: [], piece: "", tail: "", bytes: 0, oversized: false };
497
- data = data.slice(start + 6);
482
+ data = data.slice(start);
498
483
  }
499
- // Protocol: inspect only the new fragment plus a delimiter-sized suffix, not the whole growing body.
500
- const pending = this.#terminalPaste;
501
- const fragment = pending.tail + data;
502
- const end = fragment.indexOf("\x1b[201~");
503
- const append = (part) => {
504
- if (pending.oversized || part.length === 0)
505
- return;
506
- if (this.options.beginTextPaste !== undefined) {
507
- pending.bytes += Buffer.byteLength(part);
508
- if (pending.bytes > MAX_CLIPBOARD_TEXT_BYTES) {
509
- pending.oversized = true;
510
- pending.parts = [];
511
- pending.piece = "";
512
- return;
513
- }
514
- }
515
- // Performance: bound fragment metadata even if a terminal supplies one character per input chunk.
516
- pending.piece += part;
517
- if (pending.piece.length >= 16_384) {
518
- pending.parts.push(pending.piece);
519
- pending.piece = "";
520
- }
521
- };
522
- if (end < 0) {
523
- let cut = Math.max(0, fragment.length - 5);
524
- if (cut > 0 && /[\uD800-\uDBFF]/u.test(fragment[cut - 1]) && /[\uDC00-\uDFFF]/u.test(fragment[cut]))
525
- cut--;
526
- append(fragment.slice(0, cut));
527
- pending.tail = fragment.slice(cut);
484
+ const pasted = this.#paste.feed(data);
485
+ if (pasted === undefined)
528
486
  return true;
529
- }
530
- append(fragment.slice(0, end));
531
- const remaining = fragment.slice(end + 6);
532
- this.#terminalPaste = undefined;
533
- if (pending.oversized) {
534
- this.options.onPasteInput?.(pending.bytes, "framing");
487
+ const { remaining, bytes } = pasted;
488
+ if (pasted.oversized) {
489
+ this.options.onPasteInput?.(bytes, "framing");
535
490
  this.options.onPasteRejected?.(new ImageAttachmentError("paste-size"));
536
491
  if (remaining.length > 0)
537
492
  this.handleInput(remaining, (replacement = remaining) => next(replacement));
538
493
  return true;
539
494
  }
540
- const text = pending.parts.join("") + pending.piece;
495
+ const text = pasted.text;
541
496
  const deferred = text.length > 0 && this.options.beginTextPaste !== undefined && (this.options.deferTextPaste?.(text) ?? true);
542
- this.options.onPasteInput?.(pending.bytes, "framing");
497
+ this.options.onPasteInput?.(bytes, "framing");
543
498
  if (text.length > 0)
544
499
  this.#redoStack = [];
545
500
  if (text.length === 0)
@@ -547,7 +502,7 @@ class PromptSelectionInterceptor {
547
502
  else if (deferred)
548
503
  this.#pasteFromClipboard(text);
549
504
  else {
550
- this.options.onPasteInput?.(pending.bytes, "inserting");
505
+ this.options.onPasteInput?.(bytes, "inserting");
551
506
  const transformed = this.options.transformPastedContent({ kind: "text", text });
552
507
  if (transformed === text && this.#orderedSelection() === undefined)
553
508
  next(`\x1b[200~${text}\x1b[201~`);
@@ -559,7 +514,7 @@ class PromptSelectionInterceptor {
559
514
  this.#redoStack = [];
560
515
  this.#requestRender();
561
516
  }
562
- this.options.onPasteInput?.(pending.bytes, "settled");
517
+ this.options.onPasteInput?.(bytes, "settled");
563
518
  }
564
519
  if (remaining.length > 0)
565
520
  this.handleInput(remaining, (replacement = remaining) => next(replacement));
@@ -835,14 +790,31 @@ class PromptSelectionInterceptor {
835
790
  const line = editorState(this.editor).lines[position.line] ?? "";
836
791
  return this.options.atomicRanges(line).find(range => position.col >= range.start && position.col < range.end);
837
792
  }
838
- #moveOntoPreviousSeparator() {
839
- const cursor = this.#cursor();
840
- if (cursor.col === 0)
841
- return;
842
- const line = editorState(this.editor).lines[cursor.line] ?? "";
843
- const previous = [...GRAPHEMES.segment(line.slice(0, cursor.col))].at(-1);
844
- if (previous !== undefined && /^\s+$/u.test(previous.segment))
845
- this.#moveCursor(-1);
793
+ // Compatibility: word-left steps over an atomic chip as one word, stops on a path boundary, and
794
+ // otherwise lands on the separator before the word, as pinned Pi's prompt editor does.
795
+ #moveWordLeft(next) {
796
+ const before = this.#cursor();
797
+ const startedWithAtomicFocus = this.#atomicFocus() !== undefined;
798
+ this.#delegateWord(-1, next);
799
+ let after = this.#cursor();
800
+ const landedAtomic = this.#atomicRangeAt(after);
801
+ if (!startedWithAtomicFocus && landedAtomic?.start === after.col && after.col > 0) {
802
+ const line = editorState(this.editor).lines[after.line] ?? "";
803
+ const previous = [...GRAPHEMES.segment(line.slice(0, after.col))].at(-1);
804
+ if (previous !== undefined && !/^\s+$/u.test(previous.segment)) {
805
+ this.#delegateWord(-1, next);
806
+ after = this.#cursor();
807
+ }
808
+ }
809
+ const line = editorState(this.editor).lines[after.line] ?? "";
810
+ const landedPath = promptPathWordRanges(line).some(range => range.start === after.col);
811
+ if (!samePosition(before, after) && !landedPath && after.col > 0) {
812
+ // Invariant: move onto the separator before the word, never past the line start.
813
+ const previous = [...GRAPHEMES.segment(line.slice(0, after.col))].at(-1);
814
+ if (previous !== undefined && /^\s+$/u.test(previous.segment))
815
+ this.#moveCursor(-1);
816
+ }
817
+ this.#requestRender();
846
818
  }
847
819
  #snapshot() {
848
820
  return { text: this.editor.getText(), cursor: this.#cursor() };
@@ -981,72 +953,6 @@ function isEditorSegment(value) {
981
953
  const input = Reflect.get(value, "input");
982
954
  return typeof segment === "string" && typeof index === "number" && typeof input === "string";
983
955
  }
984
- const DISABLED_TERMINAL_PASTES = new WeakMap();
985
- function discardTerminalPaste(owner, data, forward) {
986
- const startMarker = "\x1b[200~";
987
- const endMarker = "\x1b[201~";
988
- const state = DISABLED_TERMINAL_PASTES.get(owner) ?? { startTail: "", bodyTail: null };
989
- if (state.bodyTail !== null) {
990
- const framed = state.bodyTail + data;
991
- const end = framed.indexOf(endMarker);
992
- if (end < 0) {
993
- state.bodyTail = delimiterTail(framed, endMarker);
994
- DISABLED_TERMINAL_PASTES.set(owner, state);
995
- return true;
996
- }
997
- DISABLED_TERMINAL_PASTES.delete(owner);
998
- const remaining = framed.slice(end + endMarker.length);
999
- if (remaining.length > 0)
1000
- forward(remaining);
1001
- return true;
1002
- }
1003
- const hadStartTail = state.startTail.length > 0;
1004
- const candidate = state.startTail + data;
1005
- state.startTail = "";
1006
- const start = candidate.indexOf(startMarker);
1007
- if (start >= 0) {
1008
- if (start > 0)
1009
- forward(candidate.slice(0, start));
1010
- const framed = candidate.slice(start + startMarker.length);
1011
- const end = framed.indexOf(endMarker);
1012
- if (end < 0) {
1013
- state.bodyTail = delimiterTail(framed, endMarker);
1014
- DISABLED_TERMINAL_PASTES.set(owner, state);
1015
- }
1016
- else {
1017
- DISABLED_TERMINAL_PASTES.delete(owner);
1018
- const remaining = framed.slice(end + endMarker.length);
1019
- if (remaining.length > 0)
1020
- forward(remaining);
1021
- }
1022
- return true;
1023
- }
1024
- const partial = delimiterTail(candidate, startMarker);
1025
- // Compatibility: standalone Escape still cancels; marker fragments stay pending.
1026
- if (partial.length > 1) {
1027
- const before = candidate.slice(0, candidate.length - partial.length);
1028
- if (before.length > 0)
1029
- forward(before);
1030
- state.startTail = partial;
1031
- DISABLED_TERMINAL_PASTES.set(owner, state);
1032
- return true;
1033
- }
1034
- DISABLED_TERMINAL_PASTES.delete(owner);
1035
- if (hadStartTail) {
1036
- forward(candidate);
1037
- return true;
1038
- }
1039
- return false;
1040
- }
1041
- function delimiterTail(text, delimiter) {
1042
- const limit = Math.min(text.length, delimiter.length - 1);
1043
- for (let length = limit; length > 0; length -= 1) {
1044
- const suffix = text.slice(text.length - length);
1045
- if (delimiter.startsWith(suffix))
1046
- return suffix;
1047
- }
1048
- return "";
1049
- }
1050
956
  export function editorInsertedText(data) {
1051
957
  if (!data || data.includes("\u001b[200~"))
1052
958
  return undefined;
@@ -1,5 +1,5 @@
1
1
  import { type OwnedUiCommand, type OwnedUiExtensionPort, type OwnedUiPromptSuggestionGeneratorPort, type OwnedUiPromptSuggestionReasoning, type OwnedUiPromptSuggestionRequest, type OwnedUiPromptSuggestionResult, type OwnedUiEvent, type OwnedUiSessionBackend, type OwnedUiSessionCatalogPort, type OwnedUiSessionIdentityPort, type OwnedUiSessionPort, type OwnedUiSessionSettingsPort, type OwnedUiSessionViewModel, type OwnedUiSnapshot, type OwnedUiWorkflowPort } from "../../../contracts/owned-ui/index.js";
2
- import type { PiPinnedSettingsSnapshot, PiWorkflowHost, PiWorkflowInteractionHost, PiWorkflowRequest, PiWorkflowResult } from "./workflows.js";
2
+ import type { PiWorkflowHost, PiWorkflowInteractionHost, PiWorkflowRequest, PiWorkflowResult } from "./workflows.js";
3
3
  import type { PiSettingsBridge } from "./settings-bridge.js";
4
4
  import type { PiProjectTrustPreflightPrompt } from "./project-trust-preflight.js";
5
5
  import type { PiSessionForkPrompt, PiSessionSelection } from "./session-selection.js";
@@ -80,16 +80,9 @@ export declare class PiEngineAdapter implements OwnedUiSessionBackend, OwnedUiPr
80
80
  peakBytes: number;
81
81
  };
82
82
  flushEvents(): Promise<void>;
83
- abortBashWorkflow(): void;
84
83
  executeWorkflow(request: PiWorkflowRequest): Promise<PiWorkflowResult>;
85
- /**
86
- * The theme the engine is configured with, in the engine's own grammar: a
87
- * theme's name, or a `light/dark` pair meaning "follow the terminal".
88
- */
89
- configuredTheme(): string | undefined;
90
84
  /** Settings port for the live runtime, or null before the runtime is available. */
91
85
  settingsPort(): PiSettingsBridge | null;
92
- pinnedSettingsSnapshot(): PiPinnedSettingsSnapshot;
93
86
  clearQueuedWorkflows(): readonly string[];
94
87
  view(): OwnedUiSessionViewModel;
95
88
  snapshot(): OwnedUiSnapshot;
@@ -11,11 +11,10 @@ import { PiEngineRuntime } from "./session-runtime.js";
11
11
  import { detachedEngineHostPorts } from "./host-ports.js";
12
12
  import { PiEngineSettings } from "./settings-port.js";
13
13
  import { PiSessionEvents } from "./session-events.js";
14
- import { readUsageView } from "./usage-view.js";
15
14
  import { PiWorkflowContexts } from "./workflow-contexts.js";
16
15
  import { PiWorkflowRunner } from "./workflow-runner.js";
17
16
  import { defaultWorkflowHost } from "./workflow-support.js";
18
- import { EngineViewState } from "./view-state.js";
17
+ import { EngineViewState, projectSessionView } from "./view-state.js";
19
18
  import { createEnginePorts } from "./ports.js";
20
19
  /** Explicit flush failure when required delivery was interrupted rather than completed. */
21
20
  export class EngineDeliveryError extends Error {
@@ -254,9 +253,6 @@ export class PiEngineAdapter {
254
253
  throw new EngineDeliveryError();
255
254
  }
256
255
  }
257
- abortBashWorkflow() {
258
- this.#workflows.abortBashWorkflow();
259
- }
260
256
  async executeWorkflow(request) {
261
257
  const result = await this.#workflows.executeWorkflow(request);
262
258
  if (request.command === "tree" && result.outcome === "completed" && request.selection !== undefined) {
@@ -264,20 +260,10 @@ export class PiEngineAdapter {
264
260
  }
265
261
  return result;
266
262
  }
267
- /**
268
- * The theme the engine is configured with, in the engine's own grammar: a
269
- * theme's name, or a `light/dark` pair meaning "follow the terminal".
270
- */
271
- configuredTheme() {
272
- return this.#settings.configuredTheme();
273
- }
274
263
  /** Settings port for the live runtime, or null before the runtime is available. */
275
264
  settingsPort() {
276
265
  return this.#settings.settingsPort();
277
266
  }
278
- pinnedSettingsSnapshot() {
279
- return this.#settings.pinnedSettingsSnapshot();
280
- }
281
267
  clearQueuedWorkflows() {
282
268
  const session = this.#requireWorkflowSession();
283
269
  const result = session.clearQueue?.();
@@ -287,42 +273,13 @@ export class PiEngineAdapter {
287
273
  return [...readStringArray(result.steering), ...readStringArray(result.followUp)];
288
274
  }
289
275
  view() {
290
- const routed = this.#engine.session?.routedModel;
291
- const routedModel = routed === undefined ? null : readModel(routed.model);
292
- return {
293
- contractVersion: 1,
276
+ return projectSessionView({
294
277
  sessionId: this.#sessionId,
295
- revision: this.#state.revision,
296
- lifecycle: this.#state.lifecycle,
278
+ state: this.#state,
279
+ engine: this.#engine,
297
280
  transcript: this.#projection.snapshot(),
298
- editor: { ...this.#state.editor, queuedSubmissions: [...this.#state.editor.queuedSubmissions] },
299
- status: {
300
- ...this.#state.status,
301
- diagnostics: [...this.#state.status.diagnostics],
302
- badges: [...this.#state.status.badges],
303
- usage: this.#state.usage(() => readUsageView(this.#engine.session, this.#engine.runtime, this.#state.activeModel)),
304
- footer: {
305
- branch: this.#engine.gitBranch,
306
- repositoryPath: this.#engine.repositoryCwd,
307
- pullRequest: this.#engine.pullRequest,
308
- sessionName: this.#engine.session?.sessionManager?.getSessionName() ?? null,
309
- availableProviderCount: new Set(this.#engine.runtime?.services.modelRuntime.getAvailableSnapshot?.().map(model => model.provider).filter(provider => provider !== undefined) ?? []).size,
310
- extensionStatuses: [],
311
- },
312
- },
313
- terminal: { ...this.#state.terminal },
314
- activeModel: this.#state.activeModel === null ? null : { ...this.#state.activeModel },
315
- routedModel: routedModel === null ? null : {
316
- model: routedModel,
317
- ...(routed?.thinkingLevel === undefined ? {} : { thinkingLevel: readThinkingLevel(routed.thinkingLevel) }),
318
- },
319
- thinkingLevel: this.#state.thinkingLevel,
320
- activeCommandIds: [...this.#commands.activeCommandIds],
321
- dialog: null,
322
- overlay: null,
323
- customizations: [],
324
- diagnostics: this.#state.diagnostics.map(diagnostic => ({ ...diagnostic })),
325
- };
281
+ activeCommandIds: this.#commands.activeCommandIds,
282
+ });
326
283
  }
327
284
  snapshot() {
328
285
  const snapshot = {
@@ -134,7 +134,7 @@ class ProcessPiEngineHost {
134
134
  const theme = this.#options.theme;
135
135
  theme.apply.packageBorderProjection(theme.packageBorderProjection);
136
136
  theme.apply.accentColor(this.#accentColor);
137
- theme.apply.base(theme.base === "dark" ? "dark" : adapter.configuredTheme());
137
+ theme.apply.base(theme.base === "dark" ? "dark" : adapter.settings.configuredTheme());
138
138
  }
139
139
  async #announceOnce(adapter) {
140
140
  if (this.#announced)
@@ -0,0 +1,49 @@
1
+ import { type PiPullRequestIdentity, type PiPullRequestProbe } from "./repository-pr.js";
2
+ interface PiRepositoryContextSelection {
3
+ readonly cwd: string;
4
+ readonly branch: string;
5
+ }
6
+ export type PiRepositoryContextReader = (sessionId: string, sessionFile: string, signal: AbortSignal) => Promise<PiRepositoryContextSelection | null>;
7
+ type PiGitBranchReader = (cwd: string, signal: AbortSignal) => Promise<string | null>;
8
+ export interface RepositoryMetadataOptions {
9
+ readonly pullRequestProbe?: PiPullRequestProbe;
10
+ readonly pullRequestRefreshMs?: number;
11
+ readonly repositoryContextPollMs?: number;
12
+ readonly repositoryContextReader?: PiRepositoryContextReader;
13
+ readonly gitBranchReader?: PiGitBranchReader;
14
+ }
15
+ export interface RepositoryMetadataPorts {
16
+ /** The session binding generation; a probe started under an older one is dropped. */
17
+ bindingGeneration(): number;
18
+ /** The bound session's id and file, when its session manager exposes them. */
19
+ sessionIdentity(): {
20
+ readonly sessionId: unknown;
21
+ readonly sessionFile: unknown;
22
+ };
23
+ /** The engine cwd, used when no associated repository context is known. */
24
+ fallbackCwd(): string;
25
+ disposed(): boolean;
26
+ /** The branch, pull request or repository path changed. */
27
+ changed(): void;
28
+ }
29
+ /**
30
+ * The branch and pull request shown for the bound session: a poll loop with one probe in flight,
31
+ * a pending flag for a refresh requested meanwhile, a slower pull-request clock, and a guard that
32
+ * drops results from an aborted probe, an older binding or a disposed engine.
33
+ */
34
+ export declare class RepositoryMetadataPoller {
35
+ #private;
36
+ constructor(options: RepositoryMetadataOptions, ports: RepositoryMetadataPorts);
37
+ get cwd(): string;
38
+ get branch(): string | null;
39
+ get pullRequest(): PiPullRequestIdentity | null;
40
+ /** Enable polling and probe now. */
41
+ start(): void;
42
+ /** Probe now while polling is enabled; a request made during a probe runs once it settles. */
43
+ refresh(): void;
44
+ /** Abort the running probe and forget the previous binding's metadata. */
45
+ reset(): void;
46
+ /** Stop polling; resolves once the running probe has settled. */
47
+ dispose(): Promise<void>;
48
+ }
49
+ export {};
@@ -0,0 +1,189 @@
1
+ import { execFile } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { promisify } from "node:util";
4
+ import { readPullRequest } from "./repository-pr.js";
5
+ const execFileAsync = promisify(execFile);
6
+ const PULL_REQUEST_REFRESH_MS = 60_000;
7
+ const REPOSITORY_CONTEXT_POLL_MS = 5_000;
8
+ const GIT_BRANCH_TIMEOUT_MS = 5_000;
9
+ const GIT_BRANCH_MAX_BUFFER_BYTES = 4 * 1024;
10
+ /**
11
+ * The branch and pull request shown for the bound session: a poll loop with one probe in flight,
12
+ * a pending flag for a refresh requested meanwhile, a slower pull-request clock, and a guard that
13
+ * drops results from an aborted probe, an older binding or a disposed engine.
14
+ */
15
+ export class RepositoryMetadataPoller {
16
+ #ports;
17
+ #pullRequestProbe;
18
+ #pullRequestRefreshMs;
19
+ #pollMs;
20
+ #contextReader;
21
+ #branchReader;
22
+ #cwd;
23
+ #branch = null;
24
+ #pullRequest = null;
25
+ #timer;
26
+ #abort;
27
+ #task;
28
+ #pending = false;
29
+ #lastPullRequestProbeAt = null;
30
+ #enabled = false;
31
+ constructor(options, ports) {
32
+ this.#ports = ports;
33
+ this.#cwd = ports.fallbackCwd();
34
+ this.#pullRequestProbe = options.pullRequestProbe ?? readPullRequest;
35
+ this.#pullRequestRefreshMs = options.pullRequestRefreshMs ?? PULL_REQUEST_REFRESH_MS;
36
+ this.#pollMs = options.repositoryContextPollMs ?? REPOSITORY_CONTEXT_POLL_MS;
37
+ this.#contextReader = options.repositoryContextReader ?? (async () => null);
38
+ this.#branchReader = options.gitBranchReader ?? readGitBranch;
39
+ }
40
+ get cwd() {
41
+ return this.#cwd;
42
+ }
43
+ get branch() {
44
+ return this.#branch;
45
+ }
46
+ get pullRequest() {
47
+ return this.#pullRequest;
48
+ }
49
+ /** Enable polling and probe now. */
50
+ start() {
51
+ this.#enabled = true;
52
+ this.refresh();
53
+ }
54
+ /** Probe now while polling is enabled; a request made during a probe runs once it settles. */
55
+ refresh() {
56
+ if (!this.#enabled || this.#ports.disposed())
57
+ return;
58
+ if (this.#task !== undefined) {
59
+ this.#pending = true;
60
+ return;
61
+ }
62
+ const task = this.#probe();
63
+ this.#task = task;
64
+ const settled = () => {
65
+ if (this.#task === task)
66
+ this.#task = undefined;
67
+ if (!this.#pending)
68
+ return;
69
+ this.#pending = false;
70
+ this.refresh();
71
+ };
72
+ void task.then(settled, settled);
73
+ }
74
+ /** Abort the running probe and forget the previous binding's metadata. */
75
+ reset() {
76
+ if (this.#timer !== undefined)
77
+ clearTimeout(this.#timer);
78
+ this.#timer = undefined;
79
+ this.#abort?.abort();
80
+ this.#cwd = this.#ports.fallbackCwd();
81
+ this.#branch = null;
82
+ this.#pullRequest = null;
83
+ this.#lastPullRequestProbeAt = null;
84
+ }
85
+ /** Stop polling; resolves once the running probe has settled. */
86
+ async dispose() {
87
+ this.#enabled = false;
88
+ if (this.#timer !== undefined)
89
+ clearTimeout(this.#timer);
90
+ this.#timer = undefined;
91
+ this.#pending = false;
92
+ this.#abort?.abort();
93
+ await this.#task;
94
+ }
95
+ #current(controller, generation) {
96
+ return !controller.signal.aborted && generation === this.#ports.bindingGeneration() && this.#enabled && !this.#ports.disposed();
97
+ }
98
+ async #probe() {
99
+ if (this.#abort !== undefined || this.#ports.disposed())
100
+ return;
101
+ const controller = new AbortController();
102
+ const generation = this.#ports.bindingGeneration();
103
+ this.#abort = controller;
104
+ let associatedContext = null;
105
+ const { sessionId, sessionFile } = this.#ports.sessionIdentity();
106
+ try {
107
+ if (typeof sessionId === "string" && sessionId.length > 0 && typeof sessionFile === "string" && sessionFile.length > 0) {
108
+ try {
109
+ associatedContext = await this.#contextReader(sessionId, sessionFile, controller.signal);
110
+ }
111
+ catch {
112
+ associatedContext = null;
113
+ }
114
+ }
115
+ const nextCwd = associatedContext?.cwd ?? this.#ports.fallbackCwd();
116
+ let nextBranch = associatedContext?.branch ?? null;
117
+ if (associatedContext === null) {
118
+ try {
119
+ nextBranch = await this.#branchReader(nextCwd, controller.signal);
120
+ }
121
+ catch {
122
+ nextBranch = null;
123
+ }
124
+ }
125
+ const metadataChanged = nextCwd !== this.#cwd || nextBranch !== this.#branch;
126
+ const remoteDue = metadataChanged || this.#lastPullRequestProbeAt === null
127
+ || Date.now() - this.#lastPullRequestProbeAt >= this.#pullRequestRefreshMs;
128
+ if (remoteDue) {
129
+ let nextPullRequest = null;
130
+ try {
131
+ nextPullRequest = nextBranch === null
132
+ ? null
133
+ : await this.#pullRequestProbe(nextCwd, nextBranch, controller.signal);
134
+ }
135
+ catch {
136
+ nextPullRequest = null;
137
+ }
138
+ if (!this.#current(controller, generation))
139
+ return;
140
+ this.#apply(nextCwd, nextBranch, nextPullRequest);
141
+ }
142
+ }
143
+ catch {
144
+ if (this.#current(controller, generation))
145
+ this.#apply(this.#ports.fallbackCwd(), null, null);
146
+ }
147
+ finally {
148
+ if (this.#abort === controller)
149
+ this.#abort = undefined;
150
+ }
151
+ if (!this.#current(controller, generation))
152
+ return;
153
+ this.#timer = setTimeout(() => {
154
+ this.#timer = undefined;
155
+ this.refresh();
156
+ }, this.#pollMs);
157
+ this.#timer.unref?.();
158
+ }
159
+ #apply(cwd, branch, pullRequest) {
160
+ const changed = cwd !== this.#cwd || branch !== this.#branch || !samePullRequest(this.#pullRequest, pullRequest);
161
+ this.#cwd = cwd;
162
+ this.#branch = branch;
163
+ this.#pullRequest = pullRequest;
164
+ this.#lastPullRequestProbeAt = Date.now();
165
+ if (changed)
166
+ this.#ports.changed();
167
+ }
168
+ }
169
+ function samePullRequest(left, right) {
170
+ return left?.number === right?.number && left?.url === right?.url;
171
+ }
172
+ async function readGitBranch(cwd, signal) {
173
+ if (!existsSync(cwd))
174
+ return null;
175
+ try {
176
+ const { stdout } = await execFileAsync("git", ["branch", "--show-current"], {
177
+ cwd,
178
+ windowsHide: true,
179
+ timeout: GIT_BRANCH_TIMEOUT_MS,
180
+ maxBuffer: GIT_BRANCH_MAX_BUFFER_BYTES,
181
+ signal,
182
+ encoding: "utf8",
183
+ });
184
+ return stdout.trim() || null;
185
+ }
186
+ catch {
187
+ return null;
188
+ }
189
+ }