@timurproko/a1 0.2.5 → 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 (148) 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 +5 -1
  6. package/dist/app/session-shell/session-shell-root.js +13 -6
  7. package/dist/app/session-shell/session-shell.js +132 -51
  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 +4 -3
  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/reference-routes.d.ts +1 -1
  25. package/dist/features/owned-ui/reference-routes.js +1 -1
  26. package/dist/features/owned-ui/settings-app.js +21 -39
  27. package/dist/features/prompt-suggestions/diagnostics.js +4 -1
  28. package/dist/foundation/lifecycle/index.d.ts +2 -2
  29. package/dist/foundation/lifecycle/index.js +1 -1
  30. package/dist/foundation/lifecycle/session-repository-context.d.ts +19 -2
  31. package/dist/foundation/lifecycle/session-repository-context.js +434 -66
  32. package/dist/foundation/process-containment/index.d.ts +1 -0
  33. package/dist/foundation/process-containment/index.js +1 -0
  34. package/dist/foundation/process-containment/native-process-inspector.d.ts +3 -0
  35. package/dist/foundation/process-containment/native-process-inspector.js +21 -0
  36. package/dist/foundation/release/progress-palette.generated.d.ts +2 -2
  37. package/dist/foundation/release/progress-palette.generated.js +2 -2
  38. package/dist/foundation/startup/startup-descriptor.js +2 -2
  39. package/dist/integrations/pi/components/components.js +2 -2
  40. package/dist/integrations/pi/components/index.d.ts +2 -2
  41. package/dist/integrations/pi/components/index.js +1 -1
  42. package/dist/integrations/pi/components/lazy-selectors.d.ts +42 -0
  43. package/dist/integrations/pi/components/lazy-selectors.js +32 -0
  44. package/dist/integrations/pi/components/modal-frame.js +3 -4
  45. package/dist/integrations/pi/components/models-dialog.js +5 -5
  46. package/dist/integrations/pi/components/prompt-input-port.d.ts +1 -0
  47. package/dist/integrations/pi/components/resources/builtin-themes.d.ts +2 -2
  48. package/dist/integrations/pi/components/resources/builtin-themes.js +2 -2
  49. package/dist/integrations/pi/components/share-operation-dialog.js +3 -3
  50. package/dist/integrations/pi/components/shell-editor-autocomplete.js +8 -7
  51. package/dist/integrations/pi/components/shell-extension-ui.d.ts +2 -0
  52. package/dist/integrations/pi/components/shell-extension-ui.js +20 -10
  53. package/dist/integrations/pi/components/shell-hotkey-sections.js +2 -2
  54. package/dist/integrations/pi/components/shell-presenters-info.js +10 -10
  55. package/dist/integrations/pi/components/shell-presenters-transcript.js +10 -10
  56. package/dist/integrations/pi/components/shell-selectors-dialogs.js +5 -6
  57. package/dist/integrations/pi/components/shell-shared-facade.js +1 -1
  58. package/dist/integrations/pi/components/skills-dialog.js +3 -3
  59. package/dist/integrations/pi/components/submitted-prompt-adapter.js +2 -3
  60. package/dist/integrations/pi/components/theme.d.ts +9 -0
  61. package/dist/integrations/pi/components/theme.js +30 -0
  62. package/dist/integrations/pi/components/tool-result-adapter.js +6 -1
  63. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -1
  64. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +1 -1
  65. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +2 -2
  66. package/dist/integrations/pi/components/upstream/components/countdown-timer.d.ts +1 -1
  67. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +1 -1
  68. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +1 -1
  69. package/dist/integrations/pi/components/upstream/components/extension-editor.js +6 -6
  70. package/dist/integrations/pi/components/upstream/components/extension-input.js +4 -4
  71. package/dist/integrations/pi/components/upstream/components/extension-selector.js +4 -4
  72. package/dist/integrations/pi/components/upstream/components/mermaid.js +1 -1
  73. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +5 -5
  74. package/dist/integrations/pi/components/upstream/components/owned-editor.js +8 -5
  75. package/dist/integrations/pi/components/upstream/components/pi-logo.js +1 -1
  76. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
  77. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +3 -3
  78. package/dist/integrations/pi/components/upstream/components/session-footer.js +1 -1
  79. package/dist/integrations/pi/components/upstream/components/session-selector-search.js +1 -1
  80. package/dist/integrations/pi/components/upstream/components/session-selector.js +14 -20
  81. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.d.ts +3 -2
  82. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.js +6 -3
  83. package/dist/integrations/pi/components/upstream/components/status-indicator.d.ts +1 -1
  84. package/dist/integrations/pi/components/upstream/components/status-indicator.js +1 -1
  85. package/dist/integrations/pi/components/upstream/components/themed-text.d.ts +1 -1
  86. package/dist/integrations/pi/components/upstream/components/themed-text.js +1 -1
  87. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +1 -1
  88. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +2 -2
  89. package/dist/integrations/pi/components/upstream/components/tool-execution.d.ts +4 -0
  90. package/dist/integrations/pi/components/upstream/components/tool-execution.js +11 -1
  91. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +2 -2
  92. package/dist/integrations/pi/components/upstream/components/tree-selector.js +27 -18
  93. package/dist/integrations/pi/components/upstream/components/trust-selector.js +1 -1
  94. package/dist/integrations/pi/components/upstream/external-editor.js +1 -1
  95. package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +4 -4
  96. package/dist/integrations/pi/components/upstream/history/editor-core.js +11 -7
  97. package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +1 -1
  98. package/dist/integrations/pi/components/upstream/history/kill-ring.js +1 -1
  99. package/dist/integrations/pi/components/upstream/history/printable-key.js +1 -1
  100. package/dist/integrations/pi/components/upstream/history/text-helpers.js +1 -1
  101. package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +1 -1
  102. package/dist/integrations/pi/components/upstream/history/undo-stack.js +1 -1
  103. package/dist/integrations/pi/components/upstream/history/word-navigation.js +1 -1
  104. package/dist/integrations/pi/components/upstream/model-search.d.ts +1 -1
  105. package/dist/integrations/pi/components/upstream/program-status-reporter.d.ts +31 -0
  106. package/dist/integrations/pi/components/upstream/program-status-reporter.js +89 -0
  107. package/dist/integrations/pi/components/upstream/theme/system-theme.d.ts +1 -1
  108. package/dist/integrations/pi/components/upstream/theme/system-theme.js +1 -1
  109. package/dist/integrations/pi/components/upstream/theme/theme-controller.d.ts +1 -1
  110. package/dist/integrations/pi/components/upstream/theme/theme.d.ts +18 -1
  111. package/dist/integrations/pi/components/upstream/theme/theme.js +207 -10
  112. package/dist/integrations/pi/engine/pi-settings-metadata.json +1 -1
  113. package/dist/integrations/pi/engine/resources/changelog.json +4 -4
  114. package/dist/integrations/pi/engine/session-events.js +10 -2
  115. package/dist/integrations/pi/engine/transcript-projection.js +2 -1
  116. package/dist/integrations/pi/startup-public.js +306 -290
  117. package/dist/integrations/pi/startup-public.manifest.json +289 -265
  118. package/dist/integrations/pi/tui-runtime/adapter.d.ts +8 -1
  119. package/dist/integrations/pi/tui-runtime/adapter.js +31 -1
  120. package/dist/integrations/pi/tui-runtime/contracts.d.ts +3 -1
  121. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +1 -1
  122. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +1 -1
  123. package/dist/native/darwin-arm64/manifest.json +1 -1
  124. package/dist/native/linux-x64/manifest.json +1 -1
  125. package/dist/native/win32-x64/manifest.json +2 -2
  126. package/dist/native/win32-x64/process-guardian.exe +0 -0
  127. package/dist/product-identity.d.ts +2 -2
  128. package/dist/product-identity.js +2 -2
  129. package/dist/product-identity.json +3 -1
  130. package/dist/runtime-payload-inventory.json +3092 -2878
  131. package/dist/ui/components/prompt-input.d.ts +2 -0
  132. package/dist/ui/components/prompt-input.js +5 -1
  133. package/dist/ui/components/text-selection.d.ts +1 -1
  134. package/dist/ui/components/text-selection.js +3 -1
  135. package/dist/ui/components/theme.d.ts +2 -0
  136. package/dist/ui/components/transcript-viewport.d.ts +6 -4
  137. package/dist/ui/components/transcript-viewport.js +94 -16
  138. package/dist/ui/components/value-menu.d.ts +2 -0
  139. package/dist/ui/components/value-menu.js +13 -8
  140. package/dist/ui/settings/declarations.d.ts +26 -14
  141. package/dist/ui/settings/declarations.js +24 -11
  142. package/dist/ui/settings/migrations.js +16 -0
  143. package/docs/architecture/project-structure.md +4 -3
  144. package/docs/architecture/prompt-suggestions.md +14 -10
  145. package/docs/local-worktree-cleanup.md +23 -9
  146. package/docs/manual-owned-ui-checkpoint.md +1 -1
  147. package/docs/openspec-archive-automation.md +20 -19
  148. 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
