@timurproko/a1 0.1.7 → 0.1.8-dev.0b6d8cc
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 +24 -13
- package/bin/cli.js +2 -2
- 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.d.ts +2 -1
- package/dist/src/cli/dispatch.js +75 -21
- 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 +141 -31
- package/dist/src/foundation/pi-owned-ui-integration/session-shell.js +189 -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 +2 -0
- package/dist/src/foundation/release/update.js +82 -11
- package/docs/ci-release-runbook.md +4 -2
- package/package.json +1 -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;
|
|
@@ -128,10 +134,10 @@ export class OwnedUiSessionShellRoot {
|
|
|
128
134
|
}
|
|
129
135
|
#renderDocument(width) {
|
|
130
136
|
const transcript = this.#transcriptOrder.flatMap((id, index) => {
|
|
131
|
-
const block = this.#
|
|
137
|
+
const block = this.#blocksById.get(id);
|
|
132
138
|
if (!this.#thinkingVisible && block?.kind === "thinking")
|
|
133
139
|
return [];
|
|
134
|
-
const rows = this.#
|
|
140
|
+
const rows = this.#blockRows(id, block, width);
|
|
135
141
|
if (index > 0 && block?.kind === "user")
|
|
136
142
|
return ["", ...rows];
|
|
137
143
|
return rows;
|
|
@@ -167,6 +173,25 @@ export class OwnedUiSessionShellRoot {
|
|
|
167
173
|
...diagnosticRows,
|
|
168
174
|
];
|
|
169
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Rows for one transcript block. A finalized block renders once for a given revision
|
|
178
|
+
* and width and is reused after that, so a frame costs what changed rather than what
|
|
179
|
+
* the session has accumulated. A live block, and anything the shell drives itself, is
|
|
180
|
+
* rendered every time because its content is still moving.
|
|
181
|
+
*/
|
|
182
|
+
#blockRows(id, block, width) {
|
|
183
|
+
const component = this.#transcript.get(id);
|
|
184
|
+
if (component === undefined)
|
|
185
|
+
return [];
|
|
186
|
+
if (block === undefined || block.status !== "finalized")
|
|
187
|
+
return component.render(width);
|
|
188
|
+
const cached = this.#renderedRows.get(id);
|
|
189
|
+
if (cached && cached.width === width && cached.revision === block.revision)
|
|
190
|
+
return cached.rows;
|
|
191
|
+
const rows = component.render(width);
|
|
192
|
+
this.#renderedRows.set(id, { width, revision: block.revision, rows });
|
|
193
|
+
return rows;
|
|
194
|
+
}
|
|
170
195
|
transcriptComponent(id) {
|
|
171
196
|
return this.#transcript.get(id);
|
|
172
197
|
}
|
|
@@ -334,6 +359,8 @@ export class OwnedUiSessionShellRoot {
|
|
|
334
359
|
this.#extensionWorkingMessage = undefined;
|
|
335
360
|
this.#status.setWorkingOverride(undefined);
|
|
336
361
|
this.#footer.update(this.#viewWithExtensionStatuses(this.#view));
|
|
362
|
+
// An extension renderer may have drawn transcript blocks that are now unrendered by it.
|
|
363
|
+
this.#renderedRows.clear();
|
|
337
364
|
this.invalidate();
|
|
338
365
|
}
|
|
339
366
|
addExtensionNotification(message, type) {
|
|
@@ -384,11 +411,13 @@ export class OwnedUiSessionShellRoot {
|
|
|
384
411
|
this.#inputSurface.setFocused?.(focused);
|
|
385
412
|
}
|
|
386
413
|
dispose() {
|
|
414
|
+
this.#themeUnsubscribe();
|
|
387
415
|
this.header.dispose?.();
|
|
388
416
|
this.resources.dispose?.();
|
|
389
417
|
for (const component of this.#transcript.values())
|
|
390
418
|
component.dispose?.();
|
|
391
419
|
this.#transcript.clear();
|
|
420
|
+
this.#renderedRows.clear();
|
|
392
421
|
if (this.#inputSurface !== this.editor)
|
|
393
422
|
this.#inputSurface.dispose?.();
|
|
394
423
|
this.#extensionHeader?.dispose?.();
|
|
@@ -402,12 +431,16 @@ export class OwnedUiSessionShellRoot {
|
|
|
402
431
|
this.#queued.dispose?.();
|
|
403
432
|
}
|
|
404
433
|
#syncTranscript(blocks) {
|
|
434
|
+
this.#blocksById.clear();
|
|
435
|
+
for (const block of blocks)
|
|
436
|
+
this.#blocksById.set(block.id, block);
|
|
405
437
|
const nextIds = new Set(blocks.map(block => block.id));
|
|
406
438
|
for (const [id, component] of this.#transcript) {
|
|
407
439
|
if (id.startsWith("workflow-status-") || nextIds.has(id))
|
|
408
440
|
continue;
|
|
409
441
|
component.dispose?.();
|
|
410
442
|
this.#transcript.delete(id);
|
|
443
|
+
this.#renderedRows.delete(id);
|
|
411
444
|
}
|
|
412
445
|
for (const block of blocks) {
|
|
413
446
|
const component = this.#transcript.get(block.id);
|
|
@@ -431,8 +464,9 @@ export class OwnedUiSessionShellRoot {
|
|
|
431
464
|
if (block !== undefined)
|
|
432
465
|
order.push(block.id);
|
|
433
466
|
}
|
|
467
|
+
const placed = new Set(order);
|
|
434
468
|
for (const statusId of statusIds) {
|
|
435
|
-
if (!
|
|
469
|
+
if (!placed.has(statusId))
|
|
436
470
|
order.push(statusId);
|
|
437
471
|
}
|
|
438
472
|
this.#transcriptOrder = order;
|
|
@@ -498,6 +532,8 @@ export class OwnedUiSessionShellRoot {
|
|
|
498
532
|
this.resources.setExpanded(expanded);
|
|
499
533
|
for (const component of this.#transcript.values())
|
|
500
534
|
component.setExpanded(expanded);
|
|
535
|
+
// Expansion changes what a block draws without changing its revision.
|
|
536
|
+
this.#renderedRows.clear();
|
|
501
537
|
this.invalidate();
|
|
502
538
|
}
|
|
503
539
|
}
|
|
@@ -525,12 +561,14 @@ function shellResourceEntries(backend) {
|
|
|
525
561
|
sourcePath: resource.sourcePath,
|
|
526
562
|
diagnostic: resource.diagnostic,
|
|
527
563
|
}));
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
564
|
+
const extensions = backend.extensionResources().filter(extension => !extension.hidden);
|
|
565
|
+
const loadedExtensions = extensions.filter(extension => extension.diagnostic === null);
|
|
566
|
+
const extensionLabels = compactExtensionLabels(loadedExtensions);
|
|
567
|
+
for (const extension of extensions) {
|
|
568
|
+
const labelIndex = loadedExtensions.indexOf(extension);
|
|
531
569
|
resources.push({
|
|
532
570
|
section: "Extensions",
|
|
533
|
-
label: compactResourceLabel(extension.sourcePath ?? extension.resolvedPath ?? "extension"),
|
|
571
|
+
label: extensionLabels[labelIndex] ?? compactResourceLabel(extension.sourcePath ?? extension.resolvedPath ?? "extension"),
|
|
534
572
|
sourcePath: extension.sourcePath ?? extension.resolvedPath,
|
|
535
573
|
diagnostic: extension.diagnostic,
|
|
536
574
|
});
|
|
@@ -538,12 +576,129 @@ function shellResourceEntries(backend) {
|
|
|
538
576
|
return resources;
|
|
539
577
|
}
|
|
540
578
|
function compactResourceLabel(path) {
|
|
541
|
-
const segments = path
|
|
579
|
+
const segments = compactPathSegments(path);
|
|
542
580
|
const leaf = segments.at(-1) ?? path;
|
|
543
581
|
if ((leaf === "index.ts" || leaf === "index.js") && segments.length > 1)
|
|
544
582
|
return segments.at(-2) ?? leaf;
|
|
545
583
|
return leaf;
|
|
546
584
|
}
|
|
585
|
+
/**
|
|
586
|
+
* Pinned from InteractiveMode's compact extension-label helpers at Pi commit
|
|
587
|
+
* 914cf1472e715297caa30db4b9535d534a9eb718. The source metadata crosses an
|
|
588
|
+
* A1-owned boundary first; the owned shell never inspects Pi's private root.
|
|
589
|
+
*/
|
|
590
|
+
function compactExtensionLabels(extensions) {
|
|
591
|
+
const localExtensions = extensions
|
|
592
|
+
.filter(extension => !isPackageExtensionSource(extension.sourceInfo))
|
|
593
|
+
.map(extension => {
|
|
594
|
+
const path = extension.sourcePath ?? extension.resolvedPath ?? "extension";
|
|
595
|
+
const segments = compactPathSegments(path);
|
|
596
|
+
if (segments.length > 1 && (segments.at(-1) === "index.ts" || segments.at(-1) === "index.js"))
|
|
597
|
+
segments.pop();
|
|
598
|
+
return { extension, segments };
|
|
599
|
+
});
|
|
600
|
+
return extensions.map(extension => {
|
|
601
|
+
const resourcePath = extension.sourcePath ?? extension.resolvedPath ?? "extension";
|
|
602
|
+
if (isPackageExtensionSource(extension.sourceInfo)) {
|
|
603
|
+
return compactPackageExtensionLabel(resourcePath, extension.sourceInfo);
|
|
604
|
+
}
|
|
605
|
+
const localIndex = localExtensions.findIndex(item => item.extension === extension);
|
|
606
|
+
const segments = localExtensions[localIndex]?.segments;
|
|
607
|
+
if (!segments || segments.length === 0)
|
|
608
|
+
return compactResourceLabel(resourcePath);
|
|
609
|
+
for (let count = 1; count <= segments.length; count += 1) {
|
|
610
|
+
const candidate = segments.slice(-count).join("/");
|
|
611
|
+
if (localExtensions.every((item, itemIndex) => itemIndex === localIndex || item.segments.slice(-count).join("/") !== candidate)) {
|
|
612
|
+
return candidate;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return segments.join("/");
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
function compactPackageExtensionLabel(resourcePath, sourceInfo) {
|
|
619
|
+
const sourceLabel = compactPackageSourceLabel(sourceInfo.source);
|
|
620
|
+
if (!sourceLabel)
|
|
621
|
+
return compactResourceLabel(resourcePath);
|
|
622
|
+
const shortPath = shortPackagePath(resourcePath, sourceInfo).replaceAll("\\", "/");
|
|
623
|
+
const packagePath = shortPath.startsWith("extensions/") ? shortPath.slice("extensions/".length) : shortPath;
|
|
624
|
+
const slash = packagePath.lastIndexOf("/");
|
|
625
|
+
const fileName = slash < 0 ? packagePath : packagePath.slice(slash + 1);
|
|
626
|
+
const directory = slash < 0 ? "" : packagePath.slice(0, slash);
|
|
627
|
+
const extension = fileName.lastIndexOf(".");
|
|
628
|
+
const name = extension <= 0 ? fileName : fileName.slice(0, extension);
|
|
629
|
+
if (name === "index")
|
|
630
|
+
return !directory || directory === "." ? sourceLabel : `${sourceLabel}:${directory}`;
|
|
631
|
+
return `${sourceLabel}:${packagePath}`;
|
|
632
|
+
}
|
|
633
|
+
function compactPackageSourceLabel(source) {
|
|
634
|
+
if (source.startsWith("npm:"))
|
|
635
|
+
return source.slice("npm:".length) || source;
|
|
636
|
+
if (!source.startsWith("git:"))
|
|
637
|
+
return source;
|
|
638
|
+
const gitSource = source.slice("git:".length).trim();
|
|
639
|
+
let repositoryPath;
|
|
640
|
+
const scpLike = gitSource.match(/^git@[^:]+:(.+)$/);
|
|
641
|
+
if (scpLike?.[1]) {
|
|
642
|
+
repositoryPath = scpLike[1];
|
|
643
|
+
}
|
|
644
|
+
else if (/^[a-z]+:\/\//i.test(gitSource)) {
|
|
645
|
+
try {
|
|
646
|
+
repositoryPath = new URL(gitSource).pathname.replace(/^\/+/, "");
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
return source;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
const slash = gitSource.indexOf("/");
|
|
654
|
+
if (slash >= 0)
|
|
655
|
+
repositoryPath = gitSource.slice(slash + 1);
|
|
656
|
+
}
|
|
657
|
+
if (!repositoryPath)
|
|
658
|
+
return source;
|
|
659
|
+
const ref = repositoryPath.indexOf("@");
|
|
660
|
+
const withoutRef = ref < 0 ? repositoryPath : repositoryPath.slice(0, ref);
|
|
661
|
+
return withoutRef.replace(/\.git$/, "") || source;
|
|
662
|
+
}
|
|
663
|
+
function shortPackagePath(resourcePath, sourceInfo) {
|
|
664
|
+
const fullPath = normalizeResourcePath(resourcePath);
|
|
665
|
+
const baseDir = sourceInfo.baseDir === null ? undefined : normalizeResourcePath(sourceInfo.baseDir).replace(/\/$/, "");
|
|
666
|
+
if (baseDir) {
|
|
667
|
+
const npmRoot = baseDir.match(/^(.*\/node_modules)\/(@?[^/]+(?:\/[^/]+)?)$/);
|
|
668
|
+
if (npmRoot?.[1] && fullPath.startsWith(`${npmRoot[1]}/`))
|
|
669
|
+
return relativeResourcePath(baseDir, fullPath);
|
|
670
|
+
if (fullPath === baseDir)
|
|
671
|
+
return ".";
|
|
672
|
+
if (fullPath.startsWith(`${baseDir}/`))
|
|
673
|
+
return fullPath.slice(baseDir.length + 1);
|
|
674
|
+
}
|
|
675
|
+
const npmMatch = fullPath.match(/node_modules\/(@?[^/]+(?:\/[^/]+)?)\/(.*)/);
|
|
676
|
+
if (npmMatch?.[2] && sourceInfo.source.startsWith("npm:"))
|
|
677
|
+
return npmMatch[2];
|
|
678
|
+
const gitMatch = fullPath.match(/git\/[^/]+\/[^/]+\/(.*)/);
|
|
679
|
+
if (gitMatch?.[1] && sourceInfo.source.startsWith("git:"))
|
|
680
|
+
return gitMatch[1];
|
|
681
|
+
return resourcePath;
|
|
682
|
+
}
|
|
683
|
+
function relativeResourcePath(from, to) {
|
|
684
|
+
const fromSegments = from.split("/").filter(Boolean);
|
|
685
|
+
const toSegments = to.split("/").filter(Boolean);
|
|
686
|
+
let common = 0;
|
|
687
|
+
while (common < fromSegments.length && common < toSegments.length
|
|
688
|
+
&& fromSegments[common]?.toLowerCase() === toSegments[common]?.toLowerCase())
|
|
689
|
+
common += 1;
|
|
690
|
+
return [...fromSegments.slice(common).map(() => ".."), ...toSegments.slice(common)].join("/") || ".";
|
|
691
|
+
}
|
|
692
|
+
function compactPathSegments(path) {
|
|
693
|
+
return normalizeResourcePath(path).split("/").filter(segment => segment.length > 0 && segment !== "~");
|
|
694
|
+
}
|
|
695
|
+
function normalizeResourcePath(path) {
|
|
696
|
+
return path.replaceAll("\\", "/");
|
|
697
|
+
}
|
|
698
|
+
function isPackageExtensionSource(sourceInfo) {
|
|
699
|
+
const source = sourceInfo?.source ?? "";
|
|
700
|
+
return source.startsWith("npm:") || source.startsWith("git:");
|
|
701
|
+
}
|
|
547
702
|
export class OwnedUiSessionShell {
|
|
548
703
|
backend;
|
|
549
704
|
root;
|
|
@@ -560,6 +715,7 @@ export class OwnedUiSessionShell {
|
|
|
560
715
|
#sequence = 0;
|
|
561
716
|
#started = false;
|
|
562
717
|
#disposed = false;
|
|
718
|
+
#pointerReporting = false;
|
|
563
719
|
#compactionQueue = [];
|
|
564
720
|
#lastClearTime = 0;
|
|
565
721
|
#activeLoginDialog;
|
|
@@ -638,8 +794,10 @@ export class OwnedUiSessionShell {
|
|
|
638
794
|
});
|
|
639
795
|
this.root.editor.setAutocompleteCommands(this.backend.workflowAutocompleteCommands());
|
|
640
796
|
this.#unsubscribe = this.backend.onEvent(event => {
|
|
641
|
-
|
|
642
|
-
|
|
797
|
+
// One view read per event: the model is built by the backend, and building it
|
|
798
|
+
// twice per streamed chunk is what made a long session cost more per chunk.
|
|
799
|
+
const view = this.#syncView();
|
|
800
|
+
if (view.lifecycle === "ready" && this.#compactionQueue.length > 0)
|
|
643
801
|
void this.#flushCompactionQueue();
|
|
644
802
|
if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
|
|
645
803
|
this.#resolveStopped?.();
|
|
@@ -1232,10 +1390,25 @@ export class OwnedUiSessionShell {
|
|
|
1232
1390
|
this.root.appendDaxnuts();
|
|
1233
1391
|
}
|
|
1234
1392
|
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Turns terminal pointer reporting on for a screen that reads the pointer, and off for
|
|
1395
|
+
* every path that ends it. While it is on the terminal hands A1 the wheel and the
|
|
1396
|
+
* button instead of scrolling and selecting itself, so leaving it on outlives the
|
|
1397
|
+
* screen that wanted it and takes the terminal's own scrolling and selection with it.
|
|
1398
|
+
*/
|
|
1399
|
+
#setPointerReporting(enabled) {
|
|
1400
|
+
if (this.#pointerReporting === enabled)
|
|
1401
|
+
return;
|
|
1402
|
+
this.#pointerReporting = enabled;
|
|
1403
|
+
if (!this.runtime.active)
|
|
1404
|
+
return;
|
|
1405
|
+
this.runtime.writeControl(enabled ? MOUSE_TRACKING_ON : MOUSE_TRACKING_OFF);
|
|
1406
|
+
}
|
|
1235
1407
|
async dispose() {
|
|
1236
1408
|
if (this.#disposed)
|
|
1237
1409
|
return;
|
|
1238
1410
|
this.#disposed = true;
|
|
1411
|
+
this.#setPointerReporting(false);
|
|
1239
1412
|
this.#unsubscribe();
|
|
1240
1413
|
this.#dialogHandle?.hide();
|
|
1241
1414
|
await this.backend.unbindExtensionUi();
|
|
@@ -1248,6 +1421,8 @@ export class OwnedUiSessionShell {
|
|
|
1248
1421
|
this.#sessionGeneration = this.backend.sessionGeneration;
|
|
1249
1422
|
this.#activeLoginDialog = undefined;
|
|
1250
1423
|
this.#extensionBridge.reset();
|
|
1424
|
+
// A replaced session takes its screens with it, pointer reporting included.
|
|
1425
|
+
this.#setPointerReporting(false);
|
|
1251
1426
|
this.root.setInputSurface(null);
|
|
1252
1427
|
this.root.resetExtensionUi();
|
|
1253
1428
|
this.root.resetWorkflowPresentation();
|
|
@@ -1257,6 +1432,7 @@ export class OwnedUiSessionShell {
|
|
|
1257
1432
|
this.runtime.requestRender();
|
|
1258
1433
|
for (const listener of this.#listeners)
|
|
1259
1434
|
listener(view);
|
|
1435
|
+
return view;
|
|
1260
1436
|
}
|
|
1261
1437
|
#openOwnedRoute(route) {
|
|
1262
1438
|
const surface = this.#routeHost?.open(route) ?? null;
|
|
@@ -1267,7 +1443,7 @@ export class OwnedUiSessionShell {
|
|
|
1267
1443
|
this.#dialogHandle?.hide();
|
|
1268
1444
|
// Any-event reporting: hover and drag are what the screen is driven by, and
|
|
1269
1445
|
// it also stops the terminal treating a drag as a text selection.
|
|
1270
|
-
this
|
|
1446
|
+
this.#setPointerReporting(true);
|
|
1271
1447
|
// The interrupt chord is global, so it is watched on raw input rather than
|
|
1272
1448
|
// through the overlay: the pinned shell handles that key before an overlay
|
|
1273
1449
|
// ever sees it, which is why an owned screen must not rely on being asked.
|
|
@@ -1290,7 +1466,7 @@ export class OwnedUiSessionShell {
|
|
|
1290
1466
|
});
|
|
1291
1467
|
const closeSurface = () => {
|
|
1292
1468
|
removeInterruptWatch();
|
|
1293
|
-
this
|
|
1469
|
+
this.#setPointerReporting(false);
|
|
1294
1470
|
this.#dialogHandle?.hide();
|
|
1295
1471
|
this.#dialogHandle = undefined;
|
|
1296
1472
|
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,6 +33,8 @@ 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 commit or its full version. */
|
|
37
|
+
target?: string;
|
|
36
38
|
environment?: NodeJS.ProcessEnv;
|
|
37
39
|
fileSystem?: UpdateFileSystem;
|
|
38
40
|
output?: UpdateOutput;
|
|
@@ -14,6 +14,13 @@ import { materializeRelease, readMaterializedRelease } from "./release-store.js"
|
|
|
14
14
|
import { UpdateTransactionStore } from "./update-transaction.js";
|
|
15
15
|
export const PRODUCT_PACKAGE = PRODUCT_TEXT.packageName;
|
|
16
16
|
const UPDATE_DIST_TAGS = { stable: "latest", next: "next" };
|
|
17
|
+
/**
|
|
18
|
+
* What each channel is called when A1 says it out loud. The internal name stays
|
|
19
|
+
* `stable` because that is what the channel is, but what a reader is moving to is
|
|
20
|
+
* a release, and that is the word the repository, its tags, and its GitHub
|
|
21
|
+
* releases all use.
|
|
22
|
+
*/
|
|
23
|
+
const UPDATE_CHANNEL_LABELS = { stable: "release", next: "next" };
|
|
17
24
|
const defaultFileSystem = {
|
|
18
25
|
async readFile(path) { return await readFile(path, "utf8"); },
|
|
19
26
|
realpath,
|
|
@@ -192,12 +199,17 @@ function createUpdateProgress(output, enabled) {
|
|
|
192
199
|
}, PROGRESS_TICK_MS);
|
|
193
200
|
timer.unref?.();
|
|
194
201
|
},
|
|
202
|
+
// The bar exists to say the update is still moving. Once it has finished
|
|
203
|
+
// there is a better line to occupy that row — the one naming what is now
|
|
204
|
+
// installed — so the bar gives the row back rather than leaving a full
|
|
205
|
+
// meter above a message that already implies it.
|
|
195
206
|
finish() {
|
|
196
207
|
stopCreep();
|
|
197
208
|
if (!visible)
|
|
198
209
|
return;
|
|
199
|
-
output.stdout(`\r${
|
|
210
|
+
output.stdout(`\r${" ".repeat(PROGRESS_BAR_WIDTH + 6)}\r`);
|
|
200
211
|
visible = false;
|
|
212
|
+
shown = -1;
|
|
201
213
|
},
|
|
202
214
|
clear() {
|
|
203
215
|
stopCreep();
|
|
@@ -209,6 +221,64 @@ function createUpdateProgress(output, enabled) {
|
|
|
209
221
|
},
|
|
210
222
|
};
|
|
211
223
|
}
|
|
224
|
+
/** The newest version the channel points at, which is what an unqualified update takes. */
|
|
225
|
+
async function resolveChannelHead(runner, distTag, output) {
|
|
226
|
+
const lookup = await runNpm(runner, ["view", `${PRODUCT_PACKAGE}@${distTag}`, "version"], true, output, `query the npm ${distTag} channel`);
|
|
227
|
+
if (lookup.result === null)
|
|
228
|
+
return { version: null, exitCode: lookup.exitCode };
|
|
229
|
+
const version = validSemver(lookup.result.stdout.trim());
|
|
230
|
+
if (version === null) {
|
|
231
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic(`received a malformed ${distTag} version from npm: ${JSON.stringify(lookup.result.stdout.trim())}.`)}\n`);
|
|
232
|
+
return { version: null, exitCode: 1 };
|
|
233
|
+
}
|
|
234
|
+
return { version, exitCode: 0 };
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Resolve a preview the caller named.
|
|
238
|
+
*
|
|
239
|
+
* A preview is published as `<version>-dev.<commit>`, so its commit is enough to
|
|
240
|
+
* say which one is wanted — the version in front of it is not something anyone
|
|
241
|
+
* should have to remember. A full version is accepted too, for anyone reading one
|
|
242
|
+
* back from `a1 version` or a changelog.
|
|
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.
|
|
247
|
+
*/
|
|
248
|
+
async function resolveRequestedPreview(runner, requested, output) {
|
|
249
|
+
const lookup = await runNpm(runner, ["view", PRODUCT_PACKAGE, "versions", "--json"], true, output, "list the published versions");
|
|
250
|
+
if (lookup.result === null)
|
|
251
|
+
return { version: null, exitCode: lookup.exitCode };
|
|
252
|
+
let published;
|
|
253
|
+
try {
|
|
254
|
+
published = JSON.parse(lookup.result.stdout.trim() || "[]");
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic(`received a malformed version list from npm: ${JSON.stringify(lookup.result.stdout.trim())}.`)}\n`);
|
|
258
|
+
return { version: null, exitCode: 1 };
|
|
259
|
+
}
|
|
260
|
+
const versions = (Array.isArray(published) ? published : [published]).filter((value) => typeof value === "string");
|
|
261
|
+
const exact = versions.find(version => version === requested);
|
|
262
|
+
if (exact !== undefined) {
|
|
263
|
+
// Naming a release here would install it through the preview path, which is a
|
|
264
|
+
// different command with a different meaning. The commit form cannot express
|
|
265
|
+
// one, so only the fuller spelling of a preview reaches this.
|
|
266
|
+
if (!exact.includes("-dev.")) {
|
|
267
|
+
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
|
+
return { version: null, exitCode: 1 };
|
|
269
|
+
}
|
|
270
|
+
return { version: exact, exitCode: 0 };
|
|
271
|
+
}
|
|
272
|
+
const matches = versions.filter(version => version.endsWith(`-dev.${requested}`));
|
|
273
|
+
if (matches.length === 1)
|
|
274
|
+
return { version: matches[0], exitCode: 0 };
|
|
275
|
+
if (matches.length > 1) {
|
|
276
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic(`found more than one preview for ${requested}: ${matches.join(", ")}. Name the version instead.`)}\n`);
|
|
277
|
+
return { version: null, exitCode: 1 };
|
|
278
|
+
}
|
|
279
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic(`published no preview for ${requested}.`)}\n`);
|
|
280
|
+
return { version: null, exitCode: 1 };
|
|
281
|
+
}
|
|
212
282
|
export async function runSelfUpdate(options) {
|
|
213
283
|
const fileSystem = options.fileSystem ?? defaultFileSystem;
|
|
214
284
|
const output = options.output ?? defaultOutput;
|
|
@@ -237,15 +307,16 @@ export async function runSelfUpdate(options) {
|
|
|
237
307
|
output.stderr(`${PRODUCT_TEXT.diagnostic(`could not read its running package version: ${errorMessage(error)}`)}\n`);
|
|
238
308
|
return 1;
|
|
239
309
|
}
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
310
|
+
const requested = options.target?.trim();
|
|
311
|
+
const resolved = await measure("target-resolution", async () => requested === undefined || requested.length === 0
|
|
312
|
+
? await resolveChannelHead(runner, distTag, output)
|
|
313
|
+
: await resolveRequestedPreview(runner, requested, output));
|
|
314
|
+
if (resolved.version === null)
|
|
315
|
+
return resolved.exitCode;
|
|
316
|
+
const targetVersion = resolved.version;
|
|
317
|
+
// No full stop after a version: it already ends in a dot-separated identifier,
|
|
318
|
+
// and a trailing one reads as part of the version rather than as punctuation.
|
|
319
|
+
output.stdout(`${PRODUCT_TEXT.commandName} update (${UPDATE_CHANNEL_LABELS[channel]}): ${runningVersion} → ${targetVersion}\n`);
|
|
249
320
|
const progress = createUpdateProgress(output, options.progress ?? (options.output === undefined && process.stdout.isTTY === true));
|
|
250
321
|
const rootLookup = await measure("global-root", async () => await runNpm(runner, ["root", "--global"], true, output, "resolve npm's global package root"));
|
|
251
322
|
if (rootLookup.result === null)
|
|
@@ -334,7 +405,7 @@ export async function runSelfUpdate(options) {
|
|
|
334
405
|
await transactionStore.clearCompleted();
|
|
335
406
|
options.onPhaseTiming?.({ phase: "transaction-complete", durationMs: Math.max(0, now() - transactionStartedAt) });
|
|
336
407
|
progress.finish();
|
|
337
|
-
output.stdout(`${PRODUCT_TEXT.commandName} updated successfully: ${targetVersion}
|
|
408
|
+
output.stdout(`${PRODUCT_TEXT.commandName} updated successfully: ${targetVersion}\n`);
|
|
338
409
|
return 0;
|
|
339
410
|
}
|
|
340
411
|
catch (error) {
|
|
@@ -37,7 +37,9 @@ The version is stamped at publish time — `<major.minor.patch>-dev.<short commi
|
|
|
37
37
|
the base taken from whatever `package.json` declares and the suffix from the commit
|
|
38
38
|
being published — and is never written back to the repository. An installed preview
|
|
39
39
|
therefore names the exact source it came from, and rebuilding a commit produces the
|
|
40
|
-
same version rather than a new one.
|
|
40
|
+
same version rather than a new one. That suffix is also how a specific preview is
|
|
41
|
+
installed: `a1 update:<commit>` resolves it against the published list and
|
|
42
|
+
refuses a commit that was never published. `develop` therefore carries one open prerelease version between
|
|
41
43
|
releases, and no commit is ever spent on a preview.
|
|
42
44
|
|
|
43
45
|
One consequence worth knowing: a push that would republish an existing version
|
|
@@ -52,7 +54,7 @@ npm run release -- patch # or minor, major, or an exact x.y.z
|
|
|
52
54
|
```
|
|
53
55
|
|
|
54
56
|
It lands `x.y.z` on `develop` through a pull request that merges itself, waits for
|
|
55
|
-
that publication to succeed, and then lands `x.y.(z+1)-dev
|
|
57
|
+
that publication to succeed, and then lands `x.y.(z+1)-dev` so previews resume
|
|
56
58
|
immediately. It publishes nothing itself and creates no tag.
|
|
57
59
|
|
|
58
60
|
Landing the stable version is what publishes. The same pipeline sees a commit
|
package/package.json
CHANGED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
export declare const PREVIEW_RELEASE_SCHEMA: string;
|
|
2
|
-
export interface DevelopmentPreviewCandidate {
|
|
3
|
-
readonly version: string;
|
|
4
|
-
readonly requiresVersionCommit: boolean;
|
|
5
|
-
}
|
|
6
|
-
export interface DevelopmentPreviewRegistryState {
|
|
7
|
-
readonly published: boolean;
|
|
8
|
-
readonly nextVersion: string | null;
|
|
9
|
-
}
|
|
10
|
-
export interface DevelopmentPreviewVerificationOptions {
|
|
11
|
-
readonly attempts?: number;
|
|
12
|
-
readonly delayMs?: number;
|
|
13
|
-
readonly delay?: (milliseconds: number) => Promise<void>;
|
|
14
|
-
}
|
|
15
|
-
export interface DevelopmentPreviewPublishResult {
|
|
16
|
-
readonly published: boolean;
|
|
17
|
-
readonly recoveredPublishError: unknown | null;
|
|
18
|
-
}
|
|
19
|
-
export interface UncertifiedDevelopmentPreviewEvidenceInput {
|
|
20
|
-
readonly packageName: string;
|
|
21
|
-
readonly version: string;
|
|
22
|
-
readonly commit: string;
|
|
23
|
-
readonly tarball: string;
|
|
24
|
-
readonly integrity: string;
|
|
25
|
-
readonly shasum: string;
|
|
26
|
-
readonly platform: NodeJS.Platform;
|
|
27
|
-
readonly architecture: string;
|
|
28
|
-
readonly recordedAt: string;
|
|
29
|
-
}
|
|
30
|
-
export interface UncertifiedDevelopmentPreviewEvidence extends UncertifiedDevelopmentPreviewEvidenceInput {
|
|
31
|
-
readonly schema: typeof PREVIEW_RELEASE_SCHEMA;
|
|
32
|
-
readonly channel: "next";
|
|
33
|
-
readonly certificationStatus: "uncertified-development-preview";
|
|
34
|
-
readonly terminalCapability: "owned-ui";
|
|
35
|
-
readonly manualAcceptance: "accepted";
|
|
36
|
-
readonly physicalHostCertification: "deferred";
|
|
37
|
-
readonly crossPlatformCertification: "deferred";
|
|
38
|
-
readonly stableReleaseEligible: false;
|
|
39
|
-
}
|
|
40
|
-
export declare function createUncertifiedDevelopmentPreviewEvidence(input: UncertifiedDevelopmentPreviewEvidenceInput): UncertifiedDevelopmentPreviewEvidence;
|
|
41
|
-
export declare function requireManuallyAcceptedDevelopmentPreview(version: string, acceptedVersion: string): void;
|
|
42
|
-
export declare function selectDevelopmentPreviewCandidate(currentVersion: string, publishedVersions: readonly string[]): DevelopmentPreviewCandidate;
|
|
43
|
-
/**
|
|
44
|
-
* Treats npm's process result as provisional: browser-auth completion can fail
|
|
45
|
-
* after the immutable upload succeeds. Registry identity remains authoritative.
|
|
46
|
-
*/
|
|
47
|
-
export declare function publishDevelopmentPreviewWithRecovery(publish: () => Promise<void>, verify: () => Promise<void>): Promise<DevelopmentPreviewPublishResult>;
|
|
48
|
-
export declare function verifyDevelopmentPreviewRegistry(version: string, observe: () => Promise<DevelopmentPreviewRegistryState>, repairNextTag: () => Promise<void>, options?: DevelopmentPreviewVerificationOptions): Promise<void>;
|
|
49
|
-
export declare function developmentPreviewTarballName(packageName: string, version: string): string;
|