@timurproko/a1 0.1.8-dev.0aceebe → 0.1.8-dev.112
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/README.md +41 -31
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/src/cli/dispatch.js +67 -30
- package/dist/src/cli/packages.d.ts +9 -1
- package/dist/src/cli/packages.js +24 -20
- package/dist/src/cli/version-stats.js +4 -4
- package/dist/src/foundation/pi-component-adapter/index.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/index.js +1 -1
- package/dist/src/foundation/pi-component-adapter/shell-presenters-transcript.js +2 -1
- package/dist/src/foundation/pi-engine-adapter/adapter.d.ts +7 -0
- package/dist/src/foundation/pi-engine-adapter/adapter.js +193 -39
- package/dist/src/foundation/pi-owned-ui-integration/session-shell.d.ts +6 -0
- package/dist/src/foundation/pi-owned-ui-integration/session-shell.js +223 -13
- package/dist/src/foundation/release/index.d.ts +0 -1
- package/dist/src/foundation/release/index.js +0 -1
- package/dist/src/foundation/release/update.d.ts +1 -1
- package/dist/src/foundation/release/update.js +13 -15
- package/docs/architecture/toolchain.md +1 -1
- package/docs/ci-release-runbook.md +76 -79
- package/docs/features/launch-profiles.md +1 -1
- package/docs/manual-transparent-checkpoint.md +1 -1
- package/package.json +3 -1
- package/dist/src/foundation/release/development-preview-release.d.ts +0 -49
- package/dist/src/foundation/release/development-preview-release.js +0 -114
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseMouseInput } from "../ui-components/index.js";
|
|
2
2
|
import { PINNED_PI_HIDDEN_COMMAND_NAMES, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "../pi-engine-adapter/index.js";
|
|
3
|
-
import { createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellStatus, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, piTheme, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../pi-component-adapter/index.js";
|
|
3
|
+
import { createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellStatus, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, onPiThemeChange, piTheme, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../pi-component-adapter/index.js";
|
|
4
4
|
import { PiTuiRuntimeAdapter, } from "../pi-tui-runtime-adapter/index.js";
|
|
5
5
|
export class OwnedUiSessionShellRoot {
|
|
6
6
|
editor;
|
|
@@ -8,6 +8,9 @@ export class OwnedUiSessionShellRoot {
|
|
|
8
8
|
resources;
|
|
9
9
|
#cwd;
|
|
10
10
|
#transcript = new Map();
|
|
11
|
+
#blocksById = new Map();
|
|
12
|
+
#renderedRows = new Map();
|
|
13
|
+
#themeUnsubscribe;
|
|
11
14
|
#transcriptOrder = [];
|
|
12
15
|
#view;
|
|
13
16
|
#status;
|
|
@@ -54,6 +57,9 @@ export class OwnedUiSessionShellRoot {
|
|
|
54
57
|
this.editor.addToHistory(block.text);
|
|
55
58
|
}
|
|
56
59
|
this.#syncTranscript(view.transcript);
|
|
60
|
+
// Colours come from the active theme, so rendered rows outlive their revision only
|
|
61
|
+
// until the theme under them changes.
|
|
62
|
+
this.#themeUnsubscribe = onPiThemeChange(() => this.#renderedRows.clear());
|
|
57
63
|
}
|
|
58
64
|
update(view) {
|
|
59
65
|
this.#view = view;
|
|
@@ -65,6 +71,25 @@ export class OwnedUiSessionShellRoot {
|
|
|
65
71
|
this.editor.setThinkingLevel(view.thinkingLevel);
|
|
66
72
|
this.invalidate();
|
|
67
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Applies one block: its component is created or updated in place and the order grows
|
|
76
|
+
* only when the block is new. A streamed chunk costs one component update rather than a
|
|
77
|
+
* walk of the whole transcript.
|
|
78
|
+
*/
|
|
79
|
+
applyTranscriptBlock(block) {
|
|
80
|
+
this.#blocksById.set(block.id, block);
|
|
81
|
+
const component = this.#transcript.get(block.id);
|
|
82
|
+
if (component === undefined) {
|
|
83
|
+
const created = createPiShellTranscriptComponent(block, this.#cwd, this.#extensionRenderers);
|
|
84
|
+
created.setExpanded(this.#toolsExpanded);
|
|
85
|
+
this.#transcript.set(block.id, created);
|
|
86
|
+
this.#transcriptOrder.push(block.id);
|
|
87
|
+
}
|
|
88
|
+
else if (component.revision !== block.revision) {
|
|
89
|
+
component.update(block);
|
|
90
|
+
}
|
|
91
|
+
this.invalidate();
|
|
92
|
+
}
|
|
68
93
|
render(width) {
|
|
69
94
|
const queued = this.#view.editor.queuedSubmissions.length === 0 ? [] : this.#queued.render(width);
|
|
70
95
|
return [
|
|
@@ -128,10 +153,10 @@ export class OwnedUiSessionShellRoot {
|
|
|
128
153
|
}
|
|
129
154
|
#renderDocument(width) {
|
|
130
155
|
const transcript = this.#transcriptOrder.flatMap((id, index) => {
|
|
131
|
-
const block = this.#
|
|
156
|
+
const block = this.#blocksById.get(id);
|
|
132
157
|
if (!this.#thinkingVisible && block?.kind === "thinking")
|
|
133
158
|
return [];
|
|
134
|
-
const rows = this.#
|
|
159
|
+
const rows = this.#blockRows(id, block, width);
|
|
135
160
|
if (index > 0 && block?.kind === "user")
|
|
136
161
|
return ["", ...rows];
|
|
137
162
|
return rows;
|
|
@@ -167,6 +192,25 @@ export class OwnedUiSessionShellRoot {
|
|
|
167
192
|
...diagnosticRows,
|
|
168
193
|
];
|
|
169
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Rows for one transcript block. A finalized block renders once for a given revision
|
|
197
|
+
* and width and is reused after that, so a frame costs what changed rather than what
|
|
198
|
+
* the session has accumulated. A live block, and anything the shell drives itself, is
|
|
199
|
+
* rendered every time because its content is still moving.
|
|
200
|
+
*/
|
|
201
|
+
#blockRows(id, block, width) {
|
|
202
|
+
const component = this.#transcript.get(id);
|
|
203
|
+
if (component === undefined)
|
|
204
|
+
return [];
|
|
205
|
+
if (block === undefined || block.status !== "finalized")
|
|
206
|
+
return component.render(width);
|
|
207
|
+
const cached = this.#renderedRows.get(id);
|
|
208
|
+
if (cached && cached.width === width && cached.revision === block.revision)
|
|
209
|
+
return cached.rows;
|
|
210
|
+
const rows = component.render(width);
|
|
211
|
+
this.#renderedRows.set(id, { width, revision: block.revision, rows });
|
|
212
|
+
return rows;
|
|
213
|
+
}
|
|
170
214
|
transcriptComponent(id) {
|
|
171
215
|
return this.#transcript.get(id);
|
|
172
216
|
}
|
|
@@ -334,6 +378,8 @@ export class OwnedUiSessionShellRoot {
|
|
|
334
378
|
this.#extensionWorkingMessage = undefined;
|
|
335
379
|
this.#status.setWorkingOverride(undefined);
|
|
336
380
|
this.#footer.update(this.#viewWithExtensionStatuses(this.#view));
|
|
381
|
+
// An extension renderer may have drawn transcript blocks that are now unrendered by it.
|
|
382
|
+
this.#renderedRows.clear();
|
|
337
383
|
this.invalidate();
|
|
338
384
|
}
|
|
339
385
|
addExtensionNotification(message, type) {
|
|
@@ -384,11 +430,13 @@ export class OwnedUiSessionShellRoot {
|
|
|
384
430
|
this.#inputSurface.setFocused?.(focused);
|
|
385
431
|
}
|
|
386
432
|
dispose() {
|
|
433
|
+
this.#themeUnsubscribe();
|
|
387
434
|
this.header.dispose?.();
|
|
388
435
|
this.resources.dispose?.();
|
|
389
436
|
for (const component of this.#transcript.values())
|
|
390
437
|
component.dispose?.();
|
|
391
438
|
this.#transcript.clear();
|
|
439
|
+
this.#renderedRows.clear();
|
|
392
440
|
if (this.#inputSurface !== this.editor)
|
|
393
441
|
this.#inputSurface.dispose?.();
|
|
394
442
|
this.#extensionHeader?.dispose?.();
|
|
@@ -402,12 +450,16 @@ export class OwnedUiSessionShellRoot {
|
|
|
402
450
|
this.#queued.dispose?.();
|
|
403
451
|
}
|
|
404
452
|
#syncTranscript(blocks) {
|
|
453
|
+
this.#blocksById.clear();
|
|
454
|
+
for (const block of blocks)
|
|
455
|
+
this.#blocksById.set(block.id, block);
|
|
405
456
|
const nextIds = new Set(blocks.map(block => block.id));
|
|
406
457
|
for (const [id, component] of this.#transcript) {
|
|
407
458
|
if (id.startsWith("workflow-status-") || nextIds.has(id))
|
|
408
459
|
continue;
|
|
409
460
|
component.dispose?.();
|
|
410
461
|
this.#transcript.delete(id);
|
|
462
|
+
this.#renderedRows.delete(id);
|
|
411
463
|
}
|
|
412
464
|
for (const block of blocks) {
|
|
413
465
|
const component = this.#transcript.get(block.id);
|
|
@@ -431,8 +483,9 @@ export class OwnedUiSessionShellRoot {
|
|
|
431
483
|
if (block !== undefined)
|
|
432
484
|
order.push(block.id);
|
|
433
485
|
}
|
|
486
|
+
const placed = new Set(order);
|
|
434
487
|
for (const statusId of statusIds) {
|
|
435
|
-
if (!
|
|
488
|
+
if (!placed.has(statusId))
|
|
436
489
|
order.push(statusId);
|
|
437
490
|
}
|
|
438
491
|
this.#transcriptOrder = order;
|
|
@@ -498,6 +551,8 @@ export class OwnedUiSessionShellRoot {
|
|
|
498
551
|
this.resources.setExpanded(expanded);
|
|
499
552
|
for (const component of this.#transcript.values())
|
|
500
553
|
component.setExpanded(expanded);
|
|
554
|
+
// Expansion changes what a block draws without changing its revision.
|
|
555
|
+
this.#renderedRows.clear();
|
|
501
556
|
this.invalidate();
|
|
502
557
|
}
|
|
503
558
|
}
|
|
@@ -525,12 +580,14 @@ function shellResourceEntries(backend) {
|
|
|
525
580
|
sourcePath: resource.sourcePath,
|
|
526
581
|
diagnostic: resource.diagnostic,
|
|
527
582
|
}));
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
583
|
+
const extensions = backend.extensionResources().filter(extension => !extension.hidden);
|
|
584
|
+
const loadedExtensions = extensions.filter(extension => extension.diagnostic === null);
|
|
585
|
+
const extensionLabels = compactExtensionLabels(loadedExtensions);
|
|
586
|
+
for (const extension of extensions) {
|
|
587
|
+
const labelIndex = loadedExtensions.indexOf(extension);
|
|
531
588
|
resources.push({
|
|
532
589
|
section: "Extensions",
|
|
533
|
-
label: compactResourceLabel(extension.sourcePath ?? extension.resolvedPath ?? "extension"),
|
|
590
|
+
label: extensionLabels[labelIndex] ?? compactResourceLabel(extension.sourcePath ?? extension.resolvedPath ?? "extension"),
|
|
534
591
|
sourcePath: extension.sourcePath ?? extension.resolvedPath,
|
|
535
592
|
diagnostic: extension.diagnostic,
|
|
536
593
|
});
|
|
@@ -538,12 +595,129 @@ function shellResourceEntries(backend) {
|
|
|
538
595
|
return resources;
|
|
539
596
|
}
|
|
540
597
|
function compactResourceLabel(path) {
|
|
541
|
-
const segments = path
|
|
598
|
+
const segments = compactPathSegments(path);
|
|
542
599
|
const leaf = segments.at(-1) ?? path;
|
|
543
600
|
if ((leaf === "index.ts" || leaf === "index.js") && segments.length > 1)
|
|
544
601
|
return segments.at(-2) ?? leaf;
|
|
545
602
|
return leaf;
|
|
546
603
|
}
|
|
604
|
+
/**
|
|
605
|
+
* Pinned from InteractiveMode's compact extension-label helpers at Pi commit
|
|
606
|
+
* 914cf1472e715297caa30db4b9535d534a9eb718. The source metadata crosses an
|
|
607
|
+
* A1-owned boundary first; the owned shell never inspects Pi's private root.
|
|
608
|
+
*/
|
|
609
|
+
function compactExtensionLabels(extensions) {
|
|
610
|
+
const localExtensions = extensions
|
|
611
|
+
.filter(extension => !isPackageExtensionSource(extension.sourceInfo))
|
|
612
|
+
.map(extension => {
|
|
613
|
+
const path = extension.sourcePath ?? extension.resolvedPath ?? "extension";
|
|
614
|
+
const segments = compactPathSegments(path);
|
|
615
|
+
if (segments.length > 1 && (segments.at(-1) === "index.ts" || segments.at(-1) === "index.js"))
|
|
616
|
+
segments.pop();
|
|
617
|
+
return { extension, segments };
|
|
618
|
+
});
|
|
619
|
+
return extensions.map(extension => {
|
|
620
|
+
const resourcePath = extension.sourcePath ?? extension.resolvedPath ?? "extension";
|
|
621
|
+
if (isPackageExtensionSource(extension.sourceInfo)) {
|
|
622
|
+
return compactPackageExtensionLabel(resourcePath, extension.sourceInfo);
|
|
623
|
+
}
|
|
624
|
+
const localIndex = localExtensions.findIndex(item => item.extension === extension);
|
|
625
|
+
const segments = localExtensions[localIndex]?.segments;
|
|
626
|
+
if (!segments || segments.length === 0)
|
|
627
|
+
return compactResourceLabel(resourcePath);
|
|
628
|
+
for (let count = 1; count <= segments.length; count += 1) {
|
|
629
|
+
const candidate = segments.slice(-count).join("/");
|
|
630
|
+
if (localExtensions.every((item, itemIndex) => itemIndex === localIndex || item.segments.slice(-count).join("/") !== candidate)) {
|
|
631
|
+
return candidate;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return segments.join("/");
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
function compactPackageExtensionLabel(resourcePath, sourceInfo) {
|
|
638
|
+
const sourceLabel = compactPackageSourceLabel(sourceInfo.source);
|
|
639
|
+
if (!sourceLabel)
|
|
640
|
+
return compactResourceLabel(resourcePath);
|
|
641
|
+
const shortPath = shortPackagePath(resourcePath, sourceInfo).replaceAll("\\", "/");
|
|
642
|
+
const packagePath = shortPath.startsWith("extensions/") ? shortPath.slice("extensions/".length) : shortPath;
|
|
643
|
+
const slash = packagePath.lastIndexOf("/");
|
|
644
|
+
const fileName = slash < 0 ? packagePath : packagePath.slice(slash + 1);
|
|
645
|
+
const directory = slash < 0 ? "" : packagePath.slice(0, slash);
|
|
646
|
+
const extension = fileName.lastIndexOf(".");
|
|
647
|
+
const name = extension <= 0 ? fileName : fileName.slice(0, extension);
|
|
648
|
+
if (name === "index")
|
|
649
|
+
return !directory || directory === "." ? sourceLabel : `${sourceLabel}:${directory}`;
|
|
650
|
+
return `${sourceLabel}:${packagePath}`;
|
|
651
|
+
}
|
|
652
|
+
function compactPackageSourceLabel(source) {
|
|
653
|
+
if (source.startsWith("npm:"))
|
|
654
|
+
return source.slice("npm:".length) || source;
|
|
655
|
+
if (!source.startsWith("git:"))
|
|
656
|
+
return source;
|
|
657
|
+
const gitSource = source.slice("git:".length).trim();
|
|
658
|
+
let repositoryPath;
|
|
659
|
+
const scpLike = gitSource.match(/^git@[^:]+:(.+)$/);
|
|
660
|
+
if (scpLike?.[1]) {
|
|
661
|
+
repositoryPath = scpLike[1];
|
|
662
|
+
}
|
|
663
|
+
else if (/^[a-z]+:\/\//i.test(gitSource)) {
|
|
664
|
+
try {
|
|
665
|
+
repositoryPath = new URL(gitSource).pathname.replace(/^\/+/, "");
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
return source;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
const slash = gitSource.indexOf("/");
|
|
673
|
+
if (slash >= 0)
|
|
674
|
+
repositoryPath = gitSource.slice(slash + 1);
|
|
675
|
+
}
|
|
676
|
+
if (!repositoryPath)
|
|
677
|
+
return source;
|
|
678
|
+
const ref = repositoryPath.indexOf("@");
|
|
679
|
+
const withoutRef = ref < 0 ? repositoryPath : repositoryPath.slice(0, ref);
|
|
680
|
+
return withoutRef.replace(/\.git$/, "") || source;
|
|
681
|
+
}
|
|
682
|
+
function shortPackagePath(resourcePath, sourceInfo) {
|
|
683
|
+
const fullPath = normalizeResourcePath(resourcePath);
|
|
684
|
+
const baseDir = sourceInfo.baseDir === null ? undefined : normalizeResourcePath(sourceInfo.baseDir).replace(/\/$/, "");
|
|
685
|
+
if (baseDir) {
|
|
686
|
+
const npmRoot = baseDir.match(/^(.*\/node_modules)\/(@?[^/]+(?:\/[^/]+)?)$/);
|
|
687
|
+
if (npmRoot?.[1] && fullPath.startsWith(`${npmRoot[1]}/`))
|
|
688
|
+
return relativeResourcePath(baseDir, fullPath);
|
|
689
|
+
if (fullPath === baseDir)
|
|
690
|
+
return ".";
|
|
691
|
+
if (fullPath.startsWith(`${baseDir}/`))
|
|
692
|
+
return fullPath.slice(baseDir.length + 1);
|
|
693
|
+
}
|
|
694
|
+
const npmMatch = fullPath.match(/node_modules\/(@?[^/]+(?:\/[^/]+)?)\/(.*)/);
|
|
695
|
+
if (npmMatch?.[2] && sourceInfo.source.startsWith("npm:"))
|
|
696
|
+
return npmMatch[2];
|
|
697
|
+
const gitMatch = fullPath.match(/git\/[^/]+\/[^/]+\/(.*)/);
|
|
698
|
+
if (gitMatch?.[1] && sourceInfo.source.startsWith("git:"))
|
|
699
|
+
return gitMatch[1];
|
|
700
|
+
return resourcePath;
|
|
701
|
+
}
|
|
702
|
+
function relativeResourcePath(from, to) {
|
|
703
|
+
const fromSegments = from.split("/").filter(Boolean);
|
|
704
|
+
const toSegments = to.split("/").filter(Boolean);
|
|
705
|
+
let common = 0;
|
|
706
|
+
while (common < fromSegments.length && common < toSegments.length
|
|
707
|
+
&& fromSegments[common]?.toLowerCase() === toSegments[common]?.toLowerCase())
|
|
708
|
+
common += 1;
|
|
709
|
+
return [...fromSegments.slice(common).map(() => ".."), ...toSegments.slice(common)].join("/") || ".";
|
|
710
|
+
}
|
|
711
|
+
function compactPathSegments(path) {
|
|
712
|
+
return normalizeResourcePath(path).split("/").filter(segment => segment.length > 0 && segment !== "~");
|
|
713
|
+
}
|
|
714
|
+
function normalizeResourcePath(path) {
|
|
715
|
+
return path.replaceAll("\\", "/");
|
|
716
|
+
}
|
|
717
|
+
function isPackageExtensionSource(sourceInfo) {
|
|
718
|
+
const source = sourceInfo?.source ?? "";
|
|
719
|
+
return source.startsWith("npm:") || source.startsWith("git:");
|
|
720
|
+
}
|
|
547
721
|
export class OwnedUiSessionShell {
|
|
548
722
|
backend;
|
|
549
723
|
root;
|
|
@@ -560,6 +734,7 @@ export class OwnedUiSessionShell {
|
|
|
560
734
|
#sequence = 0;
|
|
561
735
|
#started = false;
|
|
562
736
|
#disposed = false;
|
|
737
|
+
#pointerReporting = false;
|
|
563
738
|
#compactionQueue = [];
|
|
564
739
|
#lastClearTime = 0;
|
|
565
740
|
#activeLoginDialog;
|
|
@@ -638,8 +813,13 @@ export class OwnedUiSessionShell {
|
|
|
638
813
|
});
|
|
639
814
|
this.root.editor.setAutocompleteCommands(this.backend.workflowAutocompleteCommands());
|
|
640
815
|
this.#unsubscribe = this.backend.onEvent(event => {
|
|
641
|
-
|
|
642
|
-
|
|
816
|
+
// A streamed chunk names one block, and touching only that block is what keeps the
|
|
817
|
+
// cost of a chunk the same in a long session as in a new one. Everything else
|
|
818
|
+
// resynchronizes the view, which is cheap next to re-reading the transcript.
|
|
819
|
+
const view = event.type === "transcript-block" && this.#sessionGeneration === this.backend.sessionGeneration
|
|
820
|
+
? this.#syncBlock(event.block)
|
|
821
|
+
: this.#syncView();
|
|
822
|
+
if (view.lifecycle === "ready" && this.#compactionQueue.length > 0)
|
|
643
823
|
void this.#flushCompactionQueue();
|
|
644
824
|
if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
|
|
645
825
|
this.#resolveStopped?.();
|
|
@@ -1232,22 +1412,51 @@ export class OwnedUiSessionShell {
|
|
|
1232
1412
|
this.root.appendDaxnuts();
|
|
1233
1413
|
}
|
|
1234
1414
|
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Turns terminal pointer reporting on for a screen that reads the pointer, and off for
|
|
1417
|
+
* every path that ends it. While it is on the terminal hands A1 the wheel and the
|
|
1418
|
+
* button instead of scrolling and selecting itself, so leaving it on outlives the
|
|
1419
|
+
* screen that wanted it and takes the terminal's own scrolling and selection with it.
|
|
1420
|
+
*/
|
|
1421
|
+
#setPointerReporting(enabled) {
|
|
1422
|
+
if (this.#pointerReporting === enabled)
|
|
1423
|
+
return;
|
|
1424
|
+
this.#pointerReporting = enabled;
|
|
1425
|
+
if (!this.runtime.active)
|
|
1426
|
+
return;
|
|
1427
|
+
this.runtime.writeControl(enabled ? MOUSE_TRACKING_ON : MOUSE_TRACKING_OFF);
|
|
1428
|
+
}
|
|
1235
1429
|
async dispose() {
|
|
1236
1430
|
if (this.#disposed)
|
|
1237
1431
|
return;
|
|
1238
1432
|
this.#disposed = true;
|
|
1433
|
+
this.#setPointerReporting(false);
|
|
1239
1434
|
this.#unsubscribe();
|
|
1240
1435
|
this.#dialogHandle?.hide();
|
|
1241
1436
|
await this.backend.unbindExtensionUi();
|
|
1242
1437
|
this.#extensionBridge.dispose();
|
|
1243
1438
|
await this.runtime.dispose();
|
|
1244
1439
|
}
|
|
1440
|
+
/**
|
|
1441
|
+
* Applies one transcript block without re-reading the session. The listeners still hear
|
|
1442
|
+
* the view they would have heard, so nothing downstream can tell the difference.
|
|
1443
|
+
*/
|
|
1444
|
+
#syncBlock(block) {
|
|
1445
|
+
this.root.applyTranscriptBlock(block);
|
|
1446
|
+
this.runtime.requestRender();
|
|
1447
|
+
const view = this.view();
|
|
1448
|
+
for (const listener of this.#listeners)
|
|
1449
|
+
listener(view);
|
|
1450
|
+
return view;
|
|
1451
|
+
}
|
|
1245
1452
|
#syncView() {
|
|
1246
1453
|
const view = this.view();
|
|
1247
1454
|
if (this.backend.sessionGeneration !== this.#sessionGeneration) {
|
|
1248
1455
|
this.#sessionGeneration = this.backend.sessionGeneration;
|
|
1249
1456
|
this.#activeLoginDialog = undefined;
|
|
1250
1457
|
this.#extensionBridge.reset();
|
|
1458
|
+
// A replaced session takes its screens with it, pointer reporting included.
|
|
1459
|
+
this.#setPointerReporting(false);
|
|
1251
1460
|
this.root.setInputSurface(null);
|
|
1252
1461
|
this.root.resetExtensionUi();
|
|
1253
1462
|
this.root.resetWorkflowPresentation();
|
|
@@ -1257,6 +1466,7 @@ export class OwnedUiSessionShell {
|
|
|
1257
1466
|
this.runtime.requestRender();
|
|
1258
1467
|
for (const listener of this.#listeners)
|
|
1259
1468
|
listener(view);
|
|
1469
|
+
return view;
|
|
1260
1470
|
}
|
|
1261
1471
|
#openOwnedRoute(route) {
|
|
1262
1472
|
const surface = this.#routeHost?.open(route) ?? null;
|
|
@@ -1267,7 +1477,7 @@ export class OwnedUiSessionShell {
|
|
|
1267
1477
|
this.#dialogHandle?.hide();
|
|
1268
1478
|
// Any-event reporting: hover and drag are what the screen is driven by, and
|
|
1269
1479
|
// it also stops the terminal treating a drag as a text selection.
|
|
1270
|
-
this
|
|
1480
|
+
this.#setPointerReporting(true);
|
|
1271
1481
|
// The interrupt chord is global, so it is watched on raw input rather than
|
|
1272
1482
|
// through the overlay: the pinned shell handles that key before an overlay
|
|
1273
1483
|
// ever sees it, which is why an owned screen must not rely on being asked.
|
|
@@ -1290,7 +1500,7 @@ export class OwnedUiSessionShell {
|
|
|
1290
1500
|
});
|
|
1291
1501
|
const closeSurface = () => {
|
|
1292
1502
|
removeInterruptWatch();
|
|
1293
|
-
this
|
|
1503
|
+
this.#setPointerReporting(false);
|
|
1294
1504
|
this.#dialogHandle?.hide();
|
|
1295
1505
|
this.#dialogHandle = undefined;
|
|
1296
1506
|
this.#dialogId = undefined;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export * from "./bootstrap.js";
|
|
2
2
|
export * from "./cohort-selection.js";
|
|
3
3
|
export * from "./cohort-state.js";
|
|
4
|
-
export * from "./development-preview-release.js";
|
|
5
4
|
export * from "./process-cleanup.js";
|
|
6
5
|
export * from "./release.js";
|
|
7
6
|
export * from "./release-gc.js";
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export * from "./bootstrap.js";
|
|
2
2
|
export * from "./cohort-selection.js";
|
|
3
3
|
export * from "./cohort-state.js";
|
|
4
|
-
export * from "./development-preview-release.js";
|
|
5
4
|
export * from "./process-cleanup.js";
|
|
6
5
|
export * from "./release.js";
|
|
7
6
|
export * from "./release-gc.js";
|
|
@@ -33,7 +33,7 @@ export interface UpdatePerformanceEvidence {
|
|
|
33
33
|
export interface SelfUpdateOptions {
|
|
34
34
|
packageRoot: string;
|
|
35
35
|
channel?: UpdateChannel;
|
|
36
|
-
/** A specific preview to install, named by its
|
|
36
|
+
/** A specific preview to install, named by its development number or full version. */
|
|
37
37
|
target?: string;
|
|
38
38
|
environment?: NodeJS.ProcessEnv;
|
|
39
39
|
fileSystem?: UpdateFileSystem;
|
|
@@ -20,7 +20,7 @@ const UPDATE_DIST_TAGS = { stable: "latest", next: "next" };
|
|
|
20
20
|
* a release, and that is the word the repository, its tags, and its GitHub
|
|
21
21
|
* releases all use.
|
|
22
22
|
*/
|
|
23
|
-
const UPDATE_CHANNEL_LABELS = { stable: "release", next: "
|
|
23
|
+
const UPDATE_CHANNEL_LABELS = { stable: "release", next: "develop" };
|
|
24
24
|
const defaultFileSystem = {
|
|
25
25
|
async readFile(path) { return await readFile(path, "utf8"); },
|
|
26
26
|
realpath,
|
|
@@ -223,12 +223,13 @@ function createUpdateProgress(output, enabled) {
|
|
|
223
223
|
}
|
|
224
224
|
/** The newest version the channel points at, which is what an unqualified update takes. */
|
|
225
225
|
async function resolveChannelHead(runner, distTag, output) {
|
|
226
|
-
const
|
|
226
|
+
const publicChannel = distTag === "next" ? "development" : "release";
|
|
227
|
+
const lookup = await runNpm(runner, ["view", `${PRODUCT_PACKAGE}@${distTag}`, "version"], true, output, `query the npm ${publicChannel} channel`);
|
|
227
228
|
if (lookup.result === null)
|
|
228
229
|
return { version: null, exitCode: lookup.exitCode };
|
|
229
230
|
const version = validSemver(lookup.result.stdout.trim());
|
|
230
231
|
if (version === null) {
|
|
231
|
-
output.stderr(`${PRODUCT_TEXT.diagnostic(`received a malformed ${
|
|
232
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic(`received a malformed ${publicChannel} channel version from npm: ${JSON.stringify(lookup.result.stdout.trim())}.`)}\n`);
|
|
232
233
|
return { version: null, exitCode: 1 };
|
|
233
234
|
}
|
|
234
235
|
return { version, exitCode: 0 };
|
|
@@ -236,14 +237,10 @@ async function resolveChannelHead(runner, distTag, output) {
|
|
|
236
237
|
/**
|
|
237
238
|
* Resolve a preview the caller named.
|
|
238
239
|
*
|
|
239
|
-
* A preview is published as `<version>-dev.<
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
* The published list is the authority: naming a commit that was never published,
|
|
245
|
-
* or one published more than once under different versions, is an error rather
|
|
246
|
-
* than a guess.
|
|
240
|
+
* A preview is published as `<version>-dev.<pull-request-number>`, so its decimal
|
|
241
|
+
* development number is enough to identify it. A full version is accepted too.
|
|
242
|
+
* The published list remains authoritative: nothing is constructed from the
|
|
243
|
+
* installed package's base version.
|
|
247
244
|
*/
|
|
248
245
|
async function resolveRequestedPreview(runner, requested, output) {
|
|
249
246
|
const lookup = await runNpm(runner, ["view", PRODUCT_PACKAGE, "versions", "--json"], true, output, "list the published versions");
|
|
@@ -261,8 +258,7 @@ async function resolveRequestedPreview(runner, requested, output) {
|
|
|
261
258
|
const exact = versions.find(version => version === requested);
|
|
262
259
|
if (exact !== undefined) {
|
|
263
260
|
// Naming a release here would install it through the preview path, which is a
|
|
264
|
-
// different command with a different meaning.
|
|
265
|
-
// one, so only the fuller spelling of a preview reaches this.
|
|
261
|
+
// different command with a different meaning.
|
|
266
262
|
if (!exact.includes("-dev.")) {
|
|
267
263
|
output.stderr(`${PRODUCT_TEXT.diagnostic(`${exact} is a release, not a preview; run ${PRODUCT_TEXT.commandName} update to move to the current release.`)}\n`);
|
|
268
264
|
return { version: null, exitCode: 1 };
|
|
@@ -314,7 +310,9 @@ export async function runSelfUpdate(options) {
|
|
|
314
310
|
if (resolved.version === null)
|
|
315
311
|
return resolved.exitCode;
|
|
316
312
|
const targetVersion = resolved.version;
|
|
317
|
-
|
|
313
|
+
// No full stop after a version: it already ends in a dot-separated identifier,
|
|
314
|
+
// and a trailing one reads as part of the version rather than as punctuation.
|
|
315
|
+
output.stdout(`${PRODUCT_TEXT.commandName} update (${UPDATE_CHANNEL_LABELS[channel]}): ${runningVersion} → ${targetVersion}\n`);
|
|
318
316
|
const progress = createUpdateProgress(output, options.progress ?? (options.output === undefined && process.stdout.isTTY === true));
|
|
319
317
|
const rootLookup = await measure("global-root", async () => await runNpm(runner, ["root", "--global"], true, output, "resolve npm's global package root"));
|
|
320
318
|
if (rootLookup.result === null)
|
|
@@ -403,7 +401,7 @@ export async function runSelfUpdate(options) {
|
|
|
403
401
|
await transactionStore.clearCompleted();
|
|
404
402
|
options.onPhaseTiming?.({ phase: "transaction-complete", durationMs: Math.max(0, now() - transactionStartedAt) });
|
|
405
403
|
progress.finish();
|
|
406
|
-
output.stdout(`${PRODUCT_TEXT.commandName} updated successfully: ${targetVersion}
|
|
404
|
+
output.stdout(`${PRODUCT_TEXT.commandName} updated successfully: ${targetVersion}\n`);
|
|
407
405
|
return 0;
|
|
408
406
|
}
|
|
409
407
|
catch (error) {
|
|
@@ -71,7 +71,7 @@ Presentation acceptance is the reader comparing `a1 pi` with pinned Pi. `node sc
|
|
|
71
71
|
|
|
72
72
|
## Publication
|
|
73
73
|
|
|
74
|
-
One workflow publishes both channels: `.github/workflows/release.yml
|
|
74
|
+
One workflow publishes both channels: `.github/workflows/release.yml`. Pushes do not publish. Nightly development verification runs at `03:17 UTC`; `npm run develop` explicitly requests a numbered preview and `npm run release` explicitly requests stable publication after its version pull request merges. A preview is stamped as `-dev.<merged pull-request number>` and uses npm `next` only as an internal dist-tag. New candidates pack once and are validated on Windows, Linux, and macOS; a repeated nightly verifies the exact immutable registry tarball. All publication uses provenance from the `npm-publish` environment, a preview never changes `latest`, and the stable tag, GitHub Release, and `master` are written only after npm has the package. One global non-cancelling concurrency group serializes the final registry check.
|
|
75
75
|
|
|
76
76
|
`docs/ci-release-runbook.md` is the operational reference.
|
|
77
77
|
|