@timurproko/a1 0.1.8-dev.536 → 0.1.8-dev.546

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.
Files changed (82) hide show
  1. package/bin/cli.js +4 -0
  2. package/bin/update-recovery.js +25 -3
  3. package/dist/app/session-shell/session-shell-root.js +15 -5
  4. package/dist/cli/dispatch.d.ts +5 -0
  5. package/dist/cli/dispatch.js +26 -0
  6. package/dist/cli/session-context.d.ts +9 -0
  7. package/dist/cli/session-context.js +29 -0
  8. package/dist/composition/owned-ui.js +6 -2
  9. package/dist/contracts/owned-ui/model.d.ts +5 -0
  10. package/dist/contracts/owned-ui/validation.js +6 -0
  11. package/dist/contracts/presentation/index.d.ts +13 -0
  12. package/dist/contracts/presentation/index.js +30 -0
  13. package/dist/features/launch/index.d.ts +2 -0
  14. package/dist/features/launch/index.js +1 -0
  15. package/dist/features/launch/session-context.d.ts +8 -0
  16. package/dist/features/launch/session-context.js +8 -0
  17. package/dist/features/owned-ui/project-trust-prompt.js +1 -1
  18. package/dist/features/owned-ui/reference-screen-app.js +7 -8
  19. package/dist/features/owned-ui/settings-app.js +9 -5
  20. package/dist/foundation/lifecycle/index.d.ts +2 -0
  21. package/dist/foundation/lifecycle/index.js +1 -0
  22. package/dist/foundation/lifecycle/session-repository-context.d.ts +18 -0
  23. package/dist/foundation/lifecycle/session-repository-context.js +173 -0
  24. package/dist/foundation/release/index.d.ts +2 -2
  25. package/dist/foundation/release/index.js +1 -1
  26. package/dist/foundation/release/update-recovery.d.ts +7 -1
  27. package/dist/foundation/release/update-recovery.js +39 -9
  28. package/dist/foundation/release/update.d.ts +7 -0
  29. package/dist/foundation/release/update.js +115 -24
  30. package/dist/integrations/pi/components/models-dialog.js +9 -9
  31. package/dist/integrations/pi/components/shell-editor-autocomplete.js +27 -3
  32. package/dist/integrations/pi/components/shell-extension-ui.js +4 -2
  33. package/dist/integrations/pi/components/shell-footer-status.js +1 -1
  34. package/dist/integrations/pi/components/shell-presenters-transcript.js +4 -2
  35. package/dist/integrations/pi/components/skills-command.d.ts +2 -0
  36. package/dist/integrations/pi/components/skills-command.js +11 -10
  37. package/dist/integrations/pi/components/skills-dialog.js +8 -3
  38. package/dist/integrations/pi/components/theme.d.ts +4 -0
  39. package/dist/integrations/pi/components/theme.js +10 -0
  40. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +4 -4
  41. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +2 -2
  42. package/dist/integrations/pi/components/upstream/components/extension-editor.js +9 -10
  43. package/dist/integrations/pi/components/upstream/components/extension-input.d.ts +17 -0
  44. package/dist/integrations/pi/components/upstream/components/extension-input.js +72 -0
  45. package/dist/integrations/pi/components/upstream/components/extension-selector.d.ts +15 -0
  46. package/dist/integrations/pi/components/upstream/components/extension-selector.js +92 -0
  47. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +3 -2
  48. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +16 -15
  49. package/dist/integrations/pi/components/upstream/components/session-footer.js +25 -9
  50. package/dist/integrations/pi/components/upstream/components/session-selector.js +25 -16
  51. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +6 -4
  52. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +22 -12
  53. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +3 -2
  54. package/dist/integrations/pi/components/upstream/components/tree-selector.js +14 -12
  55. package/dist/integrations/pi/components/upstream/components/trust-selector.js +11 -5
  56. package/dist/integrations/pi/engine/adapter.d.ts +2 -1
  57. package/dist/integrations/pi/engine/adapter.js +2 -0
  58. package/dist/integrations/pi/engine/message-values.js +1 -0
  59. package/dist/integrations/pi/engine/session-runtime.d.ts +11 -2
  60. package/dist/integrations/pi/engine/session-runtime.js +136 -33
  61. package/dist/integrations/pi/engine/transcript-projection.js +35 -0
  62. package/dist/integrations/pi/startup-public.manifest.json +2 -0
  63. package/dist/native/darwin-arm64/manifest.json +1 -1
  64. package/dist/native/linux-x64/manifest.json +1 -1
  65. package/dist/native/win32-x64/manifest.json +2 -2
  66. package/dist/native/win32-x64/process-guardian.exe +0 -0
  67. package/dist/product-identity.d.ts +1 -1
  68. package/dist/product-identity.js +1 -1
  69. package/dist/product-identity.json +2 -1
  70. package/dist/ui/components/dialog-panel.d.ts +2 -1
  71. package/dist/ui/components/dialog-panel.js +6 -1
  72. package/dist/ui/components/index.d.ts +2 -0
  73. package/dist/ui/components/index.js +1 -0
  74. package/dist/ui/components/shortcut-hints.d.ts +7 -0
  75. package/dist/ui/components/shortcut-hints.js +12 -0
  76. package/dist/ui/components/shortcuts.d.ts +2 -0
  77. package/dist/ui/components/shortcuts.js +5 -2
  78. package/dist/ui/components/transcript-viewport.d.ts +1 -1
  79. package/docs/ci-release-runbook.md +9 -7
  80. package/docs/openspec-archive-automation.md +6 -0
  81. package/docs/validation.md +5 -2
  82. package/package.json +1 -1
