@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.
- package/dist/app/session-shell/prompt-submission.d.ts +1 -1
- package/dist/app/session-shell/prompt-submission.js +9 -9
- package/dist/app/session-shell/resource-labels.d.ts +2 -0
- package/dist/app/session-shell/resource-labels.js +158 -0
- package/dist/app/session-shell/session-prompt-history.d.ts +29 -0
- package/dist/app/session-shell/session-prompt-history.js +80 -0
- package/dist/app/session-shell/session-selectors.d.ts +46 -0
- package/dist/app/session-shell/session-selectors.js +269 -0
- package/dist/app/session-shell/session-shell-root.d.ts +6 -28
- package/dist/app/session-shell/session-shell-root.js +26 -220
- package/dist/app/session-shell/session-shell.d.ts +3 -23
- package/dist/app/session-shell/session-shell.js +54 -365
- package/dist/app/session-shell/session-viewport-controller.d.ts +2 -10
- package/dist/app/session-shell/session-viewport-controller.js +40 -318
- package/dist/app/session-shell/settings-bindings.d.ts +1 -1
- package/dist/app/session-shell/settings-bindings.js +2 -2
- package/dist/app/session-shell/viewport-pointer.d.ts +83 -0
- package/dist/app/session-shell/viewport-pointer.js +361 -0
- package/dist/composition/owned-ui.js +63 -59
- package/dist/contracts/owned-ui/index.d.ts +1 -1
- package/dist/contracts/owned-ui/index.js +1 -1
- package/dist/contracts/owned-ui/validation.d.ts +1 -11
- package/dist/contracts/owned-ui/validation.js +8 -104
- package/dist/contracts/presentation/index.d.ts +1 -1
- package/dist/contracts/presentation/terminal.d.ts +0 -6
- package/dist/features/owned-ui/settings-app.js +13 -208
- package/dist/features/owned-ui/settings-model-dialog.d.ts +41 -0
- package/dist/features/owned-ui/settings-model-dialog.js +194 -0
- package/dist/foundation/release/automatic-update.js +1 -8
- package/dist/foundation/release/dependency-layer.js +3 -3
- package/dist/foundation/release/latest-release.js +1 -1
- package/dist/foundation/release/managed-path.d.ts +10 -0
- package/dist/foundation/release/managed-path.js +26 -0
- package/dist/foundation/release/release-gc.js +6 -8
- package/dist/foundation/release/restart-certification.js +3 -4
- package/dist/foundation/release/update-activation.js +1 -1
- package/dist/foundation/release/update-execution-lease.js +1 -7
- package/dist/foundation/release/update-installer.d.ts +45 -0
- package/dist/foundation/release/update-installer.js +57 -0
- package/dist/foundation/release/update-recovery.js +2 -9
- package/dist/foundation/release/update.d.ts +4 -23
- package/dist/foundation/release/update.js +160 -179
- package/dist/integrations/pi/components/bracketed-paste.d.ts +33 -0
- package/dist/integrations/pi/components/bracketed-paste.js +123 -0
- package/dist/integrations/pi/components/owned-editor-ux.js +46 -140
- package/dist/integrations/pi/engine/adapter.d.ts +1 -8
- package/dist/integrations/pi/engine/adapter.js +6 -49
- package/dist/integrations/pi/engine/host.js +1 -1
- package/dist/integrations/pi/engine/repository-metadata.d.ts +49 -0
- package/dist/integrations/pi/engine/repository-metadata.js +189 -0
- package/dist/integrations/pi/engine/session-runtime.d.ts +6 -12
- package/dist/integrations/pi/engine/session-runtime.js +21 -178
- package/dist/integrations/pi/engine/view-state.d.ts +21 -2
- package/dist/integrations/pi/engine/view-state.js +129 -1
- package/dist/integrations/pi/engine/workflow-runner.d.ts +0 -1
- package/dist/integrations/pi/engine/workflow-runner.js +254 -365
- package/dist/integrations/pi/engine/workflow-share.d.ts +9 -0
- package/dist/integrations/pi/engine/workflow-share.js +168 -0
- package/dist/integrations/pi/engine/workflow-support.d.ts +0 -14
- package/dist/integrations/pi/engine/workflow-support.js +0 -47
- package/dist/integrations/pi/tui-runtime/adapter.d.ts +9 -19
- package/dist/integrations/pi/tui-runtime/adapter.js +35 -144
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +4 -4
- package/dist/integrations/pi/tui-runtime/index.d.ts +0 -1
- package/dist/integrations/pi/tui-runtime/overlay-geometry.d.ts +2 -2
- package/dist/integrations/pi/tui-runtime/presentation-adapter.d.ts +2 -2
- package/dist/ui/components/transcript-viewport-frame.d.ts +120 -0
- package/dist/ui/components/transcript-viewport-frame.js +314 -0
- package/dist/ui/components/transcript-viewport-types.d.ts +147 -0
- package/dist/ui/components/transcript-viewport.d.ts +2 -147
- package/dist/ui/components/transcript-viewport.js +87 -315
- package/package.json +3 -3
- package/dist/integrations/pi/engine/diagnostics-state.d.ts +0 -9
- package/dist/integrations/pi/engine/diagnostics-state.js +0 -34
- package/dist/integrations/pi/engine/usage-view.d.ts +0 -4
- package/dist/integrations/pi/engine/usage-view.js +0 -56
- package/dist/integrations/pi/tui-runtime/contracts.d.ts +0 -22
- /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
|
-
#
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
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.#
|
|
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
|
-
|
|
489
|
-
|
|
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
|
-
|
|
497
|
-
data = data.slice(start + 6);
|
|
482
|
+
data = data.slice(start);
|
|
498
483
|
}
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
531
|
-
|
|
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 =
|
|
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?.(
|
|
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?.(
|
|
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?.(
|
|
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
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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 {
|
|
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
|
-
|
|
291
|
-
const routedModel = routed === undefined ? null : readModel(routed.model);
|
|
292
|
-
return {
|
|
293
|
-
contractVersion: 1,
|
|
276
|
+
return projectSessionView({
|
|
294
277
|
sessionId: this.#sessionId,
|
|
295
|
-
|
|
296
|
-
|
|
278
|
+
state: this.#state,
|
|
279
|
+
engine: this.#engine,
|
|
297
280
|
transcript: this.#projection.snapshot(),
|
|
298
|
-
|
|
299
|
-
|
|
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
|
+
}
|