@timurproko/a1 0.2.6-dev.717 → 0.2.6-dev.727

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 (143) hide show
  1. package/bin/cli.js +13 -2
  2. package/bin/ui.js +13 -0
  3. package/dist/app/session-shell/prompt-suggestion-controller.d.ts +5 -4
  4. package/dist/app/session-shell/prompt-suggestion-controller.js +148 -58
  5. package/dist/app/session-shell/session-shell-root.d.ts +2 -1
  6. package/dist/app/session-shell/session-shell-root.js +13 -6
  7. package/dist/app/session-shell/session-shell.js +45 -3
  8. package/dist/app/session-shell/session-viewport-controller.js +33 -21
  9. package/dist/cli/dispatch.js +5 -3
  10. package/dist/cli/session-context.d.ts +5 -1
  11. package/dist/cli/session-context.js +25 -8
  12. package/dist/composition/owned-ui.d.ts +3 -0
  13. package/dist/composition/owned-ui.js +66 -6
  14. package/dist/composition/settings-route-host.js +2 -1
  15. package/dist/contracts/owned-ui/index.d.ts +4 -2
  16. package/dist/contracts/owned-ui/index.js +1 -1
  17. package/dist/contracts/owned-ui/model.d.ts +4 -0
  18. package/dist/contracts/owned-ui/suggestion-diagnostics.d.ts +5 -1
  19. package/dist/contracts/owned-ui/suggestion-diagnostics.js +3 -1
  20. package/dist/contracts/owned-ui/validation.js +7 -0
  21. package/dist/features/launch/index.d.ts +1 -1
  22. package/dist/features/launch/session-context.d.ts +17 -2
  23. package/dist/features/launch/session-context.js +9 -5
  24. package/dist/features/owned-ui/settings-app.js +21 -39
  25. package/dist/features/prompt-suggestions/diagnostics.js +4 -1
  26. package/dist/foundation/lifecycle/index.d.ts +2 -2
  27. package/dist/foundation/lifecycle/index.js +1 -1
  28. package/dist/foundation/lifecycle/session-repository-context.d.ts +19 -2
  29. package/dist/foundation/lifecycle/session-repository-context.js +434 -66
  30. package/dist/foundation/process-containment/index.d.ts +1 -0
  31. package/dist/foundation/process-containment/index.js +1 -0
  32. package/dist/foundation/process-containment/native-process-inspector.d.ts +3 -0
  33. package/dist/foundation/process-containment/native-process-inspector.js +21 -0
  34. package/dist/foundation/release/progress-palette.generated.d.ts +2 -2
  35. package/dist/foundation/release/progress-palette.generated.js +2 -2
  36. package/dist/foundation/startup/startup-descriptor.js +2 -2
  37. package/dist/integrations/pi/components/components.js +2 -2
  38. package/dist/integrations/pi/components/index.d.ts +2 -2
  39. package/dist/integrations/pi/components/index.js +1 -1
  40. package/dist/integrations/pi/components/modal-frame.js +3 -4
  41. package/dist/integrations/pi/components/models-dialog.js +5 -5
  42. package/dist/integrations/pi/components/prompt-input-port.d.ts +1 -0
  43. package/dist/integrations/pi/components/resources/builtin-themes.d.ts +2 -2
  44. package/dist/integrations/pi/components/resources/builtin-themes.js +2 -2
  45. package/dist/integrations/pi/components/share-operation-dialog.js +3 -3
  46. package/dist/integrations/pi/components/shell-editor-autocomplete.js +8 -7
  47. package/dist/integrations/pi/components/shell-extension-ui.d.ts +2 -0
  48. package/dist/integrations/pi/components/shell-extension-ui.js +20 -10
  49. package/dist/integrations/pi/components/shell-hotkey-sections.js +2 -2
  50. package/dist/integrations/pi/components/shell-presenters-info.js +10 -10
  51. package/dist/integrations/pi/components/shell-presenters-transcript.js +10 -10
  52. package/dist/integrations/pi/components/shell-selectors-dialogs.js +5 -6
  53. package/dist/integrations/pi/components/shell-shared-facade.js +1 -1
  54. package/dist/integrations/pi/components/skills-dialog.js +3 -3
  55. package/dist/integrations/pi/components/submitted-prompt-adapter.js +2 -3
  56. package/dist/integrations/pi/components/theme.d.ts +9 -0
  57. package/dist/integrations/pi/components/theme.js +30 -0
  58. package/dist/integrations/pi/components/tool-result-adapter.js +6 -1
  59. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -1
  60. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +1 -1
  61. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +2 -2
  62. package/dist/integrations/pi/components/upstream/components/countdown-timer.d.ts +1 -1
  63. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +1 -1
  64. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +1 -1
  65. package/dist/integrations/pi/components/upstream/components/extension-editor.js +6 -6
  66. package/dist/integrations/pi/components/upstream/components/extension-input.js +4 -4
  67. package/dist/integrations/pi/components/upstream/components/extension-selector.js +4 -4
  68. package/dist/integrations/pi/components/upstream/components/mermaid.js +1 -1
  69. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +5 -5
  70. package/dist/integrations/pi/components/upstream/components/owned-editor.js +8 -5
  71. package/dist/integrations/pi/components/upstream/components/pi-logo.js +1 -1
  72. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
  73. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +3 -3
  74. package/dist/integrations/pi/components/upstream/components/session-footer.js +1 -1
  75. package/dist/integrations/pi/components/upstream/components/session-selector-search.js +1 -1
  76. package/dist/integrations/pi/components/upstream/components/session-selector.js +9 -9
  77. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.d.ts +3 -2
  78. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.js +6 -3
  79. package/dist/integrations/pi/components/upstream/components/status-indicator.d.ts +1 -1
  80. package/dist/integrations/pi/components/upstream/components/status-indicator.js +1 -1
  81. package/dist/integrations/pi/components/upstream/components/themed-text.d.ts +1 -1
  82. package/dist/integrations/pi/components/upstream/components/themed-text.js +1 -1
  83. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +1 -1
  84. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +2 -2
  85. package/dist/integrations/pi/components/upstream/components/tool-execution.d.ts +4 -0
  86. package/dist/integrations/pi/components/upstream/components/tool-execution.js +11 -1
  87. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +2 -2
  88. package/dist/integrations/pi/components/upstream/components/tree-selector.js +27 -18
  89. package/dist/integrations/pi/components/upstream/components/trust-selector.js +1 -1
  90. package/dist/integrations/pi/components/upstream/external-editor.js +1 -1
  91. package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +4 -4
  92. package/dist/integrations/pi/components/upstream/history/editor-core.js +11 -7
  93. package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +1 -1
  94. package/dist/integrations/pi/components/upstream/history/kill-ring.js +1 -1
  95. package/dist/integrations/pi/components/upstream/history/printable-key.js +1 -1
  96. package/dist/integrations/pi/components/upstream/history/text-helpers.js +1 -1
  97. package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +1 -1
  98. package/dist/integrations/pi/components/upstream/history/undo-stack.js +1 -1
  99. package/dist/integrations/pi/components/upstream/history/word-navigation.js +1 -1
  100. package/dist/integrations/pi/components/upstream/model-search.d.ts +1 -1
  101. package/dist/integrations/pi/components/upstream/program-status-reporter.d.ts +31 -0
  102. package/dist/integrations/pi/components/upstream/program-status-reporter.js +89 -0
  103. package/dist/integrations/pi/components/upstream/theme/system-theme.d.ts +1 -1
  104. package/dist/integrations/pi/components/upstream/theme/system-theme.js +1 -1
  105. package/dist/integrations/pi/components/upstream/theme/theme-controller.d.ts +1 -1
  106. package/dist/integrations/pi/components/upstream/theme/theme.d.ts +18 -1
  107. package/dist/integrations/pi/components/upstream/theme/theme.js +207 -10
  108. package/dist/integrations/pi/engine/pi-settings-metadata.json +1 -1
  109. package/dist/integrations/pi/engine/resources/changelog.json +4 -4
  110. package/dist/integrations/pi/engine/session-events.js +10 -2
  111. package/dist/integrations/pi/engine/transcript-projection.js +2 -1
  112. package/dist/integrations/pi/startup-public.js +306 -290
  113. package/dist/integrations/pi/startup-public.manifest.json +289 -265
  114. package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -1
  115. package/dist/integrations/pi/tui-runtime/adapter.js +7 -0
  116. package/dist/integrations/pi/tui-runtime/contracts.d.ts +3 -1
  117. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +1 -1
  118. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +1 -1
  119. package/dist/native/darwin-arm64/manifest.json +1 -1
  120. package/dist/native/linux-x64/manifest.json +1 -1
  121. package/dist/native/win32-x64/manifest.json +2 -2
  122. package/dist/native/win32-x64/process-guardian.exe +0 -0
  123. package/dist/product-identity.d.ts +2 -2
  124. package/dist/product-identity.js +2 -2
  125. package/dist/product-identity.json +3 -1
  126. package/dist/runtime-payload-inventory.json +3092 -2878
  127. package/dist/ui/components/prompt-input.d.ts +2 -0
  128. package/dist/ui/components/prompt-input.js +5 -1
  129. package/dist/ui/components/text-selection.d.ts +1 -1
  130. package/dist/ui/components/text-selection.js +3 -1
  131. package/dist/ui/components/theme.d.ts +2 -0
  132. package/dist/ui/components/transcript-viewport.d.ts +6 -4
  133. package/dist/ui/components/transcript-viewport.js +94 -16
  134. package/dist/ui/components/value-menu.d.ts +2 -0
  135. package/dist/ui/components/value-menu.js +13 -8
  136. package/dist/ui/settings/declarations.d.ts +26 -14
  137. package/dist/ui/settings/declarations.js +24 -11
  138. package/dist/ui/settings/migrations.js +16 -0
  139. package/docs/architecture/project-structure.md +4 -3
  140. package/docs/architecture/prompt-suggestions.md +14 -10
  141. package/docs/local-worktree-cleanup.md +11 -5
  142. package/docs/openspec-archive-automation.md +6 -5
  143. package/package.json +3 -3