@@ -4,14 +4,15 @@
4
4
  * Modifications: Preserve the searchable thinking-level selector, current/default semantics,
5
5
  * selection, save, cancellation, and focus while accepting the active bare-A1 cycle-key label from the
6
6
  * shell, styling the title with the established bold semantic accent treatment, placing its muted hint
7
- * directly below it, deduplicating levels, and rendering aligned muted descriptions after an adjacent
8
- * success-colored active marker. All list and border colors use the owned theme and its explicit color
9
- * mode. The comparison profile retains the public pinned component.
10
- * Deviations: owned-level-cycle-shortcut, owned-thinking-selector-heading.
7
+ * directly below it, deduplicating levels, and rendering aligned muted descriptions after adjacent
8
+ * active and bracketed default markers. All list and border colors use the owned theme and its
9
+ * explicit color mode, and the footer uses the shared bare-A1 modal shortcut row. The comparison
10
+ * profile retains the public pinned component.
11
+ * Deviations: owned-modal-shortcut-hints, owned-level-cycle-shortcut, owned-thinking-selector-heading.
11
12
  */
12
13
  import { Container, fuzzyFilter, getKeybindings, Input, SelectList, Spacer, Text, } from "@earendil-works/pi-tui";
13
14
  import { DynamicBorder } from "../../../startup-public.js";
