@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.
@@ -5,7 +5,7 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "unsupported",
8
- "builtAt": "2026-08-25T06:52:40.013Z",
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-25T06:52:58.359Z",
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-25T06:54:04.992Z",
8
+ "builtAt": "2026-08-25T07:48:23.701Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian.exe",
11
- "sha256": "b8b0c81f275bd95b0cf4a63dd19f734a845947e47feded1d85a7a3562cf072db",
11
+ "sha256": "8a76f3b0814f4e98d3b71b2285be8b18f0ad95441cc35d14f023cd184bd293ac",
12
12
  "size": 172544
13
13
  },
14
14
  "provenance": {
@@ -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.#transcript.map(block => ({ ...block })),
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.#lifecycle = "busy";
1412
- this.#status = { ...this.#status, workingMessage: "Working..." };
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.#transcript = this.#transcript.map(block => block.status === "live" ? { ...block, status: "finalized" } : block);
1457
- this.#lifecycle = "ready";
1458
- this.#status = { ...this.#status, workingMessage: null };
1459
- this.#emitEvent({ type: "session-lifecycle", lifecycle: "ready", reason: null });
1460
- this.#emitEvent({ type: "status", status: this.#status });
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.#lifecycle = "busy";
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
- case "compaction_end":
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.#lifecycle = "busy";
1488
- this.#status = { ...this.#status, workingMessage: "Compacting…" };
1489
- this.#emitEvent({ type: "status", status: this.#status });
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.#transcript = blocks;
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.#transcript.find(block => block.id === blockId);
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.#transcript.findIndex(existing => existing.id === block.id);
1756
- if (index >= 0)
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.#transcript.some(block => block.id === candidate && block.status === "live"));
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.#transcript.find(block => block.id === id);
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.#view.transcript.find(item => item.id === id);
137
+ const block = this.#blocksById.get(id);
132
138
  if (!this.#thinkingVisible && block?.kind === "thinking")
133
139
  return [];
134
- const rows = this.#transcript.get(id)?.render(width) ?? [];
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 (!order.includes(statusId))
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
- for (const extension of backend.extensionResources()) {
529
- if (extension.hidden)
530
- continue;
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.replaceAll("\\", "/").split("/").filter(Boolean);
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
- this.#syncView();
642
- if (this.view().lifecycle === "ready" && this.#compactionQueue.length > 0)
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.runtime.writeControl(MOUSE_TRACKING_ON);
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.runtime.writeControl(MOUSE_TRACKING_OFF);
1469
+ this.#setPointerReporting(false);
1294
1470
  this.#dialogHandle?.hide();
1295
1471
  this.#dialogHandle = undefined;
1296
1472
  this.#dialogId = undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timurproko/a1",
3
- "version": "0.1.8-dev.ade9b77",
3
+ "version": "0.1.8-dev.dbc8a9b",
4
4
  "description": "Standalone terminal workspace for supervised native and managed agents",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.13.0",