+ }
@@ -5,6 +5,7 @@ import type { UiRouteHost, UiRouteInput } from "../../ui/apps/contracts.js";
5
5
  import type { TranscriptViewportFrame, TranscriptViewportFrameDescriptor } from "../../ui/components/transcript-viewport.js";
6
6
  import type { PiEngineAdapter } from "../../integrations/pi/engine/adapter.js";
7
7
  import type { PiWorkflowMessage, PiWorkflowResult } from "../../integrations/pi/engine/workflows.js";
8
+ import type { PiShellLazySelectorLoader } from "../../integrations/pi/components/lazy-selectors.js";
8
9
  import { type PiShellHotkeysPresentation } from "../../integrations/pi/components/shell-presenters-info.js";
9
10
  import { type PiShellClipboardContent, type PiShellComponentPort, type PiShellEditorPort, type PiShellExtensionRendererResolver, type PiShellHeaderOptions, type PiShellHeaderPort, type PiShellImageAssetResolver, type PiShellLoadedResourcesPort, type PiShellResourceEntry, type PiShellTranscriptComponentPort } from "../../integrations/pi/components/shell-shared-facade.js";
10
11
  import { type PiTuiInputCoordinationScheduler, type PiTuiInputSurfaceKind } from "../../integrations/pi/tui-runtime/input-presentation-coordinator.js";