14
- import { piTheme } from "../theme/theme.js";
15
+ import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
15
16
  const THINKING_SELECT_LIST_LAYOUT = {
16
17
  minPrimaryColumnWidth: 12,
17
18
  maxPrimaryColumnWidth: 32,
@@ -35,6 +36,7 @@ export class OwnedThinkingSelectorComponent extends Container {
35
36
  onCancel;
36
37
  onSelectAsDefault;
37
38
  currentLevel;
39
+ defaultThinkingLevel;
38
40
  _focused = false;
39
41
  get focused() {
40
42
  return this._focused;
@@ -49,10 +51,11 @@ export class OwnedThinkingSelectorComponent extends Container {
49
51
  this.onCancel = onCancel;
50
52
  this.onSelectAsDefault = onSelectAsDefault;
51
53
  this.currentLevel = currentLevel;
54
+ this.defaultThinkingLevel = defaultThinkingLevel;
52
55
  this.allItems = [...new Set(availableLevels)].map((level) => ({
53
56
  value: level,
54
57
  label: level,
55
- description: level === defaultThinkingLevel ? `${LEVEL_DESCRIPTIONS[level]} · default` : LEVEL_DESCRIPTIONS[level],
58
+ description: LEVEL_DESCRIPTIONS[level],
56
59
  }));
57
60
  this.addChild(new DynamicBorder((text) => piTheme().fg("border", text)));
58
61
  this.addChild(new Spacer(1));
@@ -67,7 +70,11 @@ export class OwnedThinkingSelectorComponent extends Container {
67
70
  this.selectListChildIndex = this.children.length;
68
71
  this.addChild(this.selectList);
69
72
  this.addChild(new Spacer(1));
70
- this.addChild(new Text(piTheme().fg("dim", ` ${this.keyDisplayText("tui.select.confirm")} to select · ${this.keyDisplayText("app.thinking.save")} to set as default · ${this.keyDisplayText("tui.select.cancel")} to cancel`), 0, 0));
73
+ this.addChild(new Text(renderPiModalShortcutHints([
74
+ { key: this.keyDisplayText("tui.select.confirm"), action: "to select" },
75
+ { key: this.keyDisplayText("app.thinking.save"), action: "to set as default" },
76
+ { key: this.keyDisplayText("tui.select.cancel"), action: "to cancel" },
77
+ ], 2), 0, 0));
71
78
  this.addChild(new DynamicBorder((text) => piTheme().fg("border", text)));
72
79
  }
73
80
  keyDisplayText(keybinding) {
@@ -79,17 +86,20 @@ export class OwnedThinkingSelectorComponent extends Container {
79
86
  .join("/");
80
87
  }
81
88
  buildSelectList(items, preselect) {
89
+ const markerWidth = (item) => (item.value === this.currentLevel ? 2 : 0) + (item.value === this.defaultThinkingLevel ? 10 : 0);
82
90
  const primaryWidth = this.allItems.reduce((widest, item) => {
83
91
  const level = item.label ?? item.value;
84
- return Math.max(widest, level.length + (item.value === this.currentLevel ? 2 : 0));
92
+ return Math.max(widest, level.length + markerWidth(item));
85
93
  }, 0);
86
94
  const themedItems = items.map((item) => {
87
95
  const level = item.label ?? item.value;
88
- const current = item.value === this.currentLevel;
89
- const currentMarker = current ? ` ${piTheme().fg("success", "✓")}` : "";
90
- const separator = " ".repeat(Math.max(1, primaryWidth - level.length - (current ? 2 : 0) + 1));
96
+ const currentMarker = item.value === this.currentLevel ? ` ${piTheme().fg("success", "✓")}` : "";
97
+ const defaultMarker = item.value === this.defaultThinkingLevel
98
+ ? ` ${piTheme().fg("muted", "[default]")}`
99
+ : "";
100
+ const separator = " ".repeat(Math.max(1, primaryWidth - level.length - markerWidth(item) + 1));
91
101
  const description = item.description ? piTheme().fg("muted", item.description) : "";
92
- return { value: item.value, label: `${level}${currentMarker}${separator}${description}` };
102
+ return { value: item.value, label: `${level}${currentMarker}${defaultMarker}${separator}${description}` };
93
103
  });
94
104
  // Invariant: the bare selector uses the owned theme's explicit color mode, not upstream's host detection.
95
105
  const list = new SelectList(themedItems, Math.max(1, themedItems.length), {
@@ -3,8 +3,9 @@
3
3
  * packages/coding-agent/src/modes/interactive/components/tree-selector.ts.
4
4
  * Modifications: Source-synchronized tree selector port: preserve filtering, folding, labels, copying,
5
5
  * tree navigation, key hints, focus, and viewport behavior while remapping public types/components
6
- * plus owned keybindings/theme helpers required to avoid the pinned package nested pi-tui singleton.
7
- * Deviations: none.
6
+ * plus owned keybindings/theme helpers required to avoid the pinned package nested pi-tui singleton;
7
+ * bare A1 uses the shared semantic modal shortcut row.
8
+ * Deviations: owned-modal-shortcut-hints.
8
9
  */
9
10
  import { type Component, Container, type Focusable } from "@earendil-works/pi-tui";
10
11
  import { type SessionTreeNode } from "@earendil-works/pi-coding-agent";
@@ -3,13 +3,14 @@
3
3
  * packages/coding-agent/src/modes/interactive/components/tree-selector.ts.
4
4
  * Modifications: Source-synchronized tree selector port: preserve filtering, folding, labels, copying,
5
5
  * tree navigation, key hints, focus, and viewport behavior while remapping public types/components
6
- * plus owned keybindings/theme helpers required to avoid the pinned package nested pi-tui singleton.
7
- * Deviations: none.
6
+ * plus owned keybindings/theme helpers required to avoid the pinned package nested pi-tui singleton;
7
+ * bare A1 uses the shared semantic modal shortcut row.
8
+ * Deviations: owned-modal-shortcut-hints.
8
9
  */
9
10
  import { Container, Input, Spacer, sliceByColumn, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
10
11
  import { DynamicBorder } from "../../../startup-public.js";
11
12
  import { KeybindingsManager } from "../adjacent/core/keybindings.js";
12
- import { piTheme } from "../theme/theme.js";
13
+ import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
13
14
  const theme = new Proxy({}, {
14
15
  get(_target, property) {
15
16
  const active = piTheme();
@@ -24,8 +25,8 @@ function formatKeyText(key) {
24
25
  .map((value) => value.split("+").map((part) => process.platform === "darwin" && part.toLowerCase() === "alt" ? "option" : part).join("+"))
25
26
  .join("/");
26
27
  }
27
- function keyHint(keybinding, description) {
28
- return theme.fg("dim", formatKeyText(treeKeybindings.getKeys(keybinding).join("/"))) + theme.fg("muted", ` ${description}`);
28
+ function shortcutHint(keybinding, action) {
29
+ return { key: formatKeyText(treeKeybindings.getKeys(keybinding).join("/")), action };
29
30
  }
30
31
  const TREE_GUTTER_WIDTH = 2;
31
32
  const MIN_VISIBLE_ANCHOR_CONTENT_WIDTH = 4;
@@ -1075,14 +1076,12 @@ class TreeHelp {
1075
1076
  invalidate() { }
1076
1077
  render(width) {
1077
1078
  const items = TREE_HELP_ITEMS.map(({ keys, label, labelFirst }) => {
1078
- const text = formatHelpKeys(keys);
1079
- if (!text)
1080
- return label;
1081
- return labelFirst ? `${label} ${text}` : `${text} ${label}`;
1079
+ const key = formatHelpKeys(keys);
1080
+ return renderPiModalShortcutHints([key ? { key, action: label, ...(labelFirst === undefined ? {} : { actionFirst: labelFirst }) } : { action: label }]);
1082
1081
  });
1083
1082
  const availableWidth = Math.max(1, width);
1084
1083
  const indent = " ";
1085
- const separator = " · ";
1084
+ const separator = " ";
1086
1085
  const lines = [];
1087
1086
  let currentLine = "";
1088
1087
  for (const item of items) {
@@ -1101,7 +1100,7 @@ class TreeHelp {
1101
1100
  if (currentLine) {
1102
1101
  lines.push(...wrapTextWithAnsi(currentLine.trimEnd(), availableWidth));
1103
1102
  }
1104
- return lines.map((line) => theme.fg("muted", line));
1103
+ return lines;
1105
1104
  }
1106
1105
  }
1107
1106
  const TREE_HELP_ITEMS = [
@@ -1184,7 +1183,10 @@ class LabelInput {
1184
1183
  const availableWidth = width - indent.length;
1185
1184
  lines.push(truncateToWidth(`${indent}${theme.fg("muted", "Label (empty to remove):")}`, width));
1186
1185
  lines.push(...this.input.render(availableWidth).map((line) => truncateToWidth(`${indent}${line}`, width)));
1187
- lines.push(truncateToWidth(`${indent}${keyHint("tui.select.confirm", "save")} ${keyHint("tui.select.cancel", "cancel")}`, width));
1186
+ lines.push(truncateToWidth(renderPiModalShortcutHints([
1187
+ shortcutHint("tui.select.confirm", "save"),
1188
+ shortcutHint("tui.select.cancel", "cancel"),
1189
+ ], indent.length), width));
1188
1190
  return lines;
1189
1191
  }
1190
1192
  handleInput(keyData) {
@@ -2,12 +2,13 @@
2
2
  * Provenance: @earendil-works/pi-coding-agent 0.87.0 (MIT), commit 16787ad5b2dc748047f314ca1bfe7708f30f54f3,
3
3
  * packages/coding-agent/src/modes/interactive/components/trust-selector.ts.
4
4
  * Modifications: Mechanical source-synchronized trust selector port with injected public
5
- * ProjectTrustStore-derived options and remapped owned theme imports.
6
- * Deviations: none.
5
+ * ProjectTrustStore-derived options, remapped owned theme imports, and the shared bare-A1 modal
6
+ * shortcut row.
7
+ * Deviations: owned-modal-shortcut-hints.
7
8
  */
8
- import { DynamicBorder, keyHint, rawKeyHint } from "../../../startup-public.js";
9
+ import { DynamicBorder } from "../../../startup-public.js";
9
10
  import { Container, getKeybindings, Spacer, Text } from "@earendil-works/pi-tui";
10
- import { piTheme } from "../theme/theme.js";
11
+ import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
11
12
  function formatDecision(trustPath, decision) {
12
13
  if (decision === null)
13
14
  return "none";
@@ -39,7 +40,12 @@ export class TrustSelectorComponent extends Container {
39
40
  this.listContainer = new Container();
40
41
  this.addChild(this.listContainer);
41
42
  this.addChild(new Spacer(1));
42
- this.addChild(new Text(`${rawKeyHint("↑↓", "navigate")} ${keyHint("tui.select.confirm", "save")} ${keyHint("tui.select.cancel", "cancel")}`, 1, 0));
43
+ const keybindings = getKeybindings();
44
+ this.addChild(new Text(renderPiModalShortcutHints([
45
+ { key: "↑↓", action: "navigate" },
46
+ { key: keybindings.getKeys("tui.select.confirm").join("/"), action: "save" },
47
+ { key: keybindings.getKeys("tui.select.cancel").join("/"), action: "cancel" },
48
+ ]), 1, 0));
43
49
  this.addChild(new Spacer(1));
44
50
  this.addChild(new DynamicBorder());
45
51
  this.updateList(options.savedDecision);
@@ -9,7 +9,7 @@ import type { AgentSettingOwner } from "../../../contracts/agent-engine/index.js
9
9
  import { type AdapterCommandResult } from "./command-dispatch.js";
10
10
  import { type OwnedPiVisualExtensionSupport } from "./extension-ui-binding.js";
11
11
  import { type OwnedPiExtensionResourceSummary, type OwnedPiResourceSummary } from "./resource-catalog.js";
12
- import { type PiEnginePackageUpdateProbe, type PiEngineRuntimeFactory } from "./session-runtime.js";
12
+ import { type PiEnginePackageUpdateProbe, type PiEngineRuntimeFactory, type PiRepositoryContextReader } from "./session-runtime.js";
13
13
  import { PiWorkflowContexts } from "./workflow-contexts.js";
14
14
  export type { OwnedPiExtensionResourceSummary, OwnedPiExtensionSourceSummary, OwnedPiResourceSummary } from "./resource-catalog.js";
15
15
  export type { PiEngineRuntimeFactory, PiEngineRuntimeFactoryInput } from "./session-runtime.js";
@@ -41,6 +41,7 @@ export interface PiEngineAdapterOptions {
41
41
  * mode. Returns display names of packages with updates available.
42
42
  */
43
43
  readonly checkPackageUpdates?: PiEnginePackageUpdateProbe;
44
+ readonly repositoryContextReader?: PiRepositoryContextReader;
44
45
  }
45
46
  /** Owns the pinned Pi session lifecycle and translates its events into the neutral agent-engine contract. */
46
47
  export declare class PiEngineAdapter implements OwnedUiPromptSuggestionGeneratorPort {
@@ -133,6 +133,7 @@ export class PiEngineAdapter {
133
133
  projectTrustPrompt: options.projectTrustPrompt,
134
134
  createRuntime: options.createRuntime,
135
135
  checkPackageUpdates: options.checkPackageUpdates,
136
+ ...(options.repositoryContextReader === undefined ? {} : { repositoryContextReader: options.repositoryContextReader }),
136
137
  host: this.#workflowHost,
137
138
  }, {
138
139
  disposed: () => this.#disposed,
@@ -428,6 +429,7 @@ export class PiEngineAdapter {
428
429
  usage: this.#usageCache ??= readUsageView(this.#engine.session, this.#engine.runtime, this.#activeModel),
429
430
  footer: {
430
431
  branch: this.#engine.gitBranch,
432
+ repositoryPath: this.#engine.repositoryCwd,
431
433
  pullRequest: this.#engine.pullRequest,
432
434
  sessionName: this.#engine.session?.sessionManager?.getSessionName() ?? null,
433
435
  availableProviderCount: new Set(this.#engine.runtime?.services.modelRuntime.getAvailableSnapshot?.().map(model => model.provider).filter(provider => provider !== undefined) ?? []).size,
@@ -29,6 +29,7 @@ export function sameBlockContent(left, right) {
29
29
  && left.status === right.status
30
30
  && left.title === right.title
31
31
  && left.text === right.text
32
+ && sameValue(left.userPresentation, right.userPresentation)
32
33
  && sameValue(left.toolState, right.toolState)
33
34
  && sameValue(left.toolRendering, right.toolRendering)
34
35
  && sameValue(left.payload, right.payload)
@@ -16,6 +16,12 @@ export interface PiEngineRuntimeFactoryInput {
16
16
  }
17
17
  export type PiEngineRuntimeFactory = (input: PiEngineRuntimeFactoryInput) => Promise<AgentSessionRuntime>;
18
18
  export type PiEnginePackageUpdateProbe = (settingsManager: AgentSessionServices["settingsManager"]) => Promise<readonly string[]>;
19
+ export interface PiRepositoryContextSelection {
20
+ readonly cwd: string;
21
+ readonly branch: string;
22
+ }
23
+ export type PiRepositoryContextReader = (sessionId: string, sessionFile: string, signal: AbortSignal) => Promise<PiRepositoryContextSelection | null>;
24
+ export type PiGitBranchReader = (cwd: string, signal: AbortSignal) => Promise<string | null>;
19
25
  export interface PiEngineRuntimeOptions {
20
26
  readonly cwd: string;
21
27
  readonly agentDir: string;
@@ -28,12 +34,14 @@ export interface PiEngineRuntimeOptions {
28
34
  readonly checkPackageUpdates: PiEnginePackageUpdateProbe | undefined;
29
35
  readonly pullRequestProbe?: PiPullRequestProbe;
30
36
  readonly pullRequestRefreshMs?: number;
31
- readonly gitBranchReader?: (cwd: string) => Promise<string | null>;
37
+ readonly repositoryContextPollMs?: number;
38
+ readonly repositoryContextReader?: PiRepositoryContextReader;
39
+ readonly gitBranchReader?: PiGitBranchReader;
32
40
  readonly host: PiWorkflowHost;
33
41
  }
34
42
  export interface PiEngineRuntimePorts {
35
43
  disposed(): boolean;
36
- /** The runtime exists and its cwd and git branch are known; nothing is bound yet. */
44
+ /** The runtime exists and its startup cwd is known; repository metadata resolves after binding. */
37
45
  started(runtime: AgentSessionRuntime): void;
38
46
  /** True while a runtime-initiated rebind must be ignored: delivery overload, stopped admission, or disposal. */
39
47
  rebindBlocked(): boolean;
@@ -65,6 +73,7 @@ export declare class PiEngineRuntime {
65
73
  /** Actual session replacements, excluding delivery-only invalidation of callbacks. */
66
74
  get bindingGeneration(): number;
67
75
  get cwd(): string;
76
+ get repositoryCwd(): string;
68
77
  get gitBranch(): string | null;
69
78
  get pullRequest(): PiPullRequestIdentity | null;
70
79
  get sessionCommands(): PiSessionCommandIntegration | undefined;
@@ -9,6 +9,9 @@ import { DefaultPackageManager, VERSION } from "../startup-public.js";
9
9
  import { readOpenPullRequest, } from "./repository-pr.js";
10
10
  const execFileAsync = promisify(execFile);
11
11
  const PULL_REQUEST_REFRESH_MS = 60_000;
12
+ const REPOSITORY_CONTEXT_POLL_MS = 5_000;
13
+ const GIT_BRANCH_TIMEOUT_MS = 5_000;
14
+ const GIT_BRANCH_MAX_BUFFER_BYTES = 4 * 1024;
12
15
  /**
13
16
  * The pinned Pi runtime and the session bound to it: creation through the runtime factory, the
14
17
  * session generation that stamps and invalidates events, the subscription that forwards session
@@ -22,6 +25,8 @@ export class PiEngineRuntime {
22
25
  #checkPackageUpdates;
23
26
  #pullRequestProbe;
24
27
  #pullRequestRefreshMs;
28
+ #repositoryContextPollMs;
29
+ #repositoryContextReader;
25
30
  #gitBranchReader;
26
31
  #cwd;
27
32
  #runtime;
@@ -31,15 +36,21 @@ export class PiEngineRuntime {
31
36
  #sessionBindingGeneration = 0;
32
37
  #sessionCommands;
33
38
  #compactionProgress = null;
39
+ #repositoryCwd;
34
40
  #gitBranch = null;
35
41
  #pullRequest = null;
36
- #pullRequestTimer;
37
- #pullRequestAbort;
42
+ #repositoryRefreshTimer;
43
+ #repositoryRefreshAbort;
44
+ #repositoryRefreshTask;
45
+ #repositoryRefreshPending = false;
46
+ #lastPullRequestProbeAt = null;
47
+ #repositoryRefreshEnabled = false;
38
48
  #disposed = false;
39
49
  constructor(options, ports) {
40
50
  this.#options = options;
41
51
  this.#ports = ports;
42
52
  this.#cwd = options.cwd;
53
+ this.#repositoryCwd = options.cwd;
43
54
  this.#runtimeFactory = options.createRuntime ?? createDefaultPiRuntime;
44
55
  this.#checkPackageUpdates = options.checkPackageUpdates
45
56
  ?? (options.createRuntime
@@ -47,6 +58,8 @@ export class PiEngineRuntime {
47
58
  : settingsManager => checkDefaultPiPackageUpdates(this.#cwd, options.agentDir, settingsManager));
48
59
  this.#pullRequestProbe = options.pullRequestProbe ?? readOpenPullRequest;
49
60
  this.#pullRequestRefreshMs = options.pullRequestRefreshMs ?? PULL_REQUEST_REFRESH_MS;
61
+ this.#repositoryContextPollMs = options.repositoryContextPollMs ?? REPOSITORY_CONTEXT_POLL_MS;
62
+ this.#repositoryContextReader = options.repositoryContextReader ?? (async () => null);
50
63
  this.#gitBranchReader = options.gitBranchReader ?? readGitBranch;
51
64
  }
52
65
  get runtime() {
@@ -66,6 +79,9 @@ export class PiEngineRuntime {
66
79
  get cwd() {
67
80
  return this.#runtime?.cwd ?? this.#cwd;
68
81
  }
82
+ get repositoryCwd() {
83
+ return this.#repositoryCwd;
84
+ }
69
85
  get gitBranch() {
70
86
  return this.#gitBranch;
71
87
  }
@@ -91,7 +107,7 @@ export class PiEngineRuntime {
91
107
  });
92
108
  this.#runtime = runtime;
93
109
  this.#cwd = runtime.cwd ?? this.#cwd;
94
- this.#gitBranch = await this.#gitBranchReader(this.#cwd);
110
+ this.#repositoryCwd = this.#cwd;
95
111
  this.#ports.started(runtime);
96
112
  runtime.setRebindSession(async (session) => {
97
113
  if (this.#ports.rebindBlocked())
@@ -104,7 +120,8 @@ export class PiEngineRuntime {
104
120
  }
105
121
  this.bindSession(runtime.session);
106
122
  await this.#announceChangelog(runtime.services.settingsManager);
107
- this.#startPullRequestRefresh();
123
+ this.#repositoryRefreshEnabled = true;
124
+ this.#startRepositoryRefresh();
108
125
  return runtime;
109
126
  }
110
127
  /** Probe extension packages for updates after startup and report them as an informational diagnostic. */
@@ -161,7 +178,10 @@ export class PiEngineRuntime {
161
178
  // Invariant: the subscription is live before the adapter rebuilds its state so the extension
162
179
  // rebind that finishes the rebuild cannot emit an event nobody is listening to.
163
180
  this.#subscribe();
181
+ this.#resetRepositoryRefresh();
164
182
  this.#ports.sessionReplaced(session);
183
+ if (this.#repositoryRefreshEnabled)
184
+ this.#startRepositoryRefresh();
165
185
  }
166
186
  /** Stop forwarding the current session's events and open a new generation; overload recovery resumes or abandons it. */
167
187
  suspend() {
@@ -201,50 +221,126 @@ export class PiEngineRuntime {
201
221
  /** Unsubscribe, stop observing compaction, and dispose the runtime; the session reference is kept for final reads. */
202
222
  async dispose() {
203
223
  this.#disposed = true;
204
- if (this.#pullRequestTimer !== undefined)
205
- clearTimeout(this.#pullRequestTimer);
206
- this.#pullRequestTimer = undefined;
207
- this.#pullRequestAbort?.abort();
208
- this.#pullRequestAbort = undefined;
224
+ this.#repositoryRefreshEnabled = false;
225
+ if (this.#repositoryRefreshTimer !== undefined)
226
+ clearTimeout(this.#repositoryRefreshTimer);
227
+ this.#repositoryRefreshTimer = undefined;
228
+ this.#repositoryRefreshPending = false;
229
+ this.#repositoryRefreshAbort?.abort();
230
+ const repositoryRefreshTask = this.#repositoryRefreshTask;
209
231
  this.#unsubscribe?.();
210
232
  this.#unsubscribe = undefined;
211
233
  this.#compactionProgress?.dispose();
212
234
  this.#compactionProgress = null;
235
+ await repositoryRefreshTask;
213
236
  await this.#runtime?.dispose();
214
237
  }
215
- #startPullRequestRefresh() {
216
- if (this.#gitBranch === null || this.#disposed || this.#ports.disposed())
238
+ #startRepositoryRefresh() {
239
+ if (!this.#repositoryRefreshEnabled || this.#disposed || this.#ports.disposed())
240
+ return;
241
+ if (this.#repositoryRefreshTask !== undefined) {
242
+ this.#repositoryRefreshPending = true;
217
243
  return;
218
- void this.#refreshPullRequest();
244
+ }
245
+ const task = this.#refreshRepositoryMetadata();
246
+ this.#repositoryRefreshTask = task;
247
+ const settled = () => {
248
+ if (this.#repositoryRefreshTask === task)
249
+ this.#repositoryRefreshTask = undefined;
250
+ if (!this.#repositoryRefreshPending)
251
+ return;
252
+ this.#repositoryRefreshPending = false;
253
+ this.#startRepositoryRefresh();
254
+ };
255
+ void task.then(settled, settled);
219
256
  }
220
- async #refreshPullRequest() {
221
- const branch = this.#gitBranch;
222
- if (branch === null || this.#pullRequestAbort !== undefined || this.#disposed || this.#ports.disposed())
257
+ #resetRepositoryRefresh() {
258
+ if (this.#repositoryRefreshTimer !== undefined)
259
+ clearTimeout(this.#repositoryRefreshTimer);
260
+ this.#repositoryRefreshTimer = undefined;
261
+ this.#repositoryRefreshAbort?.abort();
262
+ this.#repositoryCwd = this.#cwd;
263
+ this.#gitBranch = null;
264
+ this.#pullRequest = null;
265
+ this.#lastPullRequestProbeAt = null;
266
+ }
267
+ async #refreshRepositoryMetadata() {
268
+ if (this.#repositoryRefreshAbort !== undefined || this.#disposed || this.#ports.disposed())
223
269
  return;
224
270
  const controller = new AbortController();
225
- this.#pullRequestAbort = controller;
226
- let next = null;
271
+ const generation = this.#sessionBindingGeneration;
272
+ this.#repositoryRefreshAbort = controller;
273
+ let associatedContext = null;
274
+ const manager = this.#session?.sessionManager;
275
+ const sessionId = manager?.getSessionId?.();
276
+ const sessionFile = manager?.getSessionFile?.();
227
277
  try {
228
- next = await this.#pullRequestProbe(this.#cwd, branch, controller.signal);
278
+ if (typeof sessionId === "string" && sessionId.length > 0 && typeof sessionFile === "string" && sessionFile.length > 0) {
279
+ try {
280
+ associatedContext = await this.#repositoryContextReader(sessionId, sessionFile, controller.signal);
281
+ }
282
+ catch {
283
+ associatedContext = null;
284
+ }
285
+ }
286
+ const nextCwd = associatedContext?.cwd ?? this.#cwd;
287
+ let nextBranch = associatedContext?.branch ?? null;
288
+ if (associatedContext === null) {
289
+ try {
290
+ nextBranch = await this.#gitBranchReader(nextCwd, controller.signal);
291
+ }
292
+ catch {
293
+ nextBranch = null;
294
+ }
295
+ }
296
+ const metadataChanged = nextCwd !== this.#repositoryCwd || nextBranch !== this.#gitBranch;
297
+ const remoteDue = metadataChanged || this.#lastPullRequestProbeAt === null
298
+ || Date.now() - this.#lastPullRequestProbeAt >= this.#pullRequestRefreshMs;
299
+ if (remoteDue) {
300
+ let nextPullRequest = null;
301
+ try {
302
+ nextPullRequest = nextBranch === null
303
+ ? null
304
+ : await this.#pullRequestProbe(nextCwd, nextBranch, controller.signal);
305
+ }
306
+ catch {
307
+ nextPullRequest = null;
308
+ }
309
+ if (controller.signal.aborted || generation !== this.#sessionBindingGeneration || this.#disposed || this.#ports.disposed())
310
+ return;
311
+ const changed = nextCwd !== this.#repositoryCwd
312
+ || nextBranch !== this.#gitBranch
313
+ || !samePullRequest(this.#pullRequest, nextPullRequest);
314
+ this.#repositoryCwd = nextCwd;
315
+ this.#gitBranch = nextBranch;
316
+ this.#pullRequest = nextPullRequest;
317
+ this.#lastPullRequestProbeAt = Date.now();
318
+ if (changed)
319
+ this.#ports.emitView();
320
+ }
229
321
  }
230
322
  catch {
231
- next = null;
323
+ if (!controller.signal.aborted && generation === this.#sessionBindingGeneration && !this.#disposed && !this.#ports.disposed()) {
324
+ const changed = this.#repositoryCwd !== this.#cwd || this.#gitBranch !== null || this.#pullRequest !== null;
325
+ this.#repositoryCwd = this.#cwd;
326
+ this.#gitBranch = null;
327
+ this.#pullRequest = null;
328
+ this.#lastPullRequestProbeAt = Date.now();
329
+ if (changed)
330
+ this.#ports.emitView();
331
+ }
232
332
  }
233
333
  finally {
234
- if (this.#pullRequestAbort === controller)
235
- this.#pullRequestAbort = undefined;
334
+ if (this.#repositoryRefreshAbort === controller)
335
+ this.#repositoryRefreshAbort = undefined;
236
336
  }
237
- if (controller.signal.aborted || this.#disposed || this.#ports.disposed())
337
+ if (controller.signal.aborted || generation !== this.#sessionBindingGeneration || this.#disposed || this.#ports.disposed())
238
338
  return;
239
- if (!samePullRequest(this.#pullRequest, next)) {
240
- this.#pullRequest = next;
241
- this.#ports.emitView();
242
- }
243
- this.#pullRequestTimer = setTimeout(() => {
244
- this.#pullRequestTimer = undefined;
245
- void this.#refreshPullRequest();
246
- }, this.#pullRequestRefreshMs);
247
- this.#pullRequestTimer.unref?.();
339
+ this.#repositoryRefreshTimer = setTimeout(() => {
340
+ this.#repositoryRefreshTimer = undefined;
341
+ this.#startRepositoryRefresh();
342
+ }, this.#repositoryContextPollMs);
343
+ this.#repositoryRefreshTimer.unref?.();
248
344
  }
249
345
  #subscribe() {
250
346
  const session = this.#session;
@@ -296,9 +392,16 @@ async function checkDefaultPiPackageUpdates(cwd, agentDir, settingsManager) {
296
392
  function samePullRequest(left, right) {
297
393
  return left?.number === right?.number && left?.url === right?.url;
298
394
  }
299
- async function readGitBranch(cwd) {
395
+ async function readGitBranch(cwd, signal) {
300
396
  try {
301
- const { stdout } = await execFileAsync("git", ["branch", "--show-current"], { cwd, windowsHide: true });
397
+ const { stdout } = await execFileAsync("git", ["branch", "--show-current"], {
398
+ cwd,
399
+ windowsHide: true,
400
+ timeout: GIT_BRANCH_TIMEOUT_MS,
401
+ maxBuffer: GIT_BRANCH_MAX_BUFFER_BYTES,
402
+ signal,
403
+ encoding: "utf8",
404
+ });
302
405
  const branch = stdout.trim();
303
406
  return branch.length > 0 ? branch : null;
304
407
  }
@@ -2,6 +2,39 @@ import { toolRenderingInput } from "./tool-rendering.js";
2
2
  import { TranscriptImageAssets } from "./transcript-image-assets.js";
3
3
  import { acceptsTranscriptUpdate, transcriptToolState } from "../../../contracts/owned-ui/index.js";
4
4
  import { assistantContent, contentImageCount, isRecord, jsonSummary, messageFallbackKey, retainCompletedArguments, sameBlockContent, sanitizeJson, stringValue, textFromContent } from "./message-values.js";
5
+ const IMAGE_DIMENSION_NOTE = /^\[Image: original \d+x\d+, displayed at \d+x\d+\. Multiply coordinates by \d+(?:\.\d+)? to map to original image\.\]$/u;
6
+ const IMAGE_CONVERSION_NOTE = /^\[Image converted from image\/[a-z0-9.+-]+ to image\/[a-z0-9.+-]+\.\]$/iu;
7
+ const IMAGE_OMISSION_NOTE = /^\[Image omitted: could not be (?:converted to a supported inline image format|resized below the inline image size limit)\.\]$/u;
8
+ function isImageProcessingHint(line) {
9
+ return IMAGE_DIMENSION_NOTE.test(line) || IMAGE_CONVERSION_NOTE.test(line) || IMAGE_OMISSION_NOTE.test(line);
10
+ }
11
+ function userPresentation(content) {
12
+ const imageCount = contentImageCount(content);
13
+ if (imageCount === 0)
14
+ return undefined;
15
+ const lines = textFromContent(content).split("\n");
16
+ const retainedHints = [];
17
+ let dimensionCount = 0;
18
+ let hintStart = lines.length;
19
+ while (hintStart > 0) {
20
+ const line = lines[hintStart - 1];
21
+ if (!isImageProcessingHint(line))
22
+ break;
23
+ if (IMAGE_DIMENSION_NOTE.test(line))
24
+ dimensionCount++;
25
+ else
26
+ retainedHints.unshift(line);
27
+ hintStart--;
28
+ }
29
+ if (dimensionCount === 0 || dimensionCount > imageCount)
30
+ return undefined;
31
+ lines.splice(hintStart);
32
+ if (lines.at(-1) === "")
33
+ lines.pop();
34
+ if (retainedHints.length > 0)
35
+ lines.push("", ...retainedHints);
36
+ return { visibleText: lines.join("\n") };
37
+ }
5
38
  /**
6
39
  * The transcript as the owned UI sees it: Pi's session messages and tool events projected into
7
40
  * ordered, revisioned blocks. The projection owns block identity (message and tool ids that stay
@@ -122,6 +155,7 @@ export class PiTranscriptProjection {
122
155
  return [];
123
156
  const baseId = this.#messageBlockId(message, fallbackIndex, status, occurrence);
124
157
  if (message.role === "user") {
158
+ const presentation = userPresentation(message.content);
125
159
  return [{
126
160
  id: baseId,
127
161
  kind: "user",
@@ -129,6 +163,7 @@ export class PiTranscriptProjection {
129
163
  revision: this.nextRevision(baseId),
130
164
  title: "User",
131
165
  text: textFromContent(message.content),
166
+ ...(presentation === undefined ? {} : { userPresentation: presentation }),
132
167
  imageReferences: this.imageReferences(message.content, "user"),
133
168
  payload: {
134
169
  role: "user",
@@ -16,6 +16,8 @@
16
16
  "dist/integrations/pi/components/upstream/adjacent/core/keybindings.js",
17
17
  "dist/integrations/pi/components/upstream/components/earendil-announcement.js",
18
18
  "dist/integrations/pi/components/upstream/components/extension-editor.js",
19
+ "dist/integrations/pi/components/upstream/components/extension-input.js",
20
+ "dist/integrations/pi/components/upstream/components/extension-selector.js",
19
21
  "dist/integrations/pi/components/upstream/components/scoped-models-selector.js",
20
22
  "dist/integrations/pi/components/upstream/components/session-selector.js",
21
23
  "dist/integrations/pi/components/upstream/components/status-indicator.js",
@@ -5,7 +5,7 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-22T17:55:39.177Z",
8
+ "builtAt": "2026-09-22T23:31:15.773Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "9db1726bbe3fc2e8292f2ead1e7e9d0fd4d7d0dc9217b372582bf354b0f565dc",
@@ -5,7 +5,7 @@
5
5
  "platform": "linux",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-22T17:55:47.994Z",
8
+ "builtAt": "2026-09-22T23:30:53.377Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "d8cda6b0c7cb36c0cc41e90802aceebf6c02eaebbc08a83d7d963d2e918dbd7a",
@@ -5,10 +5,10 @@
5
5
  "platform": "win32",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-22T17:56:18.215Z",
8
+ "builtAt": "2026-09-22T23:31:43.770Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian.exe",
11
- "sha256": "50626418cdc7a539611dca682b0adc3a806f7aa93930007c96244f2a9aac156e",
11
+ "sha256": "57ed4829927f81b1cb638ffafa7a5b3832537284aa343c0eb0ef70592ed671d3",
12
12
  "size": 177664
13
13
  },
14
14
  "provenance": {