@timurproko/a1 0.1.8-dev.99e912e → 0.1.8-dev.9a04818
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 +11 -7
- 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/shell-presenters-transcript.js +2 -1
- package/dist/src/foundation/pi-engine-adapter/adapter.js +95 -22
- package/dist/src/foundation/pi-owned-ui-integration/session-shell.js +5 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,16 +30,20 @@ Pi extension packages install into A1's own profile, so bare `a1` loads them and
|
|
|
30
30
|
`a1 pi` and `a1 sandbox` do not. Sources are Pi's: `npm:`, git, or a local path.
|
|
31
31
|
|
|
32
32
|
```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
|
|
33
|
+
a1 pi install npm:pi-mcp-adapter # install a package into ~/.a1/agent
|
|
34
|
+
a1 pi remove npm:pi-mcp-adapter # remove it again (alias: a1 pi uninstall)
|
|
35
|
+
a1 pi list # list packages installed for a1
|
|
36
|
+
a1 pi update --extensions # update every installed package
|
|
37
|
+
a1 pi update npm:pi-mcp-adapter # update one of them
|
|
38
|
+
a1 update --models # refresh A1's model catalogs
|
|
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-25T07:10:19.265Z",
|
|
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-25T07:10:17.791Z",
|
|
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-25T07:11:29.829Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian.exe",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "2c1bb33c431cec4d90f29ff5fdf6575a62e19f868a8d1dd4bc09f097d60d5550",
|
|
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",
|
|
@@ -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)));
|
|
@@ -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,
|
|
@@ -61,6 +67,8 @@ export class PiEngineAdapter {
|
|
|
61
67
|
#eventQueue = [];
|
|
62
68
|
#eventQueueProcessing;
|
|
63
69
|
#droppedEventCount = 0;
|
|
70
|
+
#agentRunActive = false;
|
|
71
|
+
#statusKind = null;
|
|
64
72
|
#sessionCommands;
|
|
65
73
|
#gitBranch = null;
|
|
66
74
|
#extensionUi;
|
|
@@ -143,7 +151,7 @@ export class PiEngineAdapter {
|
|
|
143
151
|
if (this.#disposed || updates.length === 0)
|
|
144
152
|
return;
|
|
145
153
|
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);
|
|
154
|
+
this.#addDiagnostic("info", "package-updates", `Package updates are available. Run ${PRODUCT_IDENTITY.commandName} pi update --extensions\nPackages:\n${packages}`, true);
|
|
147
155
|
this.#emitView();
|
|
148
156
|
}
|
|
149
157
|
onEvent(listener) {
|
|
@@ -373,9 +381,10 @@ export class PiEngineAdapter {
|
|
|
373
381
|
}
|
|
374
382
|
const extensionCommands = this.#session?.extensionRunner?.getRegisteredCommands?.();
|
|
375
383
|
if (Array.isArray(extensionCommands)) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
384
|
+
const registered = extensionCommands.filter(isRecord);
|
|
385
|
+
for (const command of registered) {
|
|
386
|
+
const name = stringProperty(command, "invocationName") ?? stringProperty(command, "name");
|
|
387
|
+
if (!name || usedNames.has(name) || isPiPrefixedCompatibilityAlias(command, registered))
|
|
379
388
|
continue;
|
|
380
389
|
commands.push({ name, description: stringProperty(command, "description") ?? "Extension command", source: "extension" });
|
|
381
390
|
usedNames.add(name);
|
|
@@ -1350,6 +1359,8 @@ export class PiEngineAdapter {
|
|
|
1350
1359
|
submitEnabled: true,
|
|
1351
1360
|
};
|
|
1352
1361
|
this.#status = { ...this.#status, workingMessage: null, badges: [] };
|
|
1362
|
+
this.#statusKind = null;
|
|
1363
|
+
this.#agentRunActive = false;
|
|
1353
1364
|
this.#activeModel = readModel(session.model);
|
|
1354
1365
|
this.#reconcileActiveModelAvailability();
|
|
1355
1366
|
this.#thinkingLevel = readThinkingLevel(session.thinkingLevel);
|
|
@@ -1402,15 +1413,45 @@ export class PiEngineAdapter {
|
|
|
1402
1413
|
this.#emitView();
|
|
1403
1414
|
}
|
|
1404
1415
|
}
|
|
1416
|
+
/** Shows the state named by `kind`, which becomes the state a later end can clear. */
|
|
1417
|
+
#enterWorkState(kind, message) {
|
|
1418
|
+
const wasBusy = this.#lifecycle === "busy";
|
|
1419
|
+
this.#statusKind = kind;
|
|
1420
|
+
this.#lifecycle = "busy";
|
|
1421
|
+
this.#status = { ...this.#status, workingMessage: message };
|
|
1422
|
+
if (!wasBusy)
|
|
1423
|
+
this.#emitEvent({ type: "session-lifecycle", lifecycle: "busy", reason: null });
|
|
1424
|
+
this.#emitEvent({ type: "status", status: this.#status });
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Ends one named state. A state the shell is not in is left alone, so a finished
|
|
1428
|
+
* compaction or retry cannot clear the working state it never replaced. While the run
|
|
1429
|
+
* continues, ending either of those returns to working rather than to idle.
|
|
1430
|
+
*/
|
|
1431
|
+
#endWorkState(kind) {
|
|
1432
|
+
if (this.#statusKind !== kind)
|
|
1433
|
+
return;
|
|
1434
|
+
if (this.#agentRunActive) {
|
|
1435
|
+
this.#enterWorkState("working", "Working...");
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
this.#leaveWorkStates();
|
|
1439
|
+
}
|
|
1440
|
+
/** Leaves every work state and reports the session idle. */
|
|
1441
|
+
#leaveWorkStates() {
|
|
1442
|
+
this.#statusKind = null;
|
|
1443
|
+
this.#lifecycle = "ready";
|
|
1444
|
+
this.#status = { ...this.#status, workingMessage: null };
|
|
1445
|
+
this.#emitEvent({ type: "session-lifecycle", lifecycle: "ready", reason: null });
|
|
1446
|
+
this.#emitEvent({ type: "status", status: this.#status });
|
|
1447
|
+
}
|
|
1405
1448
|
#handlePiEvent(event) {
|
|
1406
1449
|
if (!isRecord(event) || typeof event.type !== "string")
|
|
1407
1450
|
return;
|
|
1408
1451
|
switch (event.type) {
|
|
1409
1452
|
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 });
|
|
1453
|
+
this.#agentRunActive = true;
|
|
1454
|
+
this.#enterWorkState("working", "Working...");
|
|
1414
1455
|
return;
|
|
1415
1456
|
case "message_start":
|
|
1416
1457
|
this.#upsertMessageBlock(event.message, "live");
|
|
@@ -1453,10 +1494,17 @@ export class PiEngineAdapter {
|
|
|
1453
1494
|
this.#rebuildTranscript(finalMessages, "finalized");
|
|
1454
1495
|
else
|
|
1455
1496
|
this.#transcript = this.#transcript.map(block => block.status === "live" ? { ...block, status: "finalized" } : block);
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1497
|
+
// Ending a turn leaves the working state, as the recorded pinned baseline does, but
|
|
1498
|
+
// it leaves only that state: a compaction or retry being shown outlives the turn
|
|
1499
|
+
// that ended under it. Settlement ends the run, and with it every state — the
|
|
1500
|
+
// engine ends a turn for each continuation it makes and settles once.
|
|
1501
|
+
if (event.type === "agent_settled") {
|
|
1502
|
+
this.#agentRunActive = false;
|
|
1503
|
+
this.#leaveWorkStates();
|
|
1504
|
+
}
|
|
1505
|
+
else if (this.#statusKind === null || this.#statusKind === "working") {
|
|
1506
|
+
this.#leaveWorkStates();
|
|
1507
|
+
}
|
|
1460
1508
|
this.#emitView();
|
|
1461
1509
|
return;
|
|
1462
1510
|
}
|
|
@@ -1472,20 +1520,16 @@ export class PiEngineAdapter {
|
|
|
1472
1520
|
return;
|
|
1473
1521
|
}
|
|
1474
1522
|
case "auto_retry_start":
|
|
1475
|
-
this.#
|
|
1476
|
-
this.#status = { ...this.#status, workingMessage: "Retrying…" };
|
|
1477
|
-
this.#emitEvent({ type: "status", status: this.#status });
|
|
1523
|
+
this.#enterWorkState("retry", "Retrying…");
|
|
1478
1524
|
return;
|
|
1479
1525
|
case "auto_retry_end":
|
|
1480
|
-
|
|
1481
|
-
this.#lifecycle = "ready";
|
|
1482
|
-
this.#status = { ...this.#status, workingMessage: null };
|
|
1483
|
-
this.#emitEvent({ type: "status", status: this.#status });
|
|
1526
|
+
this.#endWorkState("retry");
|
|
1484
1527
|
return;
|
|
1485
1528
|
case "compaction_start":
|
|
1486
|
-
this.#
|
|
1487
|
-
|
|
1488
|
-
|
|
1529
|
+
this.#enterWorkState("compaction", "Compacting…");
|
|
1530
|
+
return;
|
|
1531
|
+
case "compaction_end":
|
|
1532
|
+
this.#endWorkState("compaction");
|
|
1489
1533
|
return;
|
|
1490
1534
|
case "thinking_level_changed":
|
|
1491
1535
|
this.#thinkingLevel = readThinkingLevel(event.level);
|
|
@@ -1847,6 +1891,7 @@ export class PiEngineAdapter {
|
|
|
1847
1891
|
}
|
|
1848
1892
|
async #processEventQueue() {
|
|
1849
1893
|
try {
|
|
1894
|
+
let deliveredSinceYield = 0;
|
|
1850
1895
|
while (this.#eventQueue.length > 0) {
|
|
1851
1896
|
const event = this.#eventQueue.shift();
|
|
1852
1897
|
if (!event)
|
|
@@ -1859,6 +1904,14 @@ export class PiEngineAdapter {
|
|
|
1859
1904
|
this.#recordDiagnostic("warning", "event-listener", error instanceof Error ? error.message : String(error), true);
|
|
1860
1905
|
}
|
|
1861
1906
|
}
|
|
1907
|
+
deliveredSinceYield += 1;
|
|
1908
|
+
// A microtask chain runs to exhaustion before the loop turns, so a streaming
|
|
1909
|
+
// burst would hold typed input, pointer reports, and timed indicators until it
|
|
1910
|
+
// drained. Yielding on a macrotask hands those their turn between batches.
|
|
1911
|
+
if (deliveredSinceYield >= EVENT_DELIVERY_BATCH && this.#eventQueue.length > 0) {
|
|
1912
|
+
deliveredSinceYield = 0;
|
|
1913
|
+
await new Promise(resolve => { setImmediate(resolve); });
|
|
1914
|
+
}
|
|
1862
1915
|
}
|
|
1863
1916
|
}
|
|
1864
1917
|
finally {
|
|
@@ -2274,6 +2327,26 @@ function stringProperty(value, key) {
|
|
|
2274
2327
|
const item = value[key];
|
|
2275
2328
|
return typeof item === "string" && item.length > 0 ? item : undefined;
|
|
2276
2329
|
}
|
|
2330
|
+
/**
|
|
2331
|
+
* Some ecosystem extensions retain a `pi-<name>` slash-command alias beside
|
|
2332
|
+
* their unprefixed command. A1 presents the product-neutral command once while
|
|
2333
|
+
* leaving Pi's runner free to accept the compatibility alias when typed.
|
|
2334
|
+
*/
|
|
2335
|
+
function isPiPrefixedCompatibilityAlias(command, commands) {
|
|
2336
|
+
const name = stringProperty(command, "name");
|
|
2337
|
+
if (!name?.startsWith("pi-") || name.length === 3)
|
|
2338
|
+
return false;
|
|
2339
|
+
const canonicalName = name.slice(3);
|
|
2340
|
+
const description = stringProperty(command, "description");
|
|
2341
|
+
const sourcePath = extensionCommandSourcePath(command);
|
|
2342
|
+
return commands.some(candidate => candidate !== command
|
|
2343
|
+
&& stringProperty(candidate, "name") === canonicalName
|
|
2344
|
+
&& stringProperty(candidate, "description") === description
|
|
2345
|
+
&& extensionCommandSourcePath(candidate) === sourcePath);
|
|
2346
|
+
}
|
|
2347
|
+
function extensionCommandSourcePath(command) {
|
|
2348
|
+
return isRecord(command) ? stringProperty(command.sourceInfo, "path") : undefined;
|
|
2349
|
+
}
|
|
2277
2350
|
function compactResourceLabel(path) {
|
|
2278
2351
|
const segments = path.replaceAll("\\", "/").split("/").filter(Boolean);
|
|
2279
2352
|
return segments.at(-1) ?? path;
|
|
@@ -638,8 +638,10 @@ export class OwnedUiSessionShell {
|
|
|
638
638
|
});
|
|
639
639
|
this.root.editor.setAutocompleteCommands(this.backend.workflowAutocompleteCommands());
|
|
640
640
|
this.#unsubscribe = this.backend.onEvent(event => {
|
|
641
|
-
|
|
642
|
-
|
|
641
|
+
// One view read per event: the model is built by the backend, and building it
|
|
642
|
+
// twice per streamed chunk is what made a long session cost more per chunk.
|
|
643
|
+
const view = this.#syncView();
|
|
644
|
+
if (view.lifecycle === "ready" && this.#compactionQueue.length > 0)
|
|
643
645
|
void this.#flushCompactionQueue();
|
|
644
646
|
if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
|
|
645
647
|
this.#resolveStopped?.();
|
|
@@ -1257,6 +1259,7 @@ export class OwnedUiSessionShell {
|
|
|
1257
1259
|
this.runtime.requestRender();
|
|
1258
1260
|
for (const listener of this.#listeners)
|
|
1259
1261
|
listener(view);
|
|
1262
|
+
return view;
|
|
1260
1263
|
}
|
|
1261
1264
|
#openOwnedRoute(route) {
|
|
1262
1265
|
const surface = this.#routeHost?.open(route) ?? null;
|
package/package.json
CHANGED