@timurproko/a1 0.1.8-dev.0aceebe → 0.1.8-dev.21b95d6
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 +15 -11
- 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 +53 -18
- 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.js +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
package/README.md
CHANGED
|
@@ -9,10 +9,11 @@ npm install --global @timurproko/a1@latest
|
|
|
9
9
|
## Commands
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
a1
|
|
13
|
-
a1 version
|
|
14
|
-
a1 update
|
|
15
|
-
a1 update:next
|
|
12
|
+
a1 # A1-owned UI and profile: ~/.a1/agent
|
|
13
|
+
a1 version # show Installed, Release (latest), and Next versions
|
|
14
|
+
a1 update # update to npm latest
|
|
15
|
+
a1 update:next # update to npm next (or a1 update:<commit> for a specific preview)
|
|
16
|
+
a1 update --models # refresh A1's model catalogs
|
|
16
17
|
```
|
|
17
18
|
|
|
18
19
|
Prerelease builds — what `a1 update:next` installs — add two development profiles
|
|
@@ -30,16 +31,19 @@ Pi extension packages install into A1's own profile, so bare `a1` loads them and
|
|
|
30
31
|
`a1 pi` and `a1 sandbox` do not. Sources are Pi's: `npm:`, git, or a local path.
|
|
31
32
|
|
|
32
33
|
```sh
|
|
33
|
-
a1 install npm:pi-mcp-adapter # install a package into ~/.a1/agent
|
|
34
|
-
a1 remove npm:pi-mcp-adapter # remove it again (alias: a1 uninstall)
|
|
35
|
-
a1 list # list packages installed for a1
|
|
36
|
-
a1 update --extensions # update every installed package
|
|
37
|
-
a1 update npm:pi-mcp-adapter # update one of them
|
|
38
|
-
a1 update --models # refresh model catalogs
|
|
34
|
+
a1 pi install npm:pi-mcp-adapter # install a package into ~/.a1/agent
|
|
35
|
+
a1 pi remove npm:pi-mcp-adapter # remove it again (alias: a1 pi uninstall)
|
|
36
|
+
a1 pi list # list packages installed for a1
|
|
37
|
+
a1 pi update --extensions # update every installed package
|
|
38
|
+
a1 pi update npm:pi-mcp-adapter # update one of them
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
A running session loads a newly installed package after a restart. Pi's own
|
|
42
|
-
profile at `~/.pi/agent` is managed by Pi itself.
|
|
42
|
+
profile at `~/.pi/agent` is managed by Pi itself. Extension configuration is
|
|
43
|
+
isolated too: for example, MCP configuration for bare `a1` belongs under
|
|
44
|
+
`~/.a1/agent` (or the project), so `~/.pi/agent/mcp.json` is not read. Run
|
|
45
|
+
`/mcp setup` inside bare `a1` to configure it; the MCP footer status appears
|
|
46
|
+
when that A1 configuration contains a server.
|
|
43
47
|
|
|
44
48
|
## Develop
|
|
45
49
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "darwin",
|
|
6
6
|
"architecture": "arm64",
|
|
7
7
|
"capability": "unsupported",
|
|
8
|
-
"builtAt": "2026-08-
|
|
8
|
+
"builtAt": "2026-08-25T08:21:01.149Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
11
|
"sha256": "7524bf568992517f50ed53196c861c5edeba0d7275633bb4735ab45bd5963a28",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "linux",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-08-
|
|
8
|
+
"builtAt": "2026-08-25T08:20:57.618Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
11
|
"sha256": "29e22fe29de2828982bc67ef418c4adcaab1490281477b7bea592ec6fe621bcd",
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
"platform": "win32",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-08-
|
|
8
|
+
"builtAt": "2026-08-25T08:21:59.118Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian.exe",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "82d4ad108c0e73f59b8bca207c751a7822e21045895c1453296fff6b649042ea",
|
|
12
12
|
"size": 172544
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
Binary file
|
package/dist/src/cli/dispatch.js
CHANGED
|
@@ -5,12 +5,13 @@ export function cliUsage(capabilities) {
|
|
|
5
5
|
"",
|
|
6
6
|
...(capabilities.developmentProfiles ? ["pi", "sandbox"] : []),
|
|
7
7
|
"version",
|
|
8
|
-
"update [self
|
|
8
|
+
"update [self|--models]",
|
|
9
9
|
"update:next",
|
|
10
10
|
"update:<commit>",
|
|
11
|
-
"install <source>",
|
|
12
|
-
"remove <source>",
|
|
13
|
-
"list",
|
|
11
|
+
"pi install <source>",
|
|
12
|
+
"pi remove <source>",
|
|
13
|
+
"pi list",
|
|
14
|
+
"pi update [--extensions|<source>]",
|
|
14
15
|
]);
|
|
15
16
|
}
|
|
16
17
|
const PROFILE_WORDS = new Set(["pi", "sandbox"]);
|
|
@@ -32,7 +33,13 @@ export function parseCliCommand(arguments_, capabilities) {
|
|
|
32
33
|
if (arguments_.length === 0)
|
|
33
34
|
return { kind: "launch", profileId: "a1" };
|
|
34
35
|
const [command, ...rest] = arguments_;
|
|
35
|
-
if (
|
|
36
|
+
if (command === "pi") {
|
|
37
|
+
if (rest.length > 0)
|
|
38
|
+
return parsePiPackageCommand(rest);
|
|
39
|
+
if (capabilities.developmentProfiles)
|
|
40
|
+
return { kind: "launch", profileId: "pi" };
|
|
41
|
+
}
|
|
42
|
+
if (capabilities.developmentProfiles && command === "sandbox") {
|
|
36
43
|
return withoutArguments(rest, { kind: "launch", profileId: command });
|
|
37
44
|
}
|
|
38
45
|
if (command === "version")
|
|
@@ -41,11 +48,9 @@ export function parseCliCommand(arguments_, capabilities) {
|
|
|
41
48
|
return parseColonUpdate(command.slice("update:".length), rest);
|
|
42
49
|
if (command === "update")
|
|
43
50
|
return parseUpdate(rest);
|
|
44
|
-
if (command === "install" || command === "remove" || command === "uninstall") {
|
|
45
|
-
return
|
|
51
|
+
if (command === "install" || command === "remove" || command === "uninstall" || command === "list") {
|
|
52
|
+
return packageNamespaceRejection(command, rest.join(" ") || undefined);
|
|
46
53
|
}
|
|
47
|
-
if (command === "list")
|
|
48
|
-
return withoutArguments(rest, { kind: "packages", request: { verb: "list", source: null } });
|
|
49
54
|
if (command === "ui")
|
|
50
55
|
return { kind: "error", message: `The ui subcommand was removed; run bare ${PRODUCT_TEXT.commandName} for the owned UI.` };
|
|
51
56
|
if (command === "agent")
|
|
@@ -71,10 +76,9 @@ function parseColonUpdate(suffix, rest) {
|
|
|
71
76
|
return { kind: "update", channel: "next", target: suffix };
|
|
72
77
|
}
|
|
73
78
|
/**
|
|
74
|
-
* `update`
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* invalidate what was certified.
|
|
79
|
+
* Top-level `update` owns A1 itself and A1's model catalogs. Extension package
|
|
80
|
+
* maintenance lives under the `pi` compatibility namespace, while updating the
|
|
81
|
+
* pinned Pi runtime remains impossible.
|
|
78
82
|
*/
|
|
79
83
|
function parseUpdate(rest) {
|
|
80
84
|
if (rest.length === 0)
|
|
@@ -90,18 +94,42 @@ function parseUpdate(rest) {
|
|
|
90
94
|
message: PRODUCT_TEXT.diagnostic(`pins the Pi version it was certified against; run ${PRODUCT_TEXT.commandName} update to move ${PRODUCT_TEXT.displayName} itself.`),
|
|
91
95
|
};
|
|
92
96
|
}
|
|
93
|
-
// A release channel is spelled with the colon. Taking the bare word as a package
|
|
94
|
-
// source would turn a near miss into a confident search for a package nobody has.
|
|
95
97
|
if (target === "next" || target === "stable") {
|
|
96
98
|
const form = target === "next" ? `${PRODUCT_TEXT.commandName} update:next` : `${PRODUCT_TEXT.commandName} update`;
|
|
97
99
|
return { kind: "error", message: PRODUCT_TEXT.diagnostic(`selects a release channel with a colon; run ${form}.`) };
|
|
98
100
|
}
|
|
99
|
-
if (target === "--extensions")
|
|
100
|
-
return { kind: "packages", request: { verb: "update", source: null } };
|
|
101
101
|
if (target === "--models")
|
|
102
102
|
return { kind: "packages", request: { verb: "refresh-models", source: null } };
|
|
103
|
+
if (target === "--extensions" || (target !== undefined && !target.startsWith("-"))) {
|
|
104
|
+
return packageNamespaceRejection("update", target === "--extensions" ? "--extensions" : target);
|
|
105
|
+
}
|
|
106
|
+
return unknownOption(target ?? "", "update");
|
|
107
|
+
}
|
|
108
|
+
function parsePiPackageCommand(arguments_) {
|
|
109
|
+
const [verb, ...rest] = arguments_;
|
|
110
|
+
if (verb === "install" || verb === "remove" || verb === "uninstall") {
|
|
111
|
+
return parseSourceCommand(verb === "install" ? "install" : "remove", rest);
|
|
112
|
+
}
|
|
113
|
+
if (verb === "list")
|
|
114
|
+
return withoutArguments(rest, { kind: "packages", request: { verb: "list", source: null } });
|
|
115
|
+
if (verb === "update")
|
|
116
|
+
return parsePiPackageUpdate(rest);
|
|
117
|
+
return { kind: "error", message: PRODUCT_TEXT.diagnostic(`received an unknown pi package command: ${verb ?? ""}`) };
|
|
118
|
+
}
|
|
119
|
+
function parsePiPackageUpdate(rest) {
|
|
120
|
+
if (rest.length === 0) {
|
|
121
|
+
return { kind: "error", message: PRODUCT_TEXT.diagnostic(`pi update needs --extensions or a package source.`) };
|
|
122
|
+
}
|
|
123
|
+
if (rest.length > 1)
|
|
124
|
+
return { kind: "error", message: PRODUCT_TEXT.diagnostic("pi update accepts one target.") };
|
|
125
|
+
const [target] = rest;
|
|
126
|
+
if (target === "--extensions")
|
|
127
|
+
return { kind: "packages", request: { verb: "update", source: null } };
|
|
128
|
+
if (target === "--models") {
|
|
129
|
+
return { kind: "error", message: PRODUCT_TEXT.diagnostic(`refreshes its model catalogs at the top level; run ${PRODUCT_TEXT.commandName} update --models.`) };
|
|
130
|
+
}
|
|
103
131
|
if (target === undefined || target.startsWith("-"))
|
|
104
|
-
return unknownOption(target ?? "", "update");
|
|
132
|
+
return unknownOption(target ?? "", "pi update");
|
|
105
133
|
if (PROFILE_WORDS.has(target))
|
|
106
134
|
return profileRejection("update");
|
|
107
135
|
return { kind: "packages", request: { verb: "update", source: target } };
|
|
@@ -129,6 +157,13 @@ function withoutArguments(rest, command) {
|
|
|
129
157
|
return profileRejection("list");
|
|
130
158
|
return { kind: "error", message: PRODUCT_TEXT.diagnostic("commands do not accept additional arguments.") };
|
|
131
159
|
}
|
|
160
|
+
function packageNamespaceRejection(verb, target) {
|
|
161
|
+
const suffix = target === undefined ? "" : ` ${target}`;
|
|
162
|
+
return {
|
|
163
|
+
kind: "error",
|
|
164
|
+
message: PRODUCT_TEXT.diagnostic(`manages extension packages under its pi namespace; run ${PRODUCT_TEXT.commandName} pi ${verb}${suffix}.`),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
132
167
|
function profileRejection(verb) {
|
|
133
168
|
return {
|
|
134
169
|
kind: "error",
|
|
@@ -3,7 +3,7 @@ export * from "./conformance.js";
|
|
|
3
3
|
export * from "./shell-components.js";
|
|
4
4
|
export * from "./theme.js";
|
|
5
5
|
export * from "./upstream/theme/theme-controller.js";
|
|
6
|
-
export { applyConfiguredPiTheme, getAvailablePiThemes } from "./upstream/theme/theme.js";
|
|
6
|
+
export { applyConfiguredPiTheme, getAvailablePiThemes, onPiThemeChange } from "./upstream/theme/theme.js";
|
|
7
7
|
export * from "./upstream/components/countdown-timer.js";
|
|
8
8
|
export * from "./upstream/components/extension-editor.js";
|
|
9
9
|
export * from "./upstream/components/session-selector.js";
|
|
@@ -3,7 +3,7 @@ export * from "./conformance.js";
|
|
|
3
3
|
export * from "./shell-components.js";
|
|
4
4
|
export * from "./theme.js";
|
|
5
5
|
export * from "./upstream/theme/theme-controller.js";
|
|
6
|
-
export { applyConfiguredPiTheme, getAvailablePiThemes } from "./upstream/theme/theme.js";
|
|
6
|
+
export { applyConfiguredPiTheme, getAvailablePiThemes, onPiThemeChange } from "./upstream/theme/theme.js";
|
|
7
7
|
export * from "./upstream/components/countdown-timer.js";
|
|
8
8
|
export * from "./upstream/components/extension-editor.js";
|
|
9
9
|
export * from "./upstream/components/session-selector.js";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AssistantMessageComponent, BashExecutionComponent, CompactionSummaryMessageComponent, CustomMessageComponent, DynamicBorder, getMarkdownTheme, parseSkillBlock, ToolExecutionComponent, UserMessageComponent, } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { SkillInvocationMessageComponent, } from "./upstream/components/skill-invocation-message.js";
|
|
3
3
|
import { Container, Markdown, Spacer, Text, } from "#pi-tui";
|
|
4
|
+
import { PRODUCT_TEXT } from "../../product-identity.js";
|
|
4
5
|
import { KeybindingsManager, } from "./upstream/adjacent/core/keybindings.js";
|
|
5
6
|
import { PINNED_PI_LAYOUT, piTheme, } from "./theme.js";
|
|
6
7
|
import { componentPort, createTuiFacade, ensureTheme, formatSessionTokens, isRecord, } from "./shell-shared-facade.js";
|
|
@@ -168,7 +169,7 @@ export function renderPiShellPackageUpdateNotice(packages, width) {
|
|
|
168
169
|
container.addChild(new Spacer(1));
|
|
169
170
|
container.addChild(new DynamicBorder(text => theme.fg("warning", text)));
|
|
170
171
|
container.addChild(new Text(`${theme.bold(theme.fg("warning", "Package Updates Available"))}\n`
|
|
171
|
-
+ `${theme.fg("muted", "Package updates are available. Run ")}${theme.fg("accent",
|
|
172
|
+
+ `${theme.fg("muted", "Package updates are available. Run ")}${theme.fg("accent", `${PRODUCT_TEXT.commandName} pi update --extensions`)}\n`
|
|
172
173
|
+ `${theme.fg("muted", "Packages:")}\n`
|
|
173
174
|
+ packages.map(name => `- ${name}`).join("\n"), 1, 0));
|
|
174
175
|
container.addChild(new DynamicBorder(text => theme.fg("warning", text)));
|
|
@@ -60,11 +60,18 @@ export interface OwnedPiResourceSummary {
|
|
|
60
60
|
readonly sourcePath: string | null;
|
|
61
61
|
readonly diagnostic: string | null;
|
|
62
62
|
}
|
|
63
|
+
export interface OwnedPiExtensionSourceSummary {
|
|
64
|
+
readonly source: string;
|
|
65
|
+
readonly scope: "user" | "project" | "temporary";
|
|
66
|
+
readonly origin: "package" | "top-level";
|
|
67
|
+
readonly baseDir: string | null;
|
|
68
|
+
}
|
|
63
69
|
export interface OwnedPiExtensionResourceSummary {
|
|
64
70
|
readonly kind: "extension";
|
|
65
71
|
readonly id: string;
|
|
66
72
|
readonly sourcePath: string | null;
|
|
67
73
|
readonly resolvedPath: string | null;
|
|
74
|
+
readonly sourceInfo: OwnedPiExtensionSourceSummary | null;
|
|
68
75
|
readonly loaded: boolean;
|
|
69
76
|
readonly hidden: boolean;
|
|
70
77
|
readonly diagnostic: string | null;
|
|
@@ -11,6 +11,12 @@ import { createPiRuntimeIntegration } from "./runtime-integration.js";
|
|
|
11
11
|
import { PiSessionCommandIntegration } from "./session-integration.js";
|
|
12
12
|
import { PiSettingsIntegration } from "./settings-integration.js";
|
|
13
13
|
const execFileAsync = promisify(execFile);
|
|
14
|
+
/**
|
|
15
|
+
* Engine events delivered before the queue hands the event loop a turn. Small enough
|
|
16
|
+
* that a streaming burst never holds input, large enough that an ordinary turn is one
|
|
17
|
+
* batch.
|
|
18
|
+
*/
|
|
19
|
+
const EVENT_DELIVERY_BATCH = 16;
|
|
14
20
|
const DEFAULT_SURFACE = {
|
|
15
21
|
columns: 100,
|
|
16
22
|
rows: 32,
|
|
@@ -53,6 +59,8 @@ export class PiEngineAdapter {
|
|
|
53
59
|
#activeCommandIds = [];
|
|
54
60
|
#completedCommands = new Map();
|
|
55
61
|
#transcript = [];
|
|
62
|
+
#transcriptIndex = new Map();
|
|
63
|
+
#transcriptSnapshot;
|
|
56
64
|
#messageBlockIds = new WeakMap();
|
|
57
65
|
#messageFallbackIds = new Map();
|
|
58
66
|
#toolBlockIds = new Map();
|
|
@@ -61,6 +69,8 @@ export class PiEngineAdapter {
|
|
|
61
69
|
#eventQueue = [];
|
|
62
70
|
#eventQueueProcessing;
|
|
63
71
|
#droppedEventCount = 0;
|
|
72
|
+
#agentRunActive = false;
|
|
73
|
+
#statusKind = null;
|
|
64
74
|
#sessionCommands;
|
|
65
75
|
#gitBranch = null;
|
|
66
76
|
#extensionUi;
|
|
@@ -143,7 +153,7 @@ export class PiEngineAdapter {
|
|
|
143
153
|
if (this.#disposed || updates.length === 0)
|
|
144
154
|
return;
|
|
145
155
|
const packages = updates.map(name => `- ${name}`).join("\n");
|
|
146
|
-
this.#addDiagnostic("info", "package-updates", `Package updates are available. Run pi update --extensions\nPackages:\n${packages}`, true);
|
|
156
|
+
this.#addDiagnostic("info", "package-updates", `Package updates are available. Run ${PRODUCT_IDENTITY.commandName} pi update --extensions\nPackages:\n${packages}`, true);
|
|
147
157
|
this.#emitView();
|
|
148
158
|
}
|
|
149
159
|
onEvent(listener) {
|
|
@@ -278,6 +288,7 @@ export class PiEngineAdapter {
|
|
|
278
288
|
id: `extension-${resources.length}`,
|
|
279
289
|
sourcePath: extension.path,
|
|
280
290
|
resolvedPath: extension.resolvedPath,
|
|
291
|
+
sourceInfo: extensionSourceSummary(extension.sourceInfo),
|
|
281
292
|
loaded: true,
|
|
282
293
|
hidden: extension.hidden === true,
|
|
283
294
|
diagnostic: null,
|
|
@@ -373,9 +384,10 @@ export class PiEngineAdapter {
|
|
|
373
384
|
}
|
|
374
385
|
const extensionCommands = this.#session?.extensionRunner?.getRegisteredCommands?.();
|
|
375
386
|
if (Array.isArray(extensionCommands)) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
387
|
+
const registered = extensionCommands.filter(isRecord);
|
|
388
|
+
for (const command of registered) {
|
|
389
|
+
const name = stringProperty(command, "invocationName") ?? stringProperty(command, "name");
|
|
390
|
+
if (!name || usedNames.has(name) || isPiPrefixedCompatibilityAlias(command, registered))
|
|
379
391
|
continue;
|
|
380
392
|
commands.push({ name, description: stringProperty(command, "description") ?? "Extension command", source: "extension" });
|
|
381
393
|
usedNames.add(name);
|
|
@@ -724,7 +736,7 @@ export class PiEngineAdapter {
|
|
|
724
736
|
sessionId: this.#sessionId,
|
|
725
737
|
revision: this.#viewRevision,
|
|
726
738
|
lifecycle: this.#lifecycle,
|
|
727
|
-
transcript: this.#
|
|
739
|
+
transcript: this.#transcriptSnapshot ??= Object.freeze([...this.#transcript]),
|
|
728
740
|
editor: { ...this.#editor, queuedSubmissions: [...this.#editor.queuedSubmissions] },
|
|
729
741
|
status: {
|
|
730
742
|
...this.#status,
|
|
@@ -1350,6 +1362,8 @@ export class PiEngineAdapter {
|
|
|
1350
1362
|
submitEnabled: true,
|
|
1351
1363
|
};
|
|
1352
1364
|
this.#status = { ...this.#status, workingMessage: null, badges: [] };
|
|
1365
|
+
this.#statusKind = null;
|
|
1366
|
+
this.#agentRunActive = false;
|
|
1353
1367
|
this.#activeModel = readModel(session.model);
|
|
1354
1368
|
this.#reconcileActiveModelAvailability();
|
|
1355
1369
|
this.#thinkingLevel = readThinkingLevel(session.thinkingLevel);
|
|
@@ -1402,28 +1416,76 @@ export class PiEngineAdapter {
|
|
|
1402
1416
|
this.#emitView();
|
|
1403
1417
|
}
|
|
1404
1418
|
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Replaces the transcript and the index that finds a block by its identifier. Every
|
|
1421
|
+
* lookup goes through the index, so streaming a chunk costs the same in a long session
|
|
1422
|
+
* as in a new one.
|
|
1423
|
+
*/
|
|
1424
|
+
#setTranscript(blocks) {
|
|
1425
|
+
this.#transcript = blocks;
|
|
1426
|
+
this.#transcriptIndex.clear();
|
|
1427
|
+
for (const [index, block] of blocks.entries())
|
|
1428
|
+
this.#transcriptIndex.set(block.id, index);
|
|
1429
|
+
this.#transcriptSnapshot = undefined;
|
|
1430
|
+
}
|
|
1431
|
+
#transcriptBlock(id) {
|
|
1432
|
+
const index = this.#transcriptIndex.get(id);
|
|
1433
|
+
return index === undefined ? undefined : this.#transcript[index];
|
|
1434
|
+
}
|
|
1435
|
+
/** Shows the state named by `kind`, which becomes the state a later end can clear. */
|
|
1436
|
+
#enterWorkState(kind, message) {
|
|
1437
|
+
const wasBusy = this.#lifecycle === "busy";
|
|
1438
|
+
this.#statusKind = kind;
|
|
1439
|
+
this.#lifecycle = "busy";
|
|
1440
|
+
this.#status = { ...this.#status, workingMessage: message };
|
|
1441
|
+
if (!wasBusy)
|
|
1442
|
+
this.#emitEvent({ type: "session-lifecycle", lifecycle: "busy", reason: null });
|
|
1443
|
+
this.#emitEvent({ type: "status", status: this.#status });
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Ends one named state. A state the shell is not in is left alone, so a finished
|
|
1447
|
+
* compaction or retry cannot clear the working state it never replaced. While the run
|
|
1448
|
+
* continues, ending either of those returns to working rather than to idle.
|
|
1449
|
+
*/
|
|
1450
|
+
#endWorkState(kind) {
|
|
1451
|
+
if (this.#statusKind !== kind)
|
|
1452
|
+
return;
|
|
1453
|
+
if (this.#agentRunActive) {
|
|
1454
|
+
this.#enterWorkState("working", "Working...");
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
this.#leaveWorkStates();
|
|
1458
|
+
}
|
|
1459
|
+
/** Leaves every work state and reports the session idle. */
|
|
1460
|
+
#leaveWorkStates() {
|
|
1461
|
+
this.#statusKind = null;
|
|
1462
|
+
this.#lifecycle = "ready";
|
|
1463
|
+
this.#status = { ...this.#status, workingMessage: null };
|
|
1464
|
+
this.#emitEvent({ type: "session-lifecycle", lifecycle: "ready", reason: null });
|
|
1465
|
+
this.#emitEvent({ type: "status", status: this.#status });
|
|
1466
|
+
}
|
|
1405
1467
|
#handlePiEvent(event) {
|
|
1406
1468
|
if (!isRecord(event) || typeof event.type !== "string")
|
|
1407
1469
|
return;
|
|
1408
1470
|
switch (event.type) {
|
|
1409
1471
|
case "agent_start":
|
|
1410
|
-
this.#
|
|
1411
|
-
this.#
|
|
1412
|
-
this.#emitEvent({ type: "session-lifecycle", lifecycle: "busy", reason: null });
|
|
1413
|
-
this.#emitEvent({ type: "status", status: this.#status });
|
|
1472
|
+
this.#agentRunActive = true;
|
|
1473
|
+
this.#enterWorkState("working", "Working...");
|
|
1414
1474
|
return;
|
|
1415
1475
|
case "message_start":
|
|
1416
1476
|
this.#upsertMessageBlock(event.message, "live");
|
|
1417
1477
|
return;
|
|
1418
1478
|
case "message_update": {
|
|
1419
|
-
const
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1479
|
+
const delta = isRecord(event.assistantMessageEvent) && typeof event.assistantMessageEvent.delta === "string"
|
|
1480
|
+
? event.assistantMessageEvent.delta
|
|
1481
|
+
: undefined;
|
|
1482
|
+
// The delta is folded in before the block is stored, so a chunk is one update to
|
|
1483
|
+
// one block rather than a store without the delta followed by a store with it.
|
|
1484
|
+
const blocks = this.#messageBlocks(event.message, "live", this.#transcript.length);
|
|
1485
|
+
for (const [index, block] of blocks.entries()) {
|
|
1486
|
+
this.#upsertTranscriptBlock(index === 0 && delta !== undefined && !block.text.endsWith(delta)
|
|
1487
|
+
? { ...block, text: `${block.text}${delta}` }
|
|
1488
|
+
: block);
|
|
1427
1489
|
}
|
|
1428
1490
|
return;
|
|
1429
1491
|
}
|
|
@@ -1452,11 +1514,18 @@ export class PiEngineAdapter {
|
|
|
1452
1514
|
if (finalMessages.length > 0)
|
|
1453
1515
|
this.#rebuildTranscript(finalMessages, "finalized");
|
|
1454
1516
|
else
|
|
1455
|
-
this.#
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1517
|
+
this.#setTranscript(this.#transcript.map(block => block.status === "live" ? { ...block, status: "finalized" } : block));
|
|
1518
|
+
// Ending a turn leaves the working state, as the recorded pinned baseline does, but
|
|
1519
|
+
// it leaves only that state: a compaction or retry being shown outlives the turn
|
|
1520
|
+
// that ended under it. Settlement ends the run, and with it every state — the
|
|
1521
|
+
// engine ends a turn for each continuation it makes and settles once.
|
|
1522
|
+
if (event.type === "agent_settled") {
|
|
1523
|
+
this.#agentRunActive = false;
|
|
1524
|
+
this.#leaveWorkStates();
|
|
1525
|
+
}
|
|
1526
|
+
else if (this.#statusKind === null || this.#statusKind === "working") {
|
|
1527
|
+
this.#leaveWorkStates();
|
|
1528
|
+
}
|
|
1460
1529
|
this.#emitView();
|
|
1461
1530
|
return;
|
|
1462
1531
|
}
|
|
@@ -1472,20 +1541,16 @@ export class PiEngineAdapter {
|
|
|
1472
1541
|
return;
|
|
1473
1542
|
}
|
|
1474
1543
|
case "auto_retry_start":
|
|
1475
|
-
this.#
|
|
1476
|
-
this.#status = { ...this.#status, workingMessage: "Retrying…" };
|
|
1477
|
-
this.#emitEvent({ type: "status", status: this.#status });
|
|
1544
|
+
this.#enterWorkState("retry", "Retrying…");
|
|
1478
1545
|
return;
|
|
1479
1546
|
case "auto_retry_end":
|
|
1480
|
-
|
|
1481
|
-
this.#lifecycle = "ready";
|
|
1482
|
-
this.#status = { ...this.#status, workingMessage: null };
|
|
1483
|
-
this.#emitEvent({ type: "status", status: this.#status });
|
|
1547
|
+
this.#endWorkState("retry");
|
|
1484
1548
|
return;
|
|
1485
1549
|
case "compaction_start":
|
|
1486
|
-
this.#
|
|
1487
|
-
|
|
1488
|
-
|
|
1550
|
+
this.#enterWorkState("compaction", "Compacting…");
|
|
1551
|
+
return;
|
|
1552
|
+
case "compaction_end":
|
|
1553
|
+
this.#endWorkState("compaction");
|
|
1489
1554
|
return;
|
|
1490
1555
|
case "thinking_level_changed":
|
|
1491
1556
|
this.#thinkingLevel = readThinkingLevel(event.level);
|
|
@@ -1575,7 +1640,13 @@ export class PiEngineAdapter {
|
|
|
1575
1640
|
}
|
|
1576
1641
|
}
|
|
1577
1642
|
}
|
|
1578
|
-
|
|
1643
|
+
// An authoritative rebuild restates most of what is already there. Reusing the block
|
|
1644
|
+
// that already says it keeps its revision, and with it the rows the shell rendered for
|
|
1645
|
+
// it — otherwise every turn that ends re-renders the whole session.
|
|
1646
|
+
this.#setTranscript(blocks.map(block => {
|
|
1647
|
+
const existing = this.#transcriptBlock(block.id);
|
|
1648
|
+
return existing !== undefined && sameBlockContent(existing, block) ? existing : block;
|
|
1649
|
+
}));
|
|
1579
1650
|
}
|
|
1580
1651
|
#upsertMessageBlock(message, status) {
|
|
1581
1652
|
const blocks = this.#messageBlocks(message, status, this.#transcript.length);
|
|
@@ -1660,7 +1731,7 @@ export class PiEngineAdapter {
|
|
|
1660
1731
|
: this.#toolBlockIds.get(toolCallId) ?? `tool-${toolCallId}`;
|
|
1661
1732
|
if (toolCallId !== undefined)
|
|
1662
1733
|
this.#toolBlockIds.set(toolCallId, blockId);
|
|
1663
|
-
const existing = this.#
|
|
1734
|
+
const existing = this.#transcriptBlock(blockId);
|
|
1664
1735
|
const existingPayload = isRecord(existing?.payload) ? existing.payload : undefined;
|
|
1665
1736
|
return [{
|
|
1666
1737
|
id: blockId,
|
|
@@ -1751,11 +1822,20 @@ export class PiEngineAdapter {
|
|
|
1751
1822
|
});
|
|
1752
1823
|
}
|
|
1753
1824
|
#upsertTranscriptBlock(block) {
|
|
1754
|
-
const index = this.#
|
|
1755
|
-
if (index
|
|
1825
|
+
const index = this.#transcriptIndex.get(block.id);
|
|
1826
|
+
if (index !== undefined) {
|
|
1827
|
+
const existing = this.#transcript[index];
|
|
1828
|
+
// Nothing to tell the shell about a block that repeats itself, and keeping the
|
|
1829
|
+
// revision keeps the rows it already rendered.
|
|
1830
|
+
if (existing !== undefined && sameBlockContent(existing, block))
|
|
1831
|
+
return;
|
|
1756
1832
|
this.#transcript[index] = block;
|
|
1757
|
-
|
|
1833
|
+
}
|
|
1834
|
+
else {
|
|
1835
|
+
this.#transcriptIndex.set(block.id, this.#transcript.length);
|
|
1758
1836
|
this.#transcript.push(block);
|
|
1837
|
+
}
|
|
1838
|
+
this.#transcriptSnapshot = undefined;
|
|
1759
1839
|
this.#emitEvent({ type: "transcript-block", block });
|
|
1760
1840
|
}
|
|
1761
1841
|
#messageBlockId(message, fallbackIndex, status, occurrence) {
|
|
@@ -1773,7 +1853,7 @@ export class PiEngineAdapter {
|
|
|
1773
1853
|
}
|
|
1774
1854
|
}
|
|
1775
1855
|
else {
|
|
1776
|
-
id = [...cached].reverse().find(candidate => this.#
|
|
1856
|
+
id = [...cached].reverse().find(candidate => this.#transcriptBlock(candidate)?.status === "live");
|
|
1777
1857
|
if (id === undefined && status === "finalized")
|
|
1778
1858
|
id = cached.at(-1);
|
|
1779
1859
|
if (id === undefined) {
|
|
@@ -1786,7 +1866,7 @@ export class PiEngineAdapter {
|
|
|
1786
1866
|
return id;
|
|
1787
1867
|
}
|
|
1788
1868
|
#nextBlockRevision(id) {
|
|
1789
|
-
const existing = this.#
|
|
1869
|
+
const existing = this.#transcriptBlock(id);
|
|
1790
1870
|
if (existing)
|
|
1791
1871
|
return existing.revision + 1;
|
|
1792
1872
|
this.#nextBlockSequence += 1;
|
|
@@ -1847,6 +1927,7 @@ export class PiEngineAdapter {
|
|
|
1847
1927
|
}
|
|
1848
1928
|
async #processEventQueue() {
|
|
1849
1929
|
try {
|
|
1930
|
+
let deliveredSinceYield = 0;
|
|
1850
1931
|
while (this.#eventQueue.length > 0) {
|
|
1851
1932
|
const event = this.#eventQueue.shift();
|
|
1852
1933
|
if (!event)
|
|
@@ -1859,6 +1940,14 @@ export class PiEngineAdapter {
|
|
|
1859
1940
|
this.#recordDiagnostic("warning", "event-listener", error instanceof Error ? error.message : String(error), true);
|
|
1860
1941
|
}
|
|
1861
1942
|
}
|
|
1943
|
+
deliveredSinceYield += 1;
|
|
1944
|
+
// A microtask chain runs to exhaustion before the loop turns, so a streaming
|
|
1945
|
+
// burst would hold typed input, pointer reports, and timed indicators until it
|
|
1946
|
+
// drained. Yielding on a macrotask hands those their turn between batches.
|
|
1947
|
+
if (deliveredSinceYield >= EVENT_DELIVERY_BATCH && this.#eventQueue.length > 0) {
|
|
1948
|
+
deliveredSinceYield = 0;
|
|
1949
|
+
await new Promise(resolve => { setImmediate(resolve); });
|
|
1950
|
+
}
|
|
1862
1951
|
}
|
|
1863
1952
|
}
|
|
1864
1953
|
finally {
|
|
@@ -2245,11 +2334,26 @@ function extensionResourceDiagnostic(index, sourcePath, diagnostic) {
|
|
|
2245
2334
|
id: `extension-diagnostic-${index}`,
|
|
2246
2335
|
sourcePath,
|
|
2247
2336
|
resolvedPath: null,
|
|
2337
|
+
sourceInfo: null,
|
|
2248
2338
|
loaded: false,
|
|
2249
2339
|
hidden: false,
|
|
2250
2340
|
diagnostic,
|
|
2251
2341
|
};
|
|
2252
2342
|
}
|
|
2343
|
+
function extensionSourceSummary(value) {
|
|
2344
|
+
if (!isRecord(value))
|
|
2345
|
+
return null;
|
|
2346
|
+
const source = stringProperty(value, "source");
|
|
2347
|
+
const scope = value.scope;
|
|
2348
|
+
const origin = value.origin;
|
|
2349
|
+
const baseDir = value.baseDir;
|
|
2350
|
+
if (!source
|
|
2351
|
+
|| (scope !== "user" && scope !== "project" && scope !== "temporary")
|
|
2352
|
+
|| (origin !== "package" && origin !== "top-level")
|
|
2353
|
+
|| (baseDir !== undefined && typeof baseDir !== "string"))
|
|
2354
|
+
return null;
|
|
2355
|
+
return { source, scope, origin, baseDir: baseDir ?? null };
|
|
2356
|
+
}
|
|
2253
2357
|
function collectionResult(value, key) {
|
|
2254
2358
|
if (!isRecord(value))
|
|
2255
2359
|
return { values: [], diagnostics: [] };
|
|
@@ -2274,10 +2378,60 @@ function stringProperty(value, key) {
|
|
|
2274
2378
|
const item = value[key];
|
|
2275
2379
|
return typeof item === "string" && item.length > 0 ? item : undefined;
|
|
2276
2380
|
}
|
|
2381
|
+
/**
|
|
2382
|
+
* Some ecosystem extensions retain a `pi-<name>` slash-command alias beside
|
|
2383
|
+
* their unprefixed command. A1 presents the product-neutral command once while
|
|
2384
|
+
* leaving Pi's runner free to accept the compatibility alias when typed.
|
|
2385
|
+
*/
|
|
2386
|
+
function isPiPrefixedCompatibilityAlias(command, commands) {
|
|
2387
|
+
const name = stringProperty(command, "name");
|
|
2388
|
+
if (!name?.startsWith("pi-") || name.length === 3)
|
|
2389
|
+
return false;
|
|
2390
|
+
const canonicalName = name.slice(3);
|
|
2391
|
+
const description = stringProperty(command, "description");
|
|
2392
|
+
const sourcePath = extensionCommandSourcePath(command);
|
|
2393
|
+
return commands.some(candidate => candidate !== command
|
|
2394
|
+
&& stringProperty(candidate, "name") === canonicalName
|
|
2395
|
+
&& stringProperty(candidate, "description") === description
|
|
2396
|
+
&& extensionCommandSourcePath(candidate) === sourcePath);
|
|
2397
|
+
}
|
|
2398
|
+
function extensionCommandSourcePath(command) {
|
|
2399
|
+
return isRecord(command) ? stringProperty(command.sourceInfo, "path") : undefined;
|
|
2400
|
+
}
|
|
2277
2401
|
function compactResourceLabel(path) {
|
|
2278
2402
|
const segments = path.replaceAll("\\", "/").split("/").filter(Boolean);
|
|
2279
2403
|
return segments.at(-1) ?? path;
|
|
2280
2404
|
}
|
|
2405
|
+
/**
|
|
2406
|
+
* Whether two blocks say the same thing. A block that says what it already said is not a
|
|
2407
|
+
* new revision: the shell renders a block once per revision, so bumping one it did not
|
|
2408
|
+
* need re-renders it for nothing.
|
|
2409
|
+
*/
|
|
2410
|
+
function sameBlockContent(left, right) {
|
|
2411
|
+
return left.kind === right.kind
|
|
2412
|
+
&& left.status === right.status
|
|
2413
|
+
&& left.title === right.title
|
|
2414
|
+
&& left.text === right.text
|
|
2415
|
+
&& sameValue(left.payload, right.payload);
|
|
2416
|
+
}
|
|
2417
|
+
function sameValue(left, right) {
|
|
2418
|
+
if (left === right)
|
|
2419
|
+
return true;
|
|
2420
|
+
if (typeof left !== typeof right || left === null || right === null)
|
|
2421
|
+
return false;
|
|
2422
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
2423
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length)
|
|
2424
|
+
return false;
|
|
2425
|
+
return left.every((value, index) => sameValue(value, right[index]));
|
|
2426
|
+
}
|
|
2427
|
+
if (typeof left !== "object")
|
|
2428
|
+
return false;
|
|
2429
|
+
const leftKeys = Object.keys(left);
|
|
2430
|
+
const rightRecord = right;
|
|
2431
|
+
if (leftKeys.length !== Object.keys(rightRecord).length)
|
|
2432
|
+
return false;
|
|
2433
|
+
return leftKeys.every(key => Object.hasOwn(rightRecord, key) && sameValue(left[key], rightRecord[key]));
|
|
2434
|
+
}
|
|
2281
2435
|
function textFromContent(content) {
|
|
2282
2436
|
if (typeof content === "string")
|
|
2283
2437
|
return content;
|
|
@@ -39,6 +39,12 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
|
|
|
39
39
|
readonly onDequeue?: () => void;
|
|
40
40
|
}, startup?: PiShellHeaderOptions, agentDir?: string, extensionRenderers?: PiShellExtensionRendererResolver);
|
|
41
41
|
update(view: OwnedUiSessionViewModel): void;
|
|
42
|
+
/**
|
|
43
|
+
* Applies one block: its component is created or updated in place and the order grows
|
|
44
|
+
* only when the block is new. A streamed chunk costs one component update rather than a
|
|
45
|
+
* walk of the whole transcript.
|
|
46
|
+
*/
|
|
47
|
+
applyTranscriptBlock(block: OwnedUiSessionViewModel["transcript"][number]): void;
|
|
42
48
|
render(width: number): readonly string[];
|
|
43
49
|
layoutRoot(): PiTuiLayoutNode;
|
|
44
50
|
transcriptComponent(id: string): PiShellTranscriptComponentPort | undefined;
|
|
@@ -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";
|
|
@@ -314,7 +314,9 @@ export async function runSelfUpdate(options) {
|
|
|
314
314
|
if (resolved.version === null)
|
|
315
315
|
return resolved.exitCode;
|
|
316
316
|
const targetVersion = resolved.version;
|
|
317
|
-
|
|
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`);
|
|
318
320
|
const progress = createUpdateProgress(output, options.progress ?? (options.output === undefined && process.stdout.isTTY === true));
|
|
319
321
|
const rootLookup = await measure("global-root", async () => await runNpm(runner, ["root", "--global"], true, output, "resolve npm's global package root"));
|
|
320
322
|
if (rootLookup.result === null)
|
|
@@ -403,7 +405,7 @@ export async function runSelfUpdate(options) {
|
|
|
403
405
|
await transactionStore.clearCompleted();
|
|
404
406
|
options.onPhaseTiming?.({ phase: "transaction-complete", durationMs: Math.max(0, now() - transactionStartedAt) });
|
|
405
407
|
progress.finish();
|
|
406
|
-
output.stdout(`${PRODUCT_TEXT.commandName} updated successfully: ${targetVersion}
|
|
408
|
+
output.stdout(`${PRODUCT_TEXT.commandName} updated successfully: ${targetVersion}\n`);
|
|
407
409
|
return 0;
|
|
408
410
|
}
|
|
409
411
|
catch (error) {
|
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;
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { compare, inc, prerelease, valid } from "semver";
|
|
2
|
-
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
3
|
-
export const PREVIEW_RELEASE_SCHEMA = PRODUCT_IDENTITY.evidence.previewReleaseSchema;
|
|
4
|
-
export function createUncertifiedDevelopmentPreviewEvidence(input) {
|
|
5
|
-
const prereleaseParts = prerelease(input.version);
|
|
6
|
-
if (valid(input.version) === null || prereleaseParts?.[0] !== "dev") {
|
|
7
|
-
throw new Error(`uncertified preview requires a development prerelease: ${input.version}`);
|
|
8
|
-
}
|
|
9
|
-
return {
|
|
10
|
-
schema: PREVIEW_RELEASE_SCHEMA,
|
|
11
|
-
channel: "next",
|
|
12
|
-
certificationStatus: "uncertified-development-preview",
|
|
13
|
-
terminalCapability: "owned-ui",
|
|
14
|
-
manualAcceptance: "accepted",
|
|
15
|
-
physicalHostCertification: "deferred",
|
|
16
|
-
crossPlatformCertification: "deferred",
|
|
17
|
-
stableReleaseEligible: false,
|
|
18
|
-
...input,
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
export function requireManuallyAcceptedDevelopmentPreview(version, acceptedVersion) {
|
|
22
|
-
const acceptedPrerelease = prerelease(acceptedVersion);
|
|
23
|
-
if (valid(acceptedVersion) === null || acceptedPrerelease?.[0] !== "dev") {
|
|
24
|
-
throw new Error(`invalid manually accepted development preview: ${acceptedVersion}`);
|
|
25
|
-
}
|
|
26
|
-
if (version !== acceptedVersion) {
|
|
27
|
-
throw new Error(`development preview ${version} has no exact manual acceptance; accepted version is ${acceptedVersion}`);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
export function selectDevelopmentPreviewCandidate(currentVersion, publishedVersions) {
|
|
31
|
-
if (valid(currentVersion) === null)
|
|
32
|
-
throw new Error(`invalid current package version: ${currentVersion}`);
|
|
33
|
-
const published = publishedVersions.map(version => {
|
|
34
|
-
if (valid(version) === null)
|
|
35
|
-
throw new Error(`invalid published package version: ${version}`);
|
|
36
|
-
return version;
|
|
37
|
-
});
|
|
38
|
-
const publishedSet = new Set(published);
|
|
39
|
-
const highestPublished = published.reduce((highest, version) => highest === null || compare(version, highest) > 0 ? version : highest, null);
|
|
40
|
-
const currentPrerelease = prerelease(currentVersion);
|
|
41
|
-
const currentIsUnpublishedLeadingDev = currentPrerelease?.[0] === "dev"
|
|
42
|
-
&& !publishedSet.has(currentVersion)
|
|
43
|
-
&& (highestPublished === null || compare(currentVersion, highestPublished) > 0);
|
|
44
|
-
if (currentIsUnpublishedLeadingDev)
|
|
45
|
-
return { version: currentVersion, requiresVersionCommit: false };
|
|
46
|
-
const base = highestPublished === null || compare(currentVersion, highestPublished) > 0
|
|
47
|
-
? currentVersion
|
|
48
|
-
: highestPublished;
|
|
49
|
-
const basePrerelease = prerelease(base);
|
|
50
|
-
let candidate = basePrerelease?.[0] === "dev"
|
|
51
|
-
? inc(base, "prerelease", "dev")
|
|
52
|
-
: inc(base, "prepatch", "dev");
|
|
53
|
-
if (candidate === null)
|
|
54
|
-
throw new Error(`could not increment development preview from ${base}`);
|
|
55
|
-
while (publishedSet.has(candidate)) {
|
|
56
|
-
candidate = inc(candidate, "prerelease", "dev");
|
|
57
|
-
if (candidate === null)
|
|
58
|
-
throw new Error(`could not increment development preview from ${base}`);
|
|
59
|
-
}
|
|
60
|
-
return { version: candidate, requiresVersionCommit: candidate !== currentVersion };
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Treats npm's process result as provisional: browser-auth completion can fail
|
|
64
|
-
* after the immutable upload succeeds. Registry identity remains authoritative.
|
|
65
|
-
*/
|
|
66
|
-
export async function publishDevelopmentPreviewWithRecovery(publish, verify) {
|
|
67
|
-
let publishError = null;
|
|
68
|
-
try {
|
|
69
|
-
await publish();
|
|
70
|
-
}
|
|
71
|
-
catch (error) {
|
|
72
|
-
publishError = error;
|
|
73
|
-
}
|
|
74
|
-
try {
|
|
75
|
-
await verify();
|
|
76
|
-
return { published: true, recoveredPublishError: publishError };
|
|
77
|
-
}
|
|
78
|
-
catch (verificationError) {
|
|
79
|
-
if (publishError !== null)
|
|
80
|
-
throw new AggregateError([publishError, verificationError], "npm publish failed and the exact version could not be verified in the registry");
|
|
81
|
-
throw verificationError;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
export async function verifyDevelopmentPreviewRegistry(version, observe, repairNextTag, options = {}) {
|
|
85
|
-
const attempts = options.attempts ?? 12;
|
|
86
|
-
const delayMs = options.delayMs ?? 2_000;
|
|
87
|
-
const delay = options.delay ?? (async (milliseconds) => await new Promise(resolvePromise => setTimeout(resolvePromise, milliseconds)));
|
|
88
|
-
if (!Number.isInteger(attempts) || attempts < 1)
|
|
89
|
-
throw new Error(`invalid registry verification attempts: ${attempts}`);
|
|
90
|
-
let repaired = false;
|
|
91
|
-
let last = { published: false, nextVersion: null };
|
|
92
|
-
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
93
|
-
last = await observe();
|
|
94
|
-
if (last.published && last.nextVersion === version)
|
|
95
|
-
return;
|
|
96
|
-
if (last.published && !repaired) {
|
|
97
|
-
await repairNextTag();
|
|
98
|
-
repaired = true;
|
|
99
|
-
}
|
|
100
|
-
if (attempt < attempts)
|
|
101
|
-
await delay(delayMs);
|
|
102
|
-
}
|
|
103
|
-
if (!last.published)
|
|
104
|
-
throw new Error(`npm registry did not expose published version ${version} after ${attempts} attempts`);
|
|
105
|
-
throw new Error(`npm next resolved ${last.nextVersion ?? "nothing"}; expected ${version} after ${attempts} attempts`);
|
|
106
|
-
}
|
|
107
|
-
export function developmentPreviewTarballName(packageName, version) {
|
|
108
|
-
if (valid(version) === null)
|
|
109
|
-
throw new Error(`invalid development preview version: ${version}`);
|
|
110
|
-
const unscopedName = packageName.startsWith("@") ? packageName.slice(1).replace("/", "-") : packageName;
|
|
111
|
-
if (!/^[a-z0-9._-]+$/i.test(unscopedName))
|
|
112
|
-
throw new Error(`invalid package name: ${packageName}`);
|
|
113
|
-
return `${unscopedName}-${version}.tgz`;
|
|
114
|
-
}
|