package/bin/cli.js CHANGED
@@ -55,8 +55,19 @@ process.exitCode = await dispatchCli(process.argv.slice(2), {
55
55
  return await runPackageCommand(request, { createPort: createPiPackagesPort });
56
56
  },
57
57
  sessionContext: async request => {
58
- const { runSessionContextCommand } = await import("../dist/cli/session-context.js");
59
- return await runSessionContextCommand(request);
58
+ const [{ runSessionContextCommand }, { createNativeProcessInspector }, { readLaunchContext }] = await Promise.all([
59
+ import("../dist/cli/session-context.js"),
60
+ import("../dist/foundation/process-containment/native-process-inspector.js"),
61
+ import("../dist/foundation/launch-context/index.js"),
62
+ ]);
63
+ const launch = readLaunchContext(process.env);
64
+ let inspector;
65
+ return await runSessionContextCommand(request, {
66
+ inspectProcess: async pid => {
67
+ inspector ??= createNativeProcessInspector(process.env, process.platform, launch.releaseRoot);
68
+ return (await inspector).observe(pid);
69
+ },
70
+ });
60
71
  },
61
72
  }, {
62
73
  stdout: message => process.stdout.write(message),
package/bin/ui.js CHANGED
@@ -9,6 +9,13 @@ const { readLaunchContext } = await import("../dist/foundation/launch-context/in
9
9
  const launchContext = readLaunchContext(process.env, "profile");
10
10
  const profile = launchContext.launchProfile;
11
11
  if (profile === undefined) throw new Error("A1 launch profile is missing");
12
+ if (profile === "a1") {
13
+ const [{ randomUUID }, { PRODUCT_IDENTITY }] = await Promise.all([
14
+ import("node:crypto"),
15
+ import("../dist/product-identity.js"),
16
+ ]);
17
+ process.env[PRODUCT_IDENTITY.environment.sessionRuntimeId] = randomUUID();
18
+ }
12
19
  const { installFatalExit } = await import("../dist/foundation/terminal-cleanup/fatal-exit.js");
13
20
  const { resolveProductPaths } = await import("../dist/foundation/lifecycle/paths.js");
14
21
  const { join } = await import("node:path");
@@ -49,10 +56,16 @@ Promise.resolve().then(() => {
49
56
  if (sessionSelection && profile !== "a1") throw new Error("session selection requires the normal A1 profile");
50
57
  return runSelectedInteractiveRuntime(profile, {
51
58
  ownedUi: async (profileId, ownedSurfaces) => {
59
+ const processInspector = ownedSurfaces
60
+ ? await import("../dist/foundation/process-containment/native-process-inspector.js")
61
+ .then(module => module.createNativeProcessInspector(process.env, process.platform, launchContext.releaseRoot))
62
+ .catch(() => null)
63
+ : null;
52
64
  const { application, settings } = await composeOwnedUi({
53
65
  cwd: process.cwd(),
54
66
  profileId,
55
67
  ownedSurfaces,
68
+ ...(processInspector === null ? {} : { inspectProcess: pid => processInspector.observe(pid) }),
56
69
  projectTrustPrompt: createConsoleProjectTrustPrompt({ presentation: profile === "a1" ? "bare" : "comparison" }),
57
70
  sessionForkPrompt: createConsoleSessionForkPrompt(),
58
71
  ...(sessionSelection === undefined ? {} : { sessionSelection }),
@@ -17,7 +17,7 @@ export interface ContextualPromptSuggestionControllerOptions {
17
17
  type DecisionIdentity = Omit<OwnedUiPromptSuggestionIdentity, "model"> & {
18
18
  readonly model: OwnedUiPromptSuggestionIdentity["model"] | null;
19
19
  };
20
- /** Prefetches one inert candidate; owns retirement even when a provider ignores abort. */
20
+ /** Prefetches one inert candidate and owns one bounded settlement/retry recovery lifecycle. */
21
21
  export declare class ContextualPromptSuggestionController {
22
22
  #private;
23
23
  readonly options: ContextualPromptSuggestionControllerOptions;
@@ -26,12 +26,13 @@ export declare class ContextualPromptSuggestionController {
26
26
  setEnabled(enabled: boolean): void;
27
27
  skip(identity: DecisionIdentity, reason: SuggestionDecisionReason): void;
28
28
  consider(identity: OwnedUiPromptSuggestionIdentity, decision: boolean | SuggestionDecision): void;
29
- settle(identity: OwnedUiPromptSuggestionIdentity): void;
29
+ /** Settles an existing prefetch or starts the first request when that eligible prefetch was missed. */
30
+ settle(identity: OwnedUiPromptSuggestionIdentity, decision?: boolean | SuggestionDecision): void;
30
31
  accept(): void;
31
32
  invalidate(): void;
32
- /** Retires only a generating request; a prepared or available suggestion survives the draft edit. */
33
+ /** Retires generating or scheduled recovery; a prepared or available suggestion survives a draft edit. */
33
34
  abortPending(): void;
34
- /** Reasserts retained ghost text after an editor mutation makes the prompt eligible again. */
35
+ /** Reasserts retained ghost text or retries a deferred presentation when the editor is eligible. */
35
36
  restoreAvailable(): void;
36
37
  dispose(): void;
37
38
  }
@@ -1,9 +1,17 @@
1
1
  import { assertOwnedUiPromptSuggestionResult, normalizePromptSuggestionCandidate, } from "../../contracts/owned-ui/index.js";
2
- /** Prefetches one inert candidate; owns retirement even when a provider ignores abort. */
2
+ const TRANSIENT_PRESENTATION_REASONS = new Set([
3
+ "not-ready", "not-focused", "autocomplete", "prompt-mode",
4
+ ]);
5
+ const RETRYABLE_OUTCOMES = new Set([
6
+ "empty", "rejected", "provider-failure", "unavailable", "timeout",
7
+ ]);
8
+ /** Prefetches one inert candidate and owns one bounded settlement/retry recovery lifecycle. */
3
9
  export class ContextualPromptSuggestionController {
4
10
  #state = { status: "idle" };
11
+ #lifecycle = null;
5
12
  #request = null;
6
13
  #lastConsidered = "";
14
+ #retiredIdentity = "";
7
15
  #enabled;
8
16
  #disposed = false;
9
17
  #sessionKey = "";
@@ -27,6 +35,7 @@ export class ContextualPromptSuggestionController {
27
35
  }
28
36
  skip(identity, reason) {
29
37
  this.invalidate();
38
+ this.#lastConsidered = decisionIdentityKey(identity);
30
39
  this.#emit(this.#metadata(identity), "skipped", reason);
31
40
  }
32
41
  consider(identity, decision) {
@@ -34,51 +43,43 @@ export class ContextualPromptSuggestionController {
34
43
  if (key === this.#lastConsidered)
35
44
  return;
36
45
  this.#lastConsidered = key;
37
- this.invalidate();
38
- const reason = this.#disposed ? "disposed" : !this.#enabled ? "disabled"
39
- : decision === false ? "ineligible" : decision === true ? null : decision;
46
+ this.#reset(true, true);
47
+ const reason = this.#decisionReason(decision);
40
48
  if (reason !== null) {
41
49
  this.#emit(this.#metadata(identity), "skipped", reason);
42
50
  return;
43
51
  }
44
- let reasoning = "unavailable";
45
- try {
46
- reasoning = this.options.generator.suggestionReasoningPolicy?.() ?? "unavailable";
47
- }
48
- catch { /* Security: isolate diagnostic metadata failure. */ }
49
- const request = {
50
- identity,
51
- metadata: { ...this.#metadata(identity), request: ++this.#requestSequence, reasoning },
52
- abort: new AbortController(), startedAt: this.#now(), terminal: false,
53
- };
54
- this.#request = request;
55
- this.#state = { status: "generating", identity, settled: false };
56
- this.#emit(request.metadata, "started");
57
- request.timer = setTimeout(() => {
58
- if (this.#request !== request || this.#state.status !== "generating")
59
- return;
60
- this.#finish(request, "timeout");
61
- this.#state = { status: "idle" };
62
- this.#request = null;
63
- request.abort.abort();
64
- }, this.#timeoutMs);
65
- request.timer.unref?.();
66
- try {
67
- void this.options.generator.generate({ identity, signal: request.abort.signal })
68
- .then(result => this.#receive(request, result), () => this.#failed(request))
69
- .finally(() => clearTimeout(request.timer));
70
- }
71
- catch {
72
- this.#failed(request);
73
- }
52
+ this.#retiredIdentity = "";
53
+ this.#lifecycle = { identity, settled: false, attempts: 0, retryPending: false, presentationDeferred: false };
54
+ this.#startAttempt("prefetch");
74
55
  }
75
- settle(identity) {
76
- if (this.#disposed || !this.#enabled || this.#state.status === "idle")
56
+ /** Settles an existing prefetch or starts the first request when that eligible prefetch was missed. */
57
+ settle(identity, decision = null) {
58
+ if (this.#disposed || !this.#enabled || identityKey(identity) === this.#retiredIdentity)
77
59
  return;
78
- if (!samePromptSuggestionIdentity(this.#state.identity, identity)) {
60
+ const reason = this.#decisionReason(decision);
61
+ const lifecycle = this.#lifecycle;
62
+ if (lifecycle !== null && !samePromptSuggestionIdentity(lifecycle.identity, identity)) {
79
63
  if (this.#request)
80
64
  this.#finish(this.#request, "stale-result", "stale-identity");
81
- this.invalidate();
65
+ this.#reset(true, true);
66
+ }
67
+ if (reason !== null) {
68
+ if (this.#lifecycle !== null)
69
+ this.#reset(true, true);
70
+ else
71
+ this.#emit(this.#metadata(identity), "skipped", reason);
72
+ return;
73
+ }
74
+ if (this.#lifecycle === null) {
75
+ this.#retiredIdentity = "";
76
+ this.#lifecycle = { identity, settled: true, attempts: 0, retryPending: false, presentationDeferred: false };
77
+ this.#startAttempt("settlement");
78
+ return;
79
+ }
80
+ this.#lifecycle.settled = true;
81
+ if (this.#lifecycle.retryPending) {
82
+ this.#startAttempt("retry");
82
83
  return;
83
84
  }
84
85
  if (this.#state.status === "generating")
@@ -86,23 +87,23 @@ export class ContextualPromptSuggestionController {
86
87
  else if (this.#state.status === "prepared" && this.#request)
87
88
  this.#show(this.#request, this.#state.text);
88
89
  }
89
- accept() { this.#invalidate(false); }
90
- invalidate() { this.#invalidate(true); }
91
- /** Retires only a generating request; a prepared or available suggestion survives the draft edit. */
90
+ accept() { this.#reset(false, true); }
91
+ invalidate() { this.#reset(true, true); }
92
+ /** Retires generating or scheduled recovery; a prepared or available suggestion survives a draft edit. */
92
93
  abortPending() {
93
- if (this.#state.status !== "generating")
94
- return;
95
- const request = this.#request;
96
- this.#request = null;
97
- this.#state = { status: "idle" };
98
- if (request === null)
94
+ if (this.#state.status !== "generating" && !(this.#state.status === "idle" && this.#lifecycle?.retryPending))
99
95
  return;
100
- this.#finish(request, "cancelled");
101
- request.abort.abort();
96
+ this.#reset(false, true);
102
97
  }
103
- /** Reasserts retained ghost text after an editor mutation makes the prompt eligible again. */
98
+ /** Reasserts retained ghost text or retries a deferred presentation when the editor is eligible. */
104
99
  restoreAvailable() {
105
- if (this.#disposed || !this.#enabled || this.#state.status !== "available")
100
+ if (this.#disposed || !this.#enabled)
101
+ return;
102
+ if (this.#state.status === "prepared" && this.#lifecycle?.settled && this.#request) {
103
+ this.#show(this.#request, this.#state.text);
104
+ return;
105
+ }
106
+ if (this.#state.status !== "available")
106
107
  return;
107
108
  const reason = this.options.surface.presentationBlockReason?.(this.#state.identity)
108
109
  ?? (this.options.surface.canPresent(this.#state.identity) ? null : "presentation-unavailable");
@@ -114,16 +115,64 @@ export class ContextualPromptSuggestionController {
114
115
  this.#disposed = true;
115
116
  this.invalidate();
116
117
  this.#lastConsidered = "";
118
+ this.#retiredIdentity = "";
117
119
  this.#sessionKey = "";
118
120
  }
119
- #invalidate(clearSurface) {
121
+ #decisionReason(decision) {
122
+ return this.#disposed ? "disposed" : !this.#enabled ? "disabled"
123
+ : decision === false ? "ineligible" : decision === true ? null : decision;
124
+ }
125
+ #startAttempt(trigger) {
126
+ const lifecycle = this.#lifecycle;
127
+ if (lifecycle === null || this.#request !== null || lifecycle.attempts >= 2)
128
+ return;
129
+ lifecycle.retryPending = false;
130
+ lifecycle.presentationDeferred = false;
131
+ const attempt = ++lifecycle.attempts;
132
+ let reasoning = "unavailable";
133
+ try {
134
+ reasoning = this.options.generator.suggestionReasoningPolicy?.() ?? "unavailable";
135
+ }
136
+ catch { /* Security: isolate diagnostic metadata failure. */ }
137
+ const request = {
138
+ identity: lifecycle.identity,
139
+ metadata: { ...this.#metadata(lifecycle.identity), request: ++this.#requestSequence, reasoning, attempt, trigger },
140
+ abort: new AbortController(), startedAt: this.#now(), terminal: false,
141
+ };
142
+ this.#request = request;
143
+ this.#state = { status: "generating", identity: lifecycle.identity, settled: lifecycle.settled };
144
+ this.#emit(request.metadata, "started");
145
+ request.timer = setTimeout(() => {
146
+ if (this.#request !== request || this.#state.status !== "generating")
147
+ return;
148
+ this.#finish(request, "timeout");
149
+ this.#request = null;
150
+ this.#state = { status: "idle" };
151
+ request.abort.abort();
152
+ this.#completeNoCandidate(request, "timeout");
153
+ }, this.#timeoutMs);
154
+ request.timer.unref?.();
155
+ try {
156
+ void this.options.generator.generate({ identity: lifecycle.identity, signal: request.abort.signal })
157
+ .then(result => this.#receive(request, result), () => this.#failed(request))
158
+ .finally(() => clearTimeout(request.timer));
159
+ }
160
+ catch {
161
+ this.#failed(request);
162
+ }
163
+ }
164
+ #reset(clearSurface, retire) {
120
165
  const hadVisibleSuggestion = this.#state.status === "available";
166
+ const identity = this.#lifecycle?.identity ?? (this.#state.status === "idle" ? null : this.#state.identity);
121
167
  const request = this.#request;
122
168
  if (request)
123
169
  this.#finish(request, "cancelled");
124
170
  this.#request = null;
171
+ this.#lifecycle = null;
125
172
  this.#state = { status: "idle" };
126
173
  request?.abort.abort();
174
+ if (retire && identity !== null)
175
+ this.#retiredIdentity = identityKey(identity);
127
176
  if (clearSurface && hadVisibleSuggestion) {
128
177
  this.options.surface.clear();
129
178
  this.options.surface.requestRender();
@@ -137,6 +186,7 @@ export class ContextualPromptSuggestionController {
137
186
  this.#finish(request, "provider-failure");
138
187
  this.#request = null;
139
188
  this.#state = { status: "idle" };
189
+ this.#completeNoCandidate(request, "provider-failure");
140
190
  }
141
191
  #receive(request, result) {
142
192
  if (this.#disposed || this.#request !== request || request.terminal || this.#state.status !== "generating") {
@@ -148,41 +198,76 @@ export class ContextualPromptSuggestionController {
148
198
  }
149
199
  catch {
150
200
  this.#finish(request, "rejected");
151
- this.invalidate();
201
+ this.#request = null;
202
+ this.#state = { status: "idle" };
203
+ this.#completeNoCandidate(request, "rejected");
152
204
  return;
153
205
  }
154
206
  if (!samePromptSuggestionIdentity(request.identity, result.identity)) {
155
207
  this.#finish(request, "stale-result", "stale-identity");
156
- this.invalidate();
208
+ this.#reset(true, true);
157
209
  return;
158
210
  }
159
211
  if (result.outcome !== "candidate") {
160
212
  this.#finish(request, result.outcome);
161
- this.invalidate();
213
+ this.#request = null;
214
+ this.#state = { status: "idle" };
215
+ if (RETRYABLE_OUTCOMES.has(result.outcome))
216
+ this.#completeNoCandidate(request, result.outcome);
217
+ else {
218
+ this.#retiredIdentity = identityKey(request.identity);
219
+ this.#lifecycle = null;
220
+ }
162
221
  return;
163
222
  }
164
223
  const text = normalizePromptSuggestionCandidate(result.text);
165
224
  if (text === null) {
166
225
  this.#finish(request, "rejected");
167
- this.invalidate();
226
+ this.#request = null;
227
+ this.#state = { status: "idle" };
228
+ this.#completeNoCandidate(request, "rejected");
168
229
  return;
169
230
  }
170
231
  clearTimeout(request.timer);
171
- if (this.#state.settled)
232
+ if (this.#lifecycle?.settled)
172
233
  this.#show(request, text);
173
234
  else
174
235
  this.#state = { status: "prepared", identity: request.identity, text };
175
236
  }
237
+ #completeNoCandidate(request, outcome) {
238
+ const lifecycle = this.#lifecycle;
239
+ if (lifecycle === null || !samePromptSuggestionIdentity(lifecycle.identity, request.identity))
240
+ return;
241
+ if (lifecycle.attempts >= 2 || !RETRYABLE_OUTCOMES.has(outcome)) {
242
+ this.#emit({ ...request.metadata, elapsedMs: Math.max(0, this.#now() - request.startedAt) }, "retry-exhausted");
243
+ this.#retiredIdentity = identityKey(request.identity);
244
+ this.#lifecycle = null;
245
+ return;
246
+ }
247
+ lifecycle.retryPending = true;
248
+ if (lifecycle.settled)
249
+ this.#startAttempt("retry");
250
+ }
176
251
  #show(request, text) {
177
252
  const reason = this.options.surface.presentationBlockReason?.(request.identity)
178
253
  ?? (this.options.surface.canPresent(request.identity) ? null : "presentation-unavailable");
254
+ if (reason !== null && TRANSIENT_PRESENTATION_REASONS.has(reason)) {
255
+ this.#state = { status: "prepared", identity: request.identity, text };
256
+ if (this.#lifecycle !== null && !this.#lifecycle.presentationDeferred) {
257
+ this.#lifecycle.presentationDeferred = true;
258
+ this.#emit({ ...request.metadata, elapsedMs: Math.max(0, this.#now() - request.startedAt) }, "presentation-deferred", reason);
259
+ }
260
+ return;
261
+ }
179
262
  if (reason !== null || !this.options.surface.present(text)) {
180
263
  this.#finish(request, "presentation-blocked", reason ?? "presentation-unavailable");
181
- this.invalidate();
264
+ this.#reset(true, true);
182
265
  return;
183
266
  }
184
267
  this.#state = { status: "available", identity: request.identity, text };
185
268
  this.#finish(request, "displayed");
269
+ if (this.#lifecycle !== null)
270
+ this.#lifecycle.presentationDeferred = false;
186
271
  this.options.surface.requestRender();
187
272
  }
188
273
  #finish(request, event, reason) {
@@ -227,3 +312,8 @@ export function samePromptSuggestionIdentity(left, right) {
227
312
  function identityKey(identity) {
228
313
  return `${identity.sessionId}\0${identity.sessionGeneration}\0${identity.runSequence}\0${identity.responseSequence}\0${identity.model.providerId}\0${identity.model.modelId}`;
229
314
  }
315
+ function decisionIdentityKey(identity) {
316
+ return identity.model === null
317
+ ? `${identity.sessionId}\0${identity.sessionGeneration}\0${identity.runSequence}\0${identity.responseSequence}\0\0`
318
+ : identityKey(identity);
319
+ }
@@ -172,6 +172,7 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
172
172
  readonly onEditorChange?: (text: string) => void;
173
173
  readonly onPromptSuggestionAccepted?: (text: string) => void;
174
174
  readonly onInputSurfaceChanged?: () => void;
175
+ readonly onPromptSuggestionEligibilityChanged?: () => void;
175
176
  readonly onCopyText?: (text: string) => void;
176
177
  readonly readClipboardContent?: (signal?: AbortSignal) => Promise<PiShellClipboardContent | null>;
177
178
  readonly captureClipboardPaste?: () => PasteSource;
@@ -245,7 +246,7 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
245
246
  maxScroll: number;
246
247
  cause: "initial" | "steady" | "dock-input" | "follow-shift" | "detached" | "geometry-change" | null;
247
248
  };
248
- /** Visible non-selectable Steering, alignment, and Working rows, for rendering evidence. */
249
+ /** Visible non-persistent Steering, alignment, and Working rows, for rendering evidence. */
249
250
  viewportTransientTailRowCount(): number;
250
251
  hasActiveSelection(): boolean;
251
252
  setViewportConfig(config: OwnedUiViewportSettings): void;
@@ -48,6 +48,7 @@ export class OwnedUiSessionShellRoot {
48
48
  #viewportTheme;
49
49
  #onViewportFrame;
50
50
  #onInputSurfaceChanged;
51
+ #onPromptSuggestionEligibilityChanged;
51
52
  #componentRuntime;
52
53
  #toolsExpanded = false;
53
54
  #thinkingVisible = true;
@@ -107,6 +108,7 @@ export class OwnedUiSessionShellRoot {
107
108
  this.#componentRuntime = handlers;
108
109
  this.#onViewportFrame = handlers.onViewportFrame;
109
110
  this.#onInputSurfaceChanged = handlers.onInputSurfaceChanged;
111
+ this.#onPromptSuggestionEligibilityChanged = handlers.onPromptSuggestionEligibilityChanged;
110
112
  this.#dockInputReuseEnabled = handlers.enableDockInputReuse ?? true;
111
113
  this.header = createPiShellHeader({
112
114
  ...startup,
@@ -223,8 +225,8 @@ export class OwnedUiSessionShellRoot {
223
225
  // Invariant: colours come from the active theme, so rendered rows outlive their revision only
224
226
  // until the theme under them changes.
225
227
  this.#themeUnsubscribe = onPiThemeChange(() => {
226
- this.#renderedRows.clear();
227
- this.#documentLayouts.clear();
228
+ this.invalidate();
229
+ this.#componentRuntime.requestRender(true);
228
230
  });
229
231
  }
230
232
  setEditorPaddingX(padding) {
@@ -327,6 +329,7 @@ export class OwnedUiSessionShellRoot {
327
329
  this.editor.setPromptSuggestion(text);
328
330
  }
329
331
  update(view) {
332
+ const becameReady = this.#view.lifecycle !== "ready" && view.lifecycle === "ready";
330
333
  if (view.diagnostics.length !== this.#view.diagnostics.length
331
334
  || view.diagnostics.some((diagnostic, index) => diagnostic.sequence !== this.#view.diagnostics[index]?.sequence)) {
332
335
  this.#documentLayouts.clear();
@@ -349,6 +352,8 @@ export class OwnedUiSessionShellRoot {
349
352
  this.editor.setThinkingLevel(view.thinkingLevel);
350
353
  // Performance: semantic updates already invalidate affected transcript blocks. Chrome is a separate authority.
351
354
  this.#invalidateChrome();
355
+ if (becameReady)
356
+ this.#onPromptSuggestionEligibilityChanged?.();
352
357
  }
353
358
  /**
354
359
  * Applies one block: its component is created or updated in place and the order grows
@@ -458,13 +463,13 @@ export class OwnedUiSessionShellRoot {
458
463
  if (frame === null) {
459
464
  frame = this.#viewportController.compose({
460
465
  // Performance: do not copy the complete document on unchanged dock-only input.
461
- // Steering and Working retain their one non-selectable viewport-tail ownership.
466
+ // Steering and Working retain their one non-persistent viewport-tail ownership.
462
467
  documentRows: [...document.rows, ...steeringRows, ...statusRows],
463
468
  ...(this.#viewportController.transcriptPointerSelecting
464
469
  ? { paintDocumentRow: heldNativeHyperlinkStyle }
465
470
  : {}),
466
- // Invariant: selection and copying stop at the real document tail; Steering,
467
- // fitting alignment, and live Working remain transient presentation chrome.
471
+ // Invariant: this boundary tracks persistent transcript rows for streaming and
472
+ // geometry; transient Steering and Working rows remain complete-frame selectable.
468
473
  selectableDocumentRowCount,
469
474
  ...(document.liveTailStartRow === undefined ? {} : { liveTailStartRow: document.liveTailStartRow }),
470
475
  bottomAlignedTailRowCount: steeringRows.length + statusRows.length,
@@ -527,7 +532,7 @@ export class OwnedUiSessionShellRoot {
527
532
  cause: frame?.descriptor.cause ?? null,
528
533
  };
529
534
  }
530
- /** Visible non-selectable Steering, alignment, and Working rows, for rendering evidence. */
535
+ /** Visible non-persistent Steering, alignment, and Working rows, for rendering evidence. */
531
536
  viewportTransientTailRowCount() {
532
537
  return this.#viewportController.frame?.hits.transientTail.length ?? 0;
533
538
  }
@@ -1179,6 +1184,8 @@ export class OwnedUiSessionShellRoot {
1179
1184
  }
1180
1185
  setFocused(focused) {
1181
1186
  this.#inputSurface.setFocused?.(focused);
1187
+ if (focused)
1188
+ this.#onPromptSuggestionEligibilityChanged?.();
1182
1189
  }
1183
1190
  dispose() {
1184
1191
  this.#clearCopyAcknowledgement(false);
@@ -11,6 +11,7 @@ import { MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseMouseInput } from "../../ui
11
11
  import { readVisibleHyperlinks } from "../../ui/components/visible-hyperlinks.js";
12
12
  import { PINNED_PI_HIDDEN_COMMAND_NAMES, workflowCommandNames } from "../../integrations/pi/engine/workflows.js";
13
13
  import { createPiExtensionUiBridge } from "../../integrations/pi/components/shell-extension-ui.js";
14
+ import { ProgramStatusReporter } from "../../integrations/pi/components/upstream/program-status-reporter.js";
14
15
  import { createPiShellEditor } from "../../integrations/pi/components/shell-editor-autocomplete.js";
15
16
  import { SKILLS_COMMAND_NAME, findSkillByArgument, rewriteSkillsTunnelSubmission, skillPrompt, skillsFromCommands, } from "../../integrations/pi/components/skills-command.js";
16
17
  import { createPiQueuedInputStatus, createPiShellFooter, createPiShellHeader, createPiShellLoadedResources, createPiShellStatus, } from "../../integrations/pi/components/shell-footer-status.js";
@@ -79,6 +80,7 @@ export class OwnedUiSessionShell {
79
80
  #unbindTerminalSettings;
80
81
  #unbindShutdownSettings;
81
82
  #terminalProgressEnabled = false;
83
+ #programStatus;
82
84
  #showImages = true;
83
85
  #imageWidthCells = 80;
84
86
  #fullscreenExitOutput = "transcript";
@@ -199,6 +201,9 @@ export class OwnedUiSessionShell {
199
201
  this.root.reconcilePromptImageLimitNotice(text);
200
202
  },
201
203
  onPromptSuggestionAccepted: () => promptSuggestionController?.accept(),
204
+ onPromptSuggestionEligibilityChanged: () => {
205
+ queueMicrotask(() => promptSuggestionController?.restoreAvailable());
206
+ },
202
207
  onInputSurfaceChanged: () => {
203
208
  promptSuggestionController?.invalidate();
204
209
  this.#promptHistory?.synchronize();
@@ -292,6 +297,8 @@ export class OwnedUiSessionShell {
292
297
  };
293
298
  runtime = new PiTuiRuntimeAdapter(runtimeOptions);
294
299
  this.runtime = runtime;
300
+ this.#programStatus = new ProgramStatusReporter(status => { if (this.runtime.active)
301
+ this.runtime.setProgramStatus(status); }, () => this.view().status.footer?.sessionName ?? undefined);
295
302
  this.#damageTerminal = damageTerminal ?? null;
296
303
  this.#quitOutro = quitOutro;
297
304
  this.#reloadPresentation = reloadPresentation;
@@ -503,6 +510,7 @@ export class OwnedUiSessionShell {
503
510
  },
504
511
  agentDir: this.backend.agentDir,
505
512
  setInputSurface: component => this.root.setInputSurface(component, true, "opaque"),
513
+ setProgramStatusBlocked: status => this.#programStatus.setBlocked("extension-dialog", status),
506
514
  showOverlay: (component, overlayOptions) => this.runtime.showOverlay(component, overlayOptions),
507
515
  listenInput: handler => this.runtime.addInputListener(handler),
508
516
  replaceWidget: (key, component, placement) => this.root.setExtensionWidget(key, component, placement),
@@ -540,6 +548,12 @@ export class OwnedUiSessionShell {
540
548
  this.#promptSuggestions?.invalidate();
541
549
  this.root.resumeViewportFollowing();
542
550
  }
551
+ if (event.type === "assistant-message-completed" && !event.successful) {
552
+ this.#programStatus.setErrorMessage(this.view().status.diagnostics.at(-1));
553
+ }
554
+ if (event.type === "agent-run-started" || event.type === "assistant-message-completed" || event.type === "agent-run-settled") {
555
+ this.#programStatus.handleEvent(event);
556
+ }
543
557
  if (event.type === "assistant-message-completed") {
544
558
  this.root.noteCompletedAssistantMessage();
545
559
  const identity = {
@@ -571,14 +585,22 @@ export class OwnedUiSessionShell {
571
585
  if (event.type === "status" && /^(Retrying|Compacting)/.test(event.status.workingMessage ?? "")) {
572
586
  this.#promptSuggestions?.invalidate();
573
587
  }
574
- if (event.type === "agent-run-settled" && event.model !== null) {
575
- this.#promptSuggestions?.settle({
588
+ if (event.type === "agent-run-settled") {
589
+ const identity = {
576
590
  sessionId: event.sessionId,
577
591
  sessionGeneration: event.sessionGeneration,
578
592
  runSequence: event.runSequence,
579
593
  responseSequence: event.responseSequence,
580
594
  model: event.model,
581
- });
595
+ };
596
+ if (event.model === null)
597
+ this.#promptSuggestions?.skip(identity, "no-model");
598
+ else
599
+ this.#promptSuggestions?.settle({ ...identity, model: event.model }, !event.successful ? "failed-response"
600
+ : event.toolContinuation || event.stopReason === "toolUse" ? "tool-continuation"
601
+ : event.stopReason !== "stop" ? "incomplete-response"
602
+ : event.assistantMessageCount < 2 ? "early-conversation"
603
+ : this.root.promptSuggestionPrepareBlockReason());
582
604
  }
583
605
  if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
584
606
  this.#settleStoppedLifecycle();
@@ -1685,6 +1707,7 @@ export class OwnedUiSessionShell {
1685
1707
  attempt(() => this.#unsubscribe());
1686
1708
  attempt(() => this.#dialogHandle?.hide());
1687
1709
  attempt(() => this.#extensionBridge.dispose());
1710
+ attempt(() => this.#programStatus.clear());
1688
1711
  // Invariant: from here to the leave nothing but the outro paints. A throttled frame the
1689
1712
  // renderer still has queued would otherwise land during the stop-time input drain and
1690
1713
  // flash the prompt and footer, whether or not an effect plays.
@@ -1778,6 +1801,17 @@ export class OwnedUiSessionShell {
1778
1801
  if (!this.runtime.active)
1779
1802
  return;
1780
1803
  this.runtime.setTerminalProgress(this.#terminalProgressEnabled && view.lifecycle === "busy");
1804
+ if (view.lifecycle === "stopped" || view.lifecycle === "stopping") {
1805
+ this.#programStatus.clear();
1806
+ return;
1807
+ }
1808
+ this.#programStatus.setBlocked("dialog", view.dialog === null ? undefined : {
1809
+ kind: "question",
1810
+ message: view.dialog.title,
1811
+ });
1812
+ if (view.lifecycle === "failed")
1813
+ this.#programStatus.setErrorMessage(view.status.diagnostics.at(-1));
1814
+ this.#programStatus.report();
1781
1815
  }
1782
1816
  #syncView() {
1783
1817
  this.#streamPresentation.noteImmediatePresentation();
@@ -1789,6 +1823,7 @@ export class OwnedUiSessionShell {
1789
1823
  this.root.resetPendingPastes();
1790
1824
  this.#promptSuggestions?.invalidate();
1791
1825
  this.#sessionGeneration = this.backend.sessionGeneration;
1826
+ this.#programStatus.reset();
1792
1827
  // Invariant: delivery recovery invalidates callbacks, not same-session local recall or its draft.
1793
1828
  if (this.#sessionBindingGeneration !== this.backend.sessionBindingGeneration) {
1794
1829
  this.#sessionBindingGeneration = this.backend.sessionBindingGeneration;
@@ -2124,6 +2159,11 @@ export class OwnedUiSessionShell {
2124
2159
  }, request.providerName);
2125
2160
  this.#activeLoginDialog = dialog;
2126
2161
  this.root.setInputSurface(dialog);
2162
+ this.#programStatus.setBlocked("authentication", {
2163
+ kind: "auth",
2164
+ message: `Log in to ${request.providerName}`,
2165
+ });
2166
+ this.#syncTerminalProgress(this.view());
2127
2167
  this.runtime.requestRender();
2128
2168
  }
2129
2169
  #requestWorkflowInput(request) {
@@ -2180,6 +2220,8 @@ export class OwnedUiSessionShell {
2180
2220
  return;
2181
2221
  this.#activeLoginDialog = undefined;
2182
2222
  this.root.setInputSurface(null);
2223
+ this.#programStatus.setBlocked("authentication", undefined);
2224
+ this.#syncTerminalProgress(this.view());
2183
2225
  this.runtime.requestRender();
2184
2226
  }
2185
2227
  async #execute(command, draft) {