@timurproko/a1 0.1.8-dev.ade9b77 → 0.1.8-dev.dbc8a9b
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/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/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 +116 -27
- package/dist/src/foundation/pi-owned-ui-integration/session-shell.js +189 -13
- package/package.json +1 -1
|
@@ -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:47:40.603Z",
|
|
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:47:45.696Z",
|
|
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:48:23.701Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian.exe",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "8a76f3b0814f4e98d3b71b2285be8b18f0ad95441cc35d14f023cd184bd293ac",
|
|
12
12
|
"size": 172544
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
Binary file
|
|
@@ -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,15 +1416,61 @@ 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");
|
|
@@ -1453,11 +1512,18 @@ export class PiEngineAdapter {
|
|
|
1453
1512
|
if (finalMessages.length > 0)
|
|
1454
1513
|
this.#rebuildTranscript(finalMessages, "finalized");
|
|
1455
1514
|
else
|
|
1456
|
-
this.#
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1515
|
+
this.#setTranscript(this.#transcript.map(block => block.status === "live" ? { ...block, status: "finalized" } : block));
|
|
1516
|
+
// Ending a turn leaves the working state, as the recorded pinned baseline does, but
|
|
1517
|
+
// it leaves only that state: a compaction or retry being shown outlives the turn
|
|
1518
|
+
// that ended under it. Settlement ends the run, and with it every state — the
|
|
1519
|
+
// engine ends a turn for each continuation it makes and settles once.
|
|
1520
|
+
if (event.type === "agent_settled") {
|
|
1521
|
+
this.#agentRunActive = false;
|
|
1522
|
+
this.#leaveWorkStates();
|
|
1523
|
+
}
|
|
1524
|
+
else if (this.#statusKind === null || this.#statusKind === "working") {
|
|
1525
|
+
this.#leaveWorkStates();
|
|
1526
|
+
}
|
|
1461
1527
|
this.#emitView();
|
|
1462
1528
|
return;
|
|
1463
1529
|
}
|
|
@@ -1473,20 +1539,16 @@ export class PiEngineAdapter {
|
|
|
1473
1539
|
return;
|
|
1474
1540
|
}
|
|
1475
1541
|
case "auto_retry_start":
|
|
1476
|
-
this.#
|
|
1477
|
-
this.#status = { ...this.#status, workingMessage: "Retrying…" };
|
|
1478
|
-
this.#emitEvent({ type: "status", status: this.#status });
|
|
1542
|
+
this.#enterWorkState("retry", "Retrying…");
|
|
1479
1543
|
return;
|
|
1480
1544
|
case "auto_retry_end":
|
|
1481
|
-
|
|
1482
|
-
this.#lifecycle = "ready";
|
|
1483
|
-
this.#status = { ...this.#status, workingMessage: null };
|
|
1484
|
-
this.#emitEvent({ type: "status", status: this.#status });
|
|
1545
|
+
this.#endWorkState("retry");
|
|
1485
1546
|
return;
|
|
1486
1547
|
case "compaction_start":
|
|
1487
|
-
this.#
|
|
1488
|
-
|
|
1489
|
-
|
|
1548
|
+
this.#enterWorkState("compaction", "Compacting…");
|
|
1549
|
+
return;
|
|
1550
|
+
case "compaction_end":
|
|
1551
|
+
this.#endWorkState("compaction");
|
|
1490
1552
|
return;
|
|
1491
1553
|
case "thinking_level_changed":
|
|
1492
1554
|
this.#thinkingLevel = readThinkingLevel(event.level);
|
|
@@ -1576,7 +1638,7 @@ export class PiEngineAdapter {
|
|
|
1576
1638
|
}
|
|
1577
1639
|
}
|
|
1578
1640
|
}
|
|
1579
|
-
this.#
|
|
1641
|
+
this.#setTranscript(blocks);
|
|
1580
1642
|
}
|
|
1581
1643
|
#upsertMessageBlock(message, status) {
|
|
1582
1644
|
const blocks = this.#messageBlocks(message, status, this.#transcript.length);
|
|
@@ -1661,7 +1723,7 @@ export class PiEngineAdapter {
|
|
|
1661
1723
|
: this.#toolBlockIds.get(toolCallId) ?? `tool-${toolCallId}`;
|
|
1662
1724
|
if (toolCallId !== undefined)
|
|
1663
1725
|
this.#toolBlockIds.set(toolCallId, blockId);
|
|
1664
|
-
const existing = this.#
|
|
1726
|
+
const existing = this.#transcriptBlock(blockId);
|
|
1665
1727
|
const existingPayload = isRecord(existing?.payload) ? existing.payload : undefined;
|
|
1666
1728
|
return [{
|
|
1667
1729
|
id: blockId,
|
|
@@ -1752,11 +1814,14 @@ export class PiEngineAdapter {
|
|
|
1752
1814
|
});
|
|
1753
1815
|
}
|
|
1754
1816
|
#upsertTranscriptBlock(block) {
|
|
1755
|
-
const index = this.#
|
|
1756
|
-
if (index
|
|
1817
|
+
const index = this.#transcriptIndex.get(block.id);
|
|
1818
|
+
if (index !== undefined)
|
|
1757
1819
|
this.#transcript[index] = block;
|
|
1758
|
-
else
|
|
1820
|
+
else {
|
|
1821
|
+
this.#transcriptIndex.set(block.id, this.#transcript.length);
|
|
1759
1822
|
this.#transcript.push(block);
|
|
1823
|
+
}
|
|
1824
|
+
this.#transcriptSnapshot = undefined;
|
|
1760
1825
|
this.#emitEvent({ type: "transcript-block", block });
|
|
1761
1826
|
}
|
|
1762
1827
|
#messageBlockId(message, fallbackIndex, status, occurrence) {
|
|
@@ -1774,7 +1839,7 @@ export class PiEngineAdapter {
|
|
|
1774
1839
|
}
|
|
1775
1840
|
}
|
|
1776
1841
|
else {
|
|
1777
|
-
id = [...cached].reverse().find(candidate => this.#
|
|
1842
|
+
id = [...cached].reverse().find(candidate => this.#transcriptBlock(candidate)?.status === "live");
|
|
1778
1843
|
if (id === undefined && status === "finalized")
|
|
1779
1844
|
id = cached.at(-1);
|
|
1780
1845
|
if (id === undefined) {
|
|
@@ -1787,7 +1852,7 @@ export class PiEngineAdapter {
|
|
|
1787
1852
|
return id;
|
|
1788
1853
|
}
|
|
1789
1854
|
#nextBlockRevision(id) {
|
|
1790
|
-
const existing = this.#
|
|
1855
|
+
const existing = this.#transcriptBlock(id);
|
|
1791
1856
|
if (existing)
|
|
1792
1857
|
return existing.revision + 1;
|
|
1793
1858
|
this.#nextBlockSequence += 1;
|
|
@@ -1848,6 +1913,7 @@ export class PiEngineAdapter {
|
|
|
1848
1913
|
}
|
|
1849
1914
|
async #processEventQueue() {
|
|
1850
1915
|
try {
|
|
1916
|
+
let deliveredSinceYield = 0;
|
|
1851
1917
|
while (this.#eventQueue.length > 0) {
|
|
1852
1918
|
const event = this.#eventQueue.shift();
|
|
1853
1919
|
if (!event)
|
|
@@ -1860,6 +1926,14 @@ export class PiEngineAdapter {
|
|
|
1860
1926
|
this.#recordDiagnostic("warning", "event-listener", error instanceof Error ? error.message : String(error), true);
|
|
1861
1927
|
}
|
|
1862
1928
|
}
|
|
1929
|
+
deliveredSinceYield += 1;
|
|
1930
|
+
// A microtask chain runs to exhaustion before the loop turns, so a streaming
|
|
1931
|
+
// burst would hold typed input, pointer reports, and timed indicators until it
|
|
1932
|
+
// drained. Yielding on a macrotask hands those their turn between batches.
|
|
1933
|
+
if (deliveredSinceYield >= EVENT_DELIVERY_BATCH && this.#eventQueue.length > 0) {
|
|
1934
|
+
deliveredSinceYield = 0;
|
|
1935
|
+
await new Promise(resolve => { setImmediate(resolve); });
|
|
1936
|
+
}
|
|
1863
1937
|
}
|
|
1864
1938
|
}
|
|
1865
1939
|
finally {
|
|
@@ -2246,11 +2320,26 @@ function extensionResourceDiagnostic(index, sourcePath, diagnostic) {
|
|
|
2246
2320
|
id: `extension-diagnostic-${index}`,
|
|
2247
2321
|
sourcePath,
|
|
2248
2322
|
resolvedPath: null,
|
|
2323
|
+
sourceInfo: null,
|
|
2249
2324
|
loaded: false,
|
|
2250
2325
|
hidden: false,
|
|
2251
2326
|
diagnostic,
|
|
2252
2327
|
};
|
|
2253
2328
|
}
|
|
2329
|
+
function extensionSourceSummary(value) {
|
|
2330
|
+
if (!isRecord(value))
|
|
2331
|
+
return null;
|
|
2332
|
+
const source = stringProperty(value, "source");
|
|
2333
|
+
const scope = value.scope;
|
|
2334
|
+
const origin = value.origin;
|
|
2335
|
+
const baseDir = value.baseDir;
|
|
2336
|
+
if (!source
|
|
2337
|
+
|| (scope !== "user" && scope !== "project" && scope !== "temporary")
|
|
2338
|
+
|| (origin !== "package" && origin !== "top-level")
|
|
2339
|
+
|| (baseDir !== undefined && typeof baseDir !== "string"))
|
|
2340
|
+
return null;
|
|
2341
|
+
return { source, scope, origin, baseDir: baseDir ?? null };
|
|
2342
|
+
}
|
|
2254
2343
|
function collectionResult(value, key) {
|
|
2255
2344
|
if (!isRecord(value))
|
|
2256
2345
|
return { values: [], diagnostics: [] };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseMouseInput } from "../ui-components/index.js";
|
|
2
2
|
import { PINNED_PI_HIDDEN_COMMAND_NAMES, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "../pi-engine-adapter/index.js";
|
|
3
|
-
import { createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellStatus, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, piTheme, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../pi-component-adapter/index.js";
|
|
3
|
+
import { createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellStatus, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, onPiThemeChange, piTheme, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../pi-component-adapter/index.js";
|
|
4
4
|
import { PiTuiRuntimeAdapter, } from "../pi-tui-runtime-adapter/index.js";
|
|
5
5
|
export class OwnedUiSessionShellRoot {
|
|
6
6
|
editor;
|
|
@@ -8,6 +8,9 @@ export class OwnedUiSessionShellRoot {
|
|
|
8
8
|
resources;
|
|
9
9
|
#cwd;
|
|
10
10
|
#transcript = new Map();
|
|
11
|
+
#blocksById = new Map();
|
|
12
|
+
#renderedRows = new Map();
|
|
13
|
+
#themeUnsubscribe;
|
|
11
14
|
#transcriptOrder = [];
|
|
12
15
|
#view;
|
|
13
16
|
#status;
|
|
@@ -54,6 +57,9 @@ export class OwnedUiSessionShellRoot {
|
|
|
54
57
|
this.editor.addToHistory(block.text);
|
|
55
58
|
}
|
|
56
59
|
this.#syncTranscript(view.transcript);
|
|
60
|
+
// Colours come from the active theme, so rendered rows outlive their revision only
|
|
61
|
+
// until the theme under them changes.
|
|
62
|
+
this.#themeUnsubscribe = onPiThemeChange(() => this.#renderedRows.clear());
|
|
57
63
|
}
|
|
58
64
|
update(view) {
|
|
59
65
|
this.#view = view;
|
|
@@ -128,10 +134,10 @@ export class OwnedUiSessionShellRoot {
|
|
|
128
134
|
}
|
|
129
135
|
#renderDocument(width) {
|
|
130
136
|
const transcript = this.#transcriptOrder.flatMap((id, index) => {
|
|
131
|
-
const block = this.#
|
|
137
|
+
const block = this.#blocksById.get(id);
|
|
132
138
|
if (!this.#thinkingVisible && block?.kind === "thinking")
|
|
133
139
|
return [];
|
|
134
|
-
const rows = this.#
|
|
140
|
+
const rows = this.#blockRows(id, block, width);
|
|
135
141
|
if (index > 0 && block?.kind === "user")
|
|
136
142
|
return ["", ...rows];
|
|
137
143
|
return rows;
|
|
@@ -167,6 +173,25 @@ export class OwnedUiSessionShellRoot {
|
|
|
167
173
|
...diagnosticRows,
|
|
168
174
|
];
|
|
169
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Rows for one transcript block. A finalized block renders once for a given revision
|
|
178
|
+
* and width and is reused after that, so a frame costs what changed rather than what
|
|
179
|
+
* the session has accumulated. A live block, and anything the shell drives itself, is
|
|
180
|
+
* rendered every time because its content is still moving.
|
|
181
|
+
*/
|
|
182
|
+
#blockRows(id, block, width) {
|
|
183
|
+
const component = this.#transcript.get(id);
|
|
184
|
+
if (component === undefined)
|
|
185
|
+
return [];
|
|
186
|
+
if (block === undefined || block.status !== "finalized")
|
|
187
|
+
return component.render(width);
|
|
188
|
+
const cached = this.#renderedRows.get(id);
|
|
189
|
+
if (cached && cached.width === width && cached.revision === block.revision)
|
|
190
|
+
return cached.rows;
|
|
191
|
+
const rows = component.render(width);
|
|
192
|
+
this.#renderedRows.set(id, { width, revision: block.revision, rows });
|
|
193
|
+
return rows;
|
|
194
|
+
}
|
|
170
195
|
transcriptComponent(id) {
|
|
171
196
|
return this.#transcript.get(id);
|
|
172
197
|
}
|
|
@@ -334,6 +359,8 @@ export class OwnedUiSessionShellRoot {
|
|
|
334
359
|
this.#extensionWorkingMessage = undefined;
|
|
335
360
|
this.#status.setWorkingOverride(undefined);
|
|
336
361
|
this.#footer.update(this.#viewWithExtensionStatuses(this.#view));
|
|
362
|
+
// An extension renderer may have drawn transcript blocks that are now unrendered by it.
|
|
363
|
+
this.#renderedRows.clear();
|
|
337
364
|
this.invalidate();
|
|
338
365
|
}
|
|
339
366
|
addExtensionNotification(message, type) {
|
|
@@ -384,11 +411,13 @@ export class OwnedUiSessionShellRoot {
|
|
|
384
411
|
this.#inputSurface.setFocused?.(focused);
|
|
385
412
|
}
|
|
386
413
|
dispose() {
|
|
414
|
+
this.#themeUnsubscribe();
|
|
387
415
|
this.header.dispose?.();
|
|
388
416
|
this.resources.dispose?.();
|
|
389
417
|
for (const component of this.#transcript.values())
|
|
390
418
|
component.dispose?.();
|
|
391
419
|
this.#transcript.clear();
|
|
420
|
+
this.#renderedRows.clear();
|
|
392
421
|
if (this.#inputSurface !== this.editor)
|
|
393
422
|
this.#inputSurface.dispose?.();
|
|
394
423
|
this.#extensionHeader?.dispose?.();
|
|
@@ -402,12 +431,16 @@ export class OwnedUiSessionShellRoot {
|
|
|
402
431
|
this.#queued.dispose?.();
|
|
403
432
|
}
|
|
404
433
|
#syncTranscript(blocks) {
|
|
434
|
+
this.#blocksById.clear();
|
|
435
|
+
for (const block of blocks)
|
|
436
|
+
this.#blocksById.set(block.id, block);
|
|
405
437
|
const nextIds = new Set(blocks.map(block => block.id));
|
|
406
438
|
for (const [id, component] of this.#transcript) {
|
|
407
439
|
if (id.startsWith("workflow-status-") || nextIds.has(id))
|
|
408
440
|
continue;
|
|
409
441
|
component.dispose?.();
|
|
410
442
|
this.#transcript.delete(id);
|
|
443
|
+
this.#renderedRows.delete(id);
|
|
411
444
|
}
|
|
412
445
|
for (const block of blocks) {
|
|
413
446
|
const component = this.#transcript.get(block.id);
|
|
@@ -431,8 +464,9 @@ export class OwnedUiSessionShellRoot {
|
|
|
431
464
|
if (block !== undefined)
|
|
432
465
|
order.push(block.id);
|
|
433
466
|
}
|
|
467
|
+
const placed = new Set(order);
|
|
434
468
|
for (const statusId of statusIds) {
|
|
435
|
-
if (!
|
|
469
|
+
if (!placed.has(statusId))
|
|
436
470
|
order.push(statusId);
|
|
437
471
|
}
|
|
438
472
|
this.#transcriptOrder = order;
|
|
@@ -498,6 +532,8 @@ export class OwnedUiSessionShellRoot {
|
|
|
498
532
|
this.resources.setExpanded(expanded);
|
|
499
533
|
for (const component of this.#transcript.values())
|
|
500
534
|
component.setExpanded(expanded);
|
|
535
|
+
// Expansion changes what a block draws without changing its revision.
|
|
536
|
+
this.#renderedRows.clear();
|
|
501
537
|
this.invalidate();
|
|
502
538
|
}
|
|
503
539
|
}
|
|
@@ -525,12 +561,14 @@ function shellResourceEntries(backend) {
|
|
|
525
561
|
sourcePath: resource.sourcePath,
|
|
526
562
|
diagnostic: resource.diagnostic,
|
|
527
563
|
}));
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
564
|
+
const extensions = backend.extensionResources().filter(extension => !extension.hidden);
|
|
565
|
+
const loadedExtensions = extensions.filter(extension => extension.diagnostic === null);
|
|
566
|
+
const extensionLabels = compactExtensionLabels(loadedExtensions);
|
|
567
|
+
for (const extension of extensions) {
|
|
568
|
+
const labelIndex = loadedExtensions.indexOf(extension);
|
|
531
569
|
resources.push({
|
|
532
570
|
section: "Extensions",
|
|
533
|
-
label: compactResourceLabel(extension.sourcePath ?? extension.resolvedPath ?? "extension"),
|
|
571
|
+
label: extensionLabels[labelIndex] ?? compactResourceLabel(extension.sourcePath ?? extension.resolvedPath ?? "extension"),
|
|
534
572
|
sourcePath: extension.sourcePath ?? extension.resolvedPath,
|
|
535
573
|
diagnostic: extension.diagnostic,
|
|
536
574
|
});
|
|
@@ -538,12 +576,129 @@ function shellResourceEntries(backend) {
|
|
|
538
576
|
return resources;
|
|
539
577
|
}
|
|
540
578
|
function compactResourceLabel(path) {
|
|
541
|
-
const segments = path
|
|
579
|
+
const segments = compactPathSegments(path);
|
|
542
580
|
const leaf = segments.at(-1) ?? path;
|
|
543
581
|
if ((leaf === "index.ts" || leaf === "index.js") && segments.length > 1)
|
|
544
582
|
return segments.at(-2) ?? leaf;
|
|
545
583
|
return leaf;
|
|
546
584
|
}
|
|
585
|
+
/**
|
|
586
|
+
* Pinned from InteractiveMode's compact extension-label helpers at Pi commit
|
|
587
|
+
* 914cf1472e715297caa30db4b9535d534a9eb718. The source metadata crosses an
|
|
588
|
+
* A1-owned boundary first; the owned shell never inspects Pi's private root.
|
|
589
|
+
*/
|
|
590
|
+
function compactExtensionLabels(extensions) {
|
|
591
|
+
const localExtensions = extensions
|
|
592
|
+
.filter(extension => !isPackageExtensionSource(extension.sourceInfo))
|
|
593
|
+
.map(extension => {
|
|
594
|
+
const path = extension.sourcePath ?? extension.resolvedPath ?? "extension";
|
|
595
|
+
const segments = compactPathSegments(path);
|
|
596
|
+
if (segments.length > 1 && (segments.at(-1) === "index.ts" || segments.at(-1) === "index.js"))
|
|
597
|
+
segments.pop();
|
|
598
|
+
return { extension, segments };
|
|
599
|
+
});
|
|
600
|
+
return extensions.map(extension => {
|
|
601
|
+
const resourcePath = extension.sourcePath ?? extension.resolvedPath ?? "extension";
|
|
602
|
+
if (isPackageExtensionSource(extension.sourceInfo)) {
|
|
603
|
+
return compactPackageExtensionLabel(resourcePath, extension.sourceInfo);
|
|
604
|
+
}
|
|
605
|
+
const localIndex = localExtensions.findIndex(item => item.extension === extension);
|
|
606
|
+
const segments = localExtensions[localIndex]?.segments;
|
|
607
|
+
if (!segments || segments.length === 0)
|
|
608
|
+
return compactResourceLabel(resourcePath);
|
|
609
|
+
for (let count = 1; count <= segments.length; count += 1) {
|
|
610
|
+
const candidate = segments.slice(-count).join("/");
|
|
611
|
+
if (localExtensions.every((item, itemIndex) => itemIndex === localIndex || item.segments.slice(-count).join("/") !== candidate)) {
|
|
612
|
+
return candidate;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return segments.join("/");
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
function compactPackageExtensionLabel(resourcePath, sourceInfo) {
|
|
619
|
+
const sourceLabel = compactPackageSourceLabel(sourceInfo.source);
|
|
620
|
+
if (!sourceLabel)
|
|
621
|
+
return compactResourceLabel(resourcePath);
|
|
622
|
+
const shortPath = shortPackagePath(resourcePath, sourceInfo).replaceAll("\\", "/");
|
|
623
|
+
const packagePath = shortPath.startsWith("extensions/") ? shortPath.slice("extensions/".length) : shortPath;
|
|
624
|
+
const slash = packagePath.lastIndexOf("/");
|
|
625
|
+
const fileName = slash < 0 ? packagePath : packagePath.slice(slash + 1);
|
|
626
|
+
const directory = slash < 0 ? "" : packagePath.slice(0, slash);
|
|
627
|
+
const extension = fileName.lastIndexOf(".");
|
|
628
|
+
const name = extension <= 0 ? fileName : fileName.slice(0, extension);
|
|
629
|
+
if (name === "index")
|
|
630
|
+
return !directory || directory === "." ? sourceLabel : `${sourceLabel}:${directory}`;
|
|
631
|
+
return `${sourceLabel}:${packagePath}`;
|
|
632
|
+
}
|
|
633
|
+
function compactPackageSourceLabel(source) {
|
|
634
|
+
if (source.startsWith("npm:"))
|
|
635
|
+
return source.slice("npm:".length) || source;
|
|
636
|
+
if (!source.startsWith("git:"))
|
|
637
|
+
return source;
|
|
638
|
+
const gitSource = source.slice("git:".length).trim();
|
|
639
|
+
let repositoryPath;
|
|
640
|
+
const scpLike = gitSource.match(/^git@[^:]+:(.+)$/);
|
|
641
|
+
if (scpLike?.[1]) {
|
|
642
|
+
repositoryPath = scpLike[1];
|
|
643
|
+
}
|
|
644
|
+
else if (/^[a-z]+:\/\//i.test(gitSource)) {
|
|
645
|
+
try {
|
|
646
|
+
repositoryPath = new URL(gitSource).pathname.replace(/^\/+/, "");
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
return source;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
const slash = gitSource.indexOf("/");
|
|
654
|
+
if (slash >= 0)
|
|
655
|
+
repositoryPath = gitSource.slice(slash + 1);
|
|
656
|
+
}
|
|
657
|
+
if (!repositoryPath)
|
|
658
|
+
return source;
|
|
659
|
+
const ref = repositoryPath.indexOf("@");
|
|
660
|
+
const withoutRef = ref < 0 ? repositoryPath : repositoryPath.slice(0, ref);
|
|
661
|
+
return withoutRef.replace(/\.git$/, "") || source;
|
|
662
|
+
}
|
|
663
|
+
function shortPackagePath(resourcePath, sourceInfo) {
|
|
664
|
+
const fullPath = normalizeResourcePath(resourcePath);
|
|
665
|
+
const baseDir = sourceInfo.baseDir === null ? undefined : normalizeResourcePath(sourceInfo.baseDir).replace(/\/$/, "");
|
|
666
|
+
if (baseDir) {
|
|
667
|
+
const npmRoot = baseDir.match(/^(.*\/node_modules)\/(@?[^/]+(?:\/[^/]+)?)$/);
|
|
668
|
+
if (npmRoot?.[1] && fullPath.startsWith(`${npmRoot[1]}/`))
|
|
669
|
+
return relativeResourcePath(baseDir, fullPath);
|
|
670
|
+
if (fullPath === baseDir)
|
|
671
|
+
return ".";
|
|
672
|
+
if (fullPath.startsWith(`${baseDir}/`))
|
|
673
|
+
return fullPath.slice(baseDir.length + 1);
|
|
674
|
+
}
|
|
675
|
+
const npmMatch = fullPath.match(/node_modules\/(@?[^/]+(?:\/[^/]+)?)\/(.*)/);
|
|
676
|
+
if (npmMatch?.[2] && sourceInfo.source.startsWith("npm:"))
|
|
677
|
+
return npmMatch[2];
|
|
678
|
+
const gitMatch = fullPath.match(/git\/[^/]+\/[^/]+\/(.*)/);
|
|
679
|
+
if (gitMatch?.[1] && sourceInfo.source.startsWith("git:"))
|
|
680
|
+
return gitMatch[1];
|
|
681
|
+
return resourcePath;
|
|
682
|
+
}
|
|
683
|
+
function relativeResourcePath(from, to) {
|
|
684
|
+
const fromSegments = from.split("/").filter(Boolean);
|
|
685
|
+
const toSegments = to.split("/").filter(Boolean);
|
|
686
|
+
let common = 0;
|
|
687
|
+
while (common < fromSegments.length && common < toSegments.length
|
|
688
|
+
&& fromSegments[common]?.toLowerCase() === toSegments[common]?.toLowerCase())
|
|
689
|
+
common += 1;
|
|
690
|
+
return [...fromSegments.slice(common).map(() => ".."), ...toSegments.slice(common)].join("/") || ".";
|
|
691
|
+
}
|
|
692
|
+
function compactPathSegments(path) {
|
|
693
|
+
return normalizeResourcePath(path).split("/").filter(segment => segment.length > 0 && segment !== "~");
|
|
694
|
+
}
|
|
695
|
+
function normalizeResourcePath(path) {
|
|
696
|
+
return path.replaceAll("\\", "/");
|
|
697
|
+
}
|
|
698
|
+
function isPackageExtensionSource(sourceInfo) {
|
|
699
|
+
const source = sourceInfo?.source ?? "";
|
|
700
|
+
return source.startsWith("npm:") || source.startsWith("git:");
|
|
701
|
+
}
|
|
547
702
|
export class OwnedUiSessionShell {
|
|
548
703
|
backend;
|
|
549
704
|
root;
|
|
@@ -560,6 +715,7 @@ export class OwnedUiSessionShell {
|
|
|
560
715
|
#sequence = 0;
|
|
561
716
|
#started = false;
|
|
562
717
|
#disposed = false;
|
|
718
|
+
#pointerReporting = false;
|
|
563
719
|
#compactionQueue = [];
|
|
564
720
|
#lastClearTime = 0;
|
|
565
721
|
#activeLoginDialog;
|
|
@@ -638,8 +794,10 @@ export class OwnedUiSessionShell {
|
|
|
638
794
|
});
|
|
639
795
|
this.root.editor.setAutocompleteCommands(this.backend.workflowAutocompleteCommands());
|
|
640
796
|
this.#unsubscribe = this.backend.onEvent(event => {
|
|
641
|
-
|
|
642
|
-
|
|
797
|
+
// One view read per event: the model is built by the backend, and building it
|
|
798
|
+
// twice per streamed chunk is what made a long session cost more per chunk.
|
|
799
|
+
const view = this.#syncView();
|
|
800
|
+
if (view.lifecycle === "ready" && this.#compactionQueue.length > 0)
|
|
643
801
|
void this.#flushCompactionQueue();
|
|
644
802
|
if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
|
|
645
803
|
this.#resolveStopped?.();
|
|
@@ -1232,10 +1390,25 @@ export class OwnedUiSessionShell {
|
|
|
1232
1390
|
this.root.appendDaxnuts();
|
|
1233
1391
|
}
|
|
1234
1392
|
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Turns terminal pointer reporting on for a screen that reads the pointer, and off for
|
|
1395
|
+
* every path that ends it. While it is on the terminal hands A1 the wheel and the
|
|
1396
|
+
* button instead of scrolling and selecting itself, so leaving it on outlives the
|
|
1397
|
+
* screen that wanted it and takes the terminal's own scrolling and selection with it.
|
|
1398
|
+
*/
|
|
1399
|
+
#setPointerReporting(enabled) {
|
|
1400
|
+
if (this.#pointerReporting === enabled)
|
|
1401
|
+
return;
|
|
1402
|
+
this.#pointerReporting = enabled;
|
|
1403
|
+
if (!this.runtime.active)
|
|
1404
|
+
return;
|
|
1405
|
+
this.runtime.writeControl(enabled ? MOUSE_TRACKING_ON : MOUSE_TRACKING_OFF);
|
|
1406
|
+
}
|
|
1235
1407
|
async dispose() {
|
|
1236
1408
|
if (this.#disposed)
|
|
1237
1409
|
return;
|
|
1238
1410
|
this.#disposed = true;
|
|
1411
|
+
this.#setPointerReporting(false);
|
|
1239
1412
|
this.#unsubscribe();
|
|
1240
1413
|
this.#dialogHandle?.hide();
|
|
1241
1414
|
await this.backend.unbindExtensionUi();
|
|
@@ -1248,6 +1421,8 @@ export class OwnedUiSessionShell {
|
|
|
1248
1421
|
this.#sessionGeneration = this.backend.sessionGeneration;
|
|
1249
1422
|
this.#activeLoginDialog = undefined;
|
|
1250
1423
|
this.#extensionBridge.reset();
|
|
1424
|
+
// A replaced session takes its screens with it, pointer reporting included.
|
|
1425
|
+
this.#setPointerReporting(false);
|
|
1251
1426
|
this.root.setInputSurface(null);
|
|
1252
1427
|
this.root.resetExtensionUi();
|
|
1253
1428
|
this.root.resetWorkflowPresentation();
|
|
@@ -1257,6 +1432,7 @@ export class OwnedUiSessionShell {
|
|
|
1257
1432
|
this.runtime.requestRender();
|
|
1258
1433
|
for (const listener of this.#listeners)
|
|
1259
1434
|
listener(view);
|
|
1435
|
+
return view;
|
|
1260
1436
|
}
|
|
1261
1437
|
#openOwnedRoute(route) {
|
|
1262
1438
|
const surface = this.#routeHost?.open(route) ?? null;
|
|
@@ -1267,7 +1443,7 @@ export class OwnedUiSessionShell {
|
|
|
1267
1443
|
this.#dialogHandle?.hide();
|
|
1268
1444
|
// Any-event reporting: hover and drag are what the screen is driven by, and
|
|
1269
1445
|
// it also stops the terminal treating a drag as a text selection.
|
|
1270
|
-
this
|
|
1446
|
+
this.#setPointerReporting(true);
|
|
1271
1447
|
// The interrupt chord is global, so it is watched on raw input rather than
|
|
1272
1448
|
// through the overlay: the pinned shell handles that key before an overlay
|
|
1273
1449
|
// ever sees it, which is why an owned screen must not rely on being asked.
|
|
@@ -1290,7 +1466,7 @@ export class OwnedUiSessionShell {
|
|
|
1290
1466
|
});
|
|
1291
1467
|
const closeSurface = () => {
|
|
1292
1468
|
removeInterruptWatch();
|
|
1293
|
-
this
|
|
1469
|
+
this.#setPointerReporting(false);
|
|
1294
1470
|
this.#dialogHandle?.hide();
|
|
1295
1471
|
this.#dialogHandle = undefined;
|
|
1296
1472
|
this.#dialogId = undefined;
|
package/package.json
CHANGED