@@ -116,6 +117,8 @@ export interface OwnedUiShellDiagnosticOptions {
116
117
  readonly paste?: (event: PasteEvent) => void;
117
118
  /** Test seams for isolated paste preparation: an in-process starter, or the forked helper's entry. */
118
119
  readonly pastePreparation?: Omit<PastePreparationClientOptions, "onEvent" | "spareIdleMs">;
120
+ /** Test seam for pending and failed optional selector-module preparation. */
121
+ readonly lazySelectors?: PiShellLazySelectorLoader;
119
122
  }
120
123
  /** Live prompt-image admission policy supplied only by the bare-A1 profile. */
121
124
  export interface OwnedUiShellPromptImagesOptions {
@@ -169,6 +172,7 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
169
172
  readonly onEditorChange?: (text: string) => void;
170
173
  readonly onPromptSuggestionAccepted?: (text: string) => void;
171
174
  readonly onInputSurfaceChanged?: () => void;
175
+ readonly onPromptSuggestionEligibilityChanged?: () => void;
172
176
  readonly onCopyText?: (text: string) => void;
173
177
  readonly readClipboardContent?: (signal?: AbortSignal) => Promise<PiShellClipboardContent | null>;
174
178
  readonly captureClipboardPaste?: () => PasteSource;
@@ -242,7 +246,7 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
242
246
  maxScroll: number;
243
247
  cause: "initial" | "steady" | "dock-input" | "follow-shift" | "detached" | "geometry-change" | null;
244
248
  };
245
- /** Visible non-selectable Steering, alignment, and Working rows, for rendering evidence. */
249
+ /** Visible non-persistent Steering, alignment, and Working rows, for rendering evidence. */
246
250
  viewportTransientTailRowCount(): number;
247
251
  hasActiveSelection(): boolean;
248
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);