@timurproko/a1 0.1.8-dev.ade9b77 → 0.1.8-dev.c64be0e
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 +5 -5
- 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/packages.d.ts +9 -1
- package/dist/src/cli/packages.js +24 -20
- 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-engine-adapter/adapter.d.ts +7 -0
- package/dist/src/foundation/pi-engine-adapter/adapter.js +168 -35
- 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/package.json +2 -1
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
|
|
@@ -35,7 +36,6 @@ a1 pi remove npm:pi-mcp-adapter # remove it again (alias: a1 pi uninstall)
|
|
|
35
36
|
a1 pi list # list packages installed for a1
|
|
36
37
|
a1 pi update --extensions # update every installed package
|
|
37
38
|
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
|
|
@@ -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:31:32.388Z",
|
|
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:31:49.348Z",
|
|
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:32:11.007Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian.exe",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "b22997c85da32360cdb382f568e618757654c746cccb825b7c771d011cca9ba6",
|
|
12
12
|
"size": 172544
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
Binary file
|
|
@@ -11,6 +11,12 @@ export interface PackageCommandRequest {
|
|
|
11
11
|
readonly verb: PackageCommandVerb;
|
|
12
12
|
readonly source: string | null;
|
|
13
13
|
}
|
|
14
|
+
export interface PackageCommandStyle {
|
|
15
|
+
readonly dim: (message: string) => string;
|
|
16
|
+
readonly bold: (message: string) => string;
|
|
17
|
+
readonly green: (message: string) => string;
|
|
18
|
+
readonly red: (message: string) => string;
|
|
19
|
+
}
|
|
14
20
|
export interface PackageCommandEnvironment {
|
|
15
21
|
readonly createPort: (input: AgentPackagesPortInput) => AgentPackagesPort;
|
|
16
22
|
readonly cwd?: string;
|
|
@@ -18,6 +24,8 @@ export interface PackageCommandEnvironment {
|
|
|
18
24
|
readonly stdout?: (message: string) => void;
|
|
19
25
|
readonly stderr?: (message: string) => void;
|
|
20
26
|
readonly initializeProfile?: typeof initializeProductProfile;
|
|
27
|
+
/** Defaults to Chalk's terminal-aware styles; injectable for transcript tests. */
|
|
28
|
+
readonly style?: PackageCommandStyle;
|
|
21
29
|
}
|
|
22
30
|
export declare function runPackageCommand(request: PackageCommandRequest, environment: PackageCommandEnvironment): Promise<number>;
|
|
23
|
-
export declare function renderPackageOutcome(outcome: AgentPackageOutcome, profileRoot: string): string;
|
|
31
|
+
export declare function renderPackageOutcome(outcome: AgentPackageOutcome, profileRoot: string, style?: PackageCommandStyle): string;
|
package/dist/src/cli/packages.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
1
2
|
import { configurationRootForProfile, initializeProductProfile, resolveLaunchProfilePaths } from "../features/launch/index.js";
|
|
2
3
|
import { PRODUCT_TEXT } from "../product-identity.js";
|
|
3
4
|
export async function runPackageCommand(request, environment) {
|
|
@@ -5,6 +6,7 @@ export async function runPackageCommand(request, environment) {
|
|
|
5
6
|
const stderr = environment.stderr ?? (message => process.stderr.write(message));
|
|
6
7
|
const cwd = environment.cwd ?? process.cwd();
|
|
7
8
|
const processEnvironment = environment.environment ?? process.env;
|
|
9
|
+
const style = environment.style ?? chalk;
|
|
8
10
|
const paths = resolveLaunchProfilePaths({ environment: processEnvironment });
|
|
9
11
|
const profileRoot = configurationRootForProfile("a1", paths);
|
|
10
12
|
if (profileRoot === null)
|
|
@@ -19,10 +21,10 @@ export async function runPackageCommand(request, environment) {
|
|
|
19
21
|
const port = environment.createPort({
|
|
20
22
|
profileRoot,
|
|
21
23
|
cwd,
|
|
22
|
-
onProgress: progress => stdout(`${progress.message}\n`),
|
|
24
|
+
onProgress: progress => stdout(`${style.dim(progress.message)}\n`),
|
|
23
25
|
});
|
|
24
26
|
const outcome = await runVerb(port, request);
|
|
25
|
-
const rendered = renderPackageOutcome(outcome, profileRoot);
|
|
27
|
+
const rendered = renderPackageOutcome(outcome, profileRoot, style);
|
|
26
28
|
(outcome.status === "completed" ? stdout : stderr)(rendered);
|
|
27
29
|
return outcome.status === "completed" ? 0 : 1;
|
|
28
30
|
}
|
|
@@ -37,38 +39,40 @@ async function runVerb(port, request) {
|
|
|
37
39
|
throw new Error(PRODUCT_TEXT.diagnostic(`requires a source for ${request.verb}`));
|
|
38
40
|
return request.verb === "install" ? await port.install(request.source) : await port.remove(request.source);
|
|
39
41
|
}
|
|
40
|
-
export function renderPackageOutcome(outcome, profileRoot) {
|
|
41
|
-
|
|
42
|
+
export function renderPackageOutcome(outcome, profileRoot, style = chalk) {
|
|
43
|
+
// Model refresh remains an A1 top-level command. The `a1 pi` compatibility
|
|
44
|
+
// transcript applies to package operations only.
|
|
45
|
+
if (outcome.operation === "refresh-models") {
|
|
46
|
+
if (outcome.status === "failed") {
|
|
47
|
+
return `${PRODUCT_TEXT.diagnostic(`could not ${describeOperation(outcome.operation)}: ${outcome.detail ?? "unknown failure"}`)}\n`;
|
|
48
|
+
}
|
|
49
|
+
return `${PRODUCT_TEXT.displayName} refreshed the model catalogs in ${profileRoot}.\n`;
|
|
50
|
+
}
|
|
42
51
|
if (outcome.status === "failed") {
|
|
43
|
-
return `${
|
|
52
|
+
return `${style.red(`Error: ${outcome.detail ?? "Unknown package command error"}`)}\n`;
|
|
44
53
|
}
|
|
45
54
|
if (outcome.status === "not-found") {
|
|
46
|
-
return `${
|
|
55
|
+
return `${style.red(`No matching package found for ${outcome.source ?? "that source"}`)}\n`;
|
|
47
56
|
}
|
|
48
57
|
switch (outcome.operation) {
|
|
49
58
|
case "install":
|
|
50
|
-
return `${
|
|
51
|
-
+ `Restart ${PRODUCT_TEXT.commandName} for a running session to load it.\n`;
|
|
59
|
+
return `${style.green(`Installed ${outcome.source}`)}\n`;
|
|
52
60
|
case "remove":
|
|
53
|
-
return `${
|
|
61
|
+
return `${style.green(`Removed ${outcome.source}`)}\n`;
|
|
54
62
|
case "update":
|
|
55
|
-
return outcome.source === null
|
|
56
|
-
? `${name} updated the packages in ${profileRoot}.\n`
|
|
57
|
-
: `${name} updated ${outcome.source}.\n`;
|
|
58
|
-
case "refresh-models":
|
|
59
|
-
return `${name} refreshed the model catalogs in ${profileRoot}.\n`;
|
|
63
|
+
return `${style.green(outcome.source === null ? "Updated packages" : `Updated ${outcome.source}`)}\n`;
|
|
60
64
|
case "list":
|
|
61
|
-
return renderPackageList(outcome,
|
|
65
|
+
return renderPackageList(outcome, style);
|
|
62
66
|
}
|
|
63
67
|
}
|
|
64
|
-
function renderPackageList(outcome,
|
|
68
|
+
function renderPackageList(outcome, style) {
|
|
65
69
|
if (outcome.packages.length === 0)
|
|
66
|
-
return `${
|
|
67
|
-
const lines = [
|
|
70
|
+
return `${style.dim("No packages installed.")}\n`;
|
|
71
|
+
const lines = [style.bold("User packages:")];
|
|
68
72
|
for (const entry of outcome.packages) {
|
|
69
|
-
lines.push(` ${entry.source}${entry.filtered ? " (
|
|
73
|
+
lines.push(` ${entry.source}${entry.filtered ? " (filtered)" : ""}`);
|
|
70
74
|
if (entry.installedPath !== null)
|
|
71
|
-
lines.push(` ${entry.installedPath}`);
|
|
75
|
+
lines.push(style.dim(` ${entry.installedPath}`));
|
|
72
76
|
}
|
|
73
77
|
return `${lines.join("\n")}\n`;
|
|
74
78
|
}
|
|
@@ -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";
|
|
@@ -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;
|
|
@@ -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,
|
|
@@ -725,7 +736,7 @@ export class PiEngineAdapter {
|
|
|
725
736
|
sessionId: this.#sessionId,
|
|
726
737
|
revision: this.#viewRevision,
|
|
727
738
|
lifecycle: this.#lifecycle,
|
|
728
|
-
transcript: this.#
|
|
739
|
+
transcript: this.#transcriptSnapshot ??= Object.freeze([...this.#transcript]),
|
|
729
740
|
editor: { ...this.#editor, queuedSubmissions: [...this.#editor.queuedSubmissions] },
|
|
730
741
|
status: {
|
|
731
742
|
...this.#status,
|
|
@@ -1351,6 +1362,8 @@ export class PiEngineAdapter {
|
|
|
1351
1362
|
submitEnabled: true,
|
|
1352
1363
|
};
|
|
1353
1364
|
this.#status = { ...this.#status, workingMessage: null, badges: [] };
|
|
1365
|
+
this.#statusKind = null;
|
|
1366
|
+
this.#agentRunActive = false;
|
|
1354
1367
|
this.#activeModel = readModel(session.model);
|
|
1355
1368
|
this.#reconcileActiveModelAvailability();
|
|
1356
1369
|
this.#thinkingLevel = readThinkingLevel(session.thinkingLevel);
|
|
@@ -1403,28 +1416,76 @@ export class PiEngineAdapter {
|
|
|
1403
1416
|
this.#emitView();
|
|
1404
1417
|
}
|
|
1405
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
|
+
}
|
|
1406
1467
|
#handlePiEvent(event) {
|
|
1407
1468
|
if (!isRecord(event) || typeof event.type !== "string")
|
|
1408
1469
|
return;
|
|
1409
1470
|
switch (event.type) {
|
|
1410
1471
|
case "agent_start":
|
|
1411
|
-
this.#
|
|
1412
|
-
this.#
|
|
1413
|
-
this.#emitEvent({ type: "session-lifecycle", lifecycle: "busy", reason: null });
|
|
1414
|
-
this.#emitEvent({ type: "status", status: this.#status });
|
|
1472
|
+
this.#agentRunActive = true;
|
|
1473
|
+
this.#enterWorkState("working", "Working...");
|
|
1415
1474
|
return;
|
|
1416
1475
|
case "message_start":
|
|
1417
1476
|
this.#upsertMessageBlock(event.message, "live");
|
|
1418
1477
|
return;
|
|
1419
1478
|
case "message_update": {
|
|
1420
|
-
const
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
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);
|
|
1428
1489
|
}
|
|
1429
1490
|
return;
|
|
1430
1491
|
}
|
|
@@ -1453,11 +1514,18 @@ export class PiEngineAdapter {
|
|
|
1453
1514
|
if (finalMessages.length > 0)
|
|
1454
1515
|
this.#rebuildTranscript(finalMessages, "finalized");
|
|
1455
1516
|
else
|
|
1456
|
-
this.#
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
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
|
+
}
|
|
1461
1529
|
this.#emitView();
|
|
1462
1530
|
return;
|
|
1463
1531
|
}
|
|
@@ -1473,20 +1541,16 @@ export class PiEngineAdapter {
|
|
|
1473
1541
|
return;
|
|
1474
1542
|
}
|
|
1475
1543
|
case "auto_retry_start":
|
|
1476
|
-
this.#
|
|
1477
|
-
this.#status = { ...this.#status, workingMessage: "Retrying…" };
|
|
1478
|
-
this.#emitEvent({ type: "status", status: this.#status });
|
|
1544
|
+
this.#enterWorkState("retry", "Retrying…");
|
|
1479
1545
|
return;
|
|
1480
1546
|
case "auto_retry_end":
|
|
1481
|
-
|
|
1482
|
-
this.#lifecycle = "ready";
|
|
1483
|
-
this.#status = { ...this.#status, workingMessage: null };
|
|
1484
|
-
this.#emitEvent({ type: "status", status: this.#status });
|
|
1547
|
+
this.#endWorkState("retry");
|
|
1485
1548
|
return;
|
|
1486
1549
|
case "compaction_start":
|
|
1487
|
-
this.#
|
|
1488
|
-
|
|
1489
|
-
|
|
1550
|
+
this.#enterWorkState("compaction", "Compacting…");
|
|
1551
|
+
return;
|
|
1552
|
+
case "compaction_end":
|
|
1553
|
+
this.#endWorkState("compaction");
|
|
1490
1554
|
return;
|
|
1491
1555
|
case "thinking_level_changed":
|
|
1492
1556
|
this.#thinkingLevel = readThinkingLevel(event.level);
|
|
@@ -1576,7 +1640,13 @@ export class PiEngineAdapter {
|
|
|
1576
1640
|
}
|
|
1577
1641
|
}
|
|
1578
1642
|
}
|
|
1579
|
-
|
|
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
|
+
}));
|
|
1580
1650
|
}
|
|
1581
1651
|
#upsertMessageBlock(message, status) {
|
|
1582
1652
|
const blocks = this.#messageBlocks(message, status, this.#transcript.length);
|
|
@@ -1661,7 +1731,7 @@ export class PiEngineAdapter {
|
|
|
1661
1731
|
: this.#toolBlockIds.get(toolCallId) ?? `tool-${toolCallId}`;
|
|
1662
1732
|
if (toolCallId !== undefined)
|
|
1663
1733
|
this.#toolBlockIds.set(toolCallId, blockId);
|
|
1664
|
-
const existing = this.#
|
|
1734
|
+
const existing = this.#transcriptBlock(blockId);
|
|
1665
1735
|
const existingPayload = isRecord(existing?.payload) ? existing.payload : undefined;
|
|
1666
1736
|
return [{
|
|
1667
1737
|
id: blockId,
|
|
@@ -1752,11 +1822,20 @@ export class PiEngineAdapter {
|
|
|
1752
1822
|
});
|
|
1753
1823
|
}
|
|
1754
1824
|
#upsertTranscriptBlock(block) {
|
|
1755
|
-
const index = this.#
|
|
1756
|
-
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;
|
|
1757
1832
|
this.#transcript[index] = block;
|
|
1758
|
-
|
|
1833
|
+
}
|
|
1834
|
+
else {
|
|
1835
|
+
this.#transcriptIndex.set(block.id, this.#transcript.length);
|
|
1759
1836
|
this.#transcript.push(block);
|
|
1837
|
+
}
|
|
1838
|
+
this.#transcriptSnapshot = undefined;
|
|
1760
1839
|
this.#emitEvent({ type: "transcript-block", block });
|
|
1761
1840
|
}
|
|
1762
1841
|
#messageBlockId(message, fallbackIndex, status, occurrence) {
|
|
@@ -1774,7 +1853,7 @@ export class PiEngineAdapter {
|
|
|
1774
1853
|
}
|
|
1775
1854
|
}
|
|
1776
1855
|
else {
|
|
1777
|
-
id = [...cached].reverse().find(candidate => this.#
|
|
1856
|
+
id = [...cached].reverse().find(candidate => this.#transcriptBlock(candidate)?.status === "live");
|
|
1778
1857
|
if (id === undefined && status === "finalized")
|
|
1779
1858
|
id = cached.at(-1);
|
|
1780
1859
|
if (id === undefined) {
|
|
@@ -1787,7 +1866,7 @@ export class PiEngineAdapter {
|
|
|
1787
1866
|
return id;
|
|
1788
1867
|
}
|
|
1789
1868
|
#nextBlockRevision(id) {
|
|
1790
|
-
const existing = this.#
|
|
1869
|
+
const existing = this.#transcriptBlock(id);
|
|
1791
1870
|
if (existing)
|
|
1792
1871
|
return existing.revision + 1;
|
|
1793
1872
|
this.#nextBlockSequence += 1;
|
|
@@ -1848,6 +1927,7 @@ export class PiEngineAdapter {
|
|
|
1848
1927
|
}
|
|
1849
1928
|
async #processEventQueue() {
|
|
1850
1929
|
try {
|
|
1930
|
+
let deliveredSinceYield = 0;
|
|
1851
1931
|
while (this.#eventQueue.length > 0) {
|
|
1852
1932
|
const event = this.#eventQueue.shift();
|
|
1853
1933
|
if (!event)
|
|
@@ -1860,6 +1940,14 @@ export class PiEngineAdapter {
|
|
|
1860
1940
|
this.#recordDiagnostic("warning", "event-listener", error instanceof Error ? error.message : String(error), true);
|
|
1861
1941
|
}
|
|
1862
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
|
+
}
|
|
1863
1951
|
}
|
|
1864
1952
|
}
|
|
1865
1953
|
finally {
|
|
@@ -2246,11 +2334,26 @@ function extensionResourceDiagnostic(index, sourcePath, diagnostic) {
|
|
|
2246
2334
|
id: `extension-diagnostic-${index}`,
|
|
2247
2335
|
sourcePath,
|
|
2248
2336
|
resolvedPath: null,
|
|
2337
|
+
sourceInfo: null,
|
|
2249
2338
|
loaded: false,
|
|
2250
2339
|
hidden: false,
|
|
2251
2340
|
diagnostic,
|
|
2252
2341
|
};
|
|
2253
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
|
+
}
|
|
2254
2357
|
function collectionResult(value, key) {
|
|
2255
2358
|
if (!isRecord(value))
|
|
2256
2359
|
return { values: [], diagnostics: [] };
|
|
@@ -2299,6 +2402,36 @@ function compactResourceLabel(path) {
|
|
|
2299
2402
|
const segments = path.replaceAll("\\", "/").split("/").filter(Boolean);
|
|
2300
2403
|
return segments.at(-1) ?? path;
|
|
2301
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
|
+
}
|
|
2302
2435
|
function textFromContent(content) {
|
|
2303
2436
|
if (typeof content === "string")
|
|
2304
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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timurproko/a1",
|
|
3
|
-
"version": "0.1.8-dev.
|
|
3
|
+
"version": "0.1.8-dev.c64be0e",
|
|
4
4
|
"description": "Standalone terminal workspace for supervised native and managed agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@11.13.0",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@earendil-works/pi-coding-agent": "0.84.2",
|
|
59
59
|
"@earendil-works/pi-tui": "0.84.2",
|
|
60
|
+
"chalk": "5.6.2",
|
|
60
61
|
"cross-spawn": "7.0.6",
|
|
61
62
|
"grok-mermaid": "0.2.2",
|
|
62
63
|
"semver": "7.8.5"
|