@selesai/code 0.13.12 → 0.13.14

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 (203) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/extensions/pi-subagents/CHANGELOG.md +77 -1
  3. package/dist/extensions/pi-subagents/VISION.md +12 -1
  4. package/dist/extensions/pi-subagents/docs/agents.md +13 -6
  5. package/dist/extensions/pi-subagents/docs/configuration.md +35 -9
  6. package/dist/extensions/pi-subagents/docs/extension-api.md +1 -1
  7. package/dist/extensions/pi-subagents/docs/missions.md +1 -1
  8. package/dist/extensions/pi-subagents/docs/models.md +6 -6
  9. package/dist/extensions/pi-subagents/docs/observability.md +6 -3
  10. package/dist/extensions/pi-subagents/docs/tool-reference.md +3 -3
  11. package/dist/extensions/pi-subagents/docs/watchdog.md +92 -114
  12. package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
  13. package/dist/extensions/pi-subagents/install.mjs +1 -2
  14. package/dist/extensions/pi-subagents/package.json +1 -1
  15. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  16. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +6 -6
  17. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  18. package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  19. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +4 -4
  20. package/dist/extensions/pi-subagents/skills/pi-subagents/references/review-and-validation.md +1 -1
  21. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +41 -4
  22. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +3 -0
  23. package/dist/extensions/pi-subagents/src/agents/agents.ts +120 -124
  24. package/dist/extensions/pi-subagents/src/agents/runtime-agent-registry.ts +5 -1
  25. package/dist/extensions/pi-subagents/src/api/preflight.ts +4 -0
  26. package/dist/extensions/pi-subagents/src/api/shared-types.ts +3 -0
  27. package/dist/extensions/pi-subagents/src/extension/config.ts +20 -0
  28. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +1 -0
  29. package/dist/extensions/pi-subagents/src/extension/schemas.ts +6 -2
  30. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +1 -1
  31. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +19 -13
  32. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +26 -8
  33. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +103 -23
  34. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +6 -2
  35. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -2
  36. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +54 -3
  37. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +16 -0
  38. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +22 -2
  39. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +63 -6
  40. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +4 -1
  41. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +69 -12
  42. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +13 -0
  43. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +3 -1
  44. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +21 -8
  45. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +59 -6
  46. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +95 -18
  47. package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +11 -43
  48. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +1 -0
  49. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +1 -1
  50. package/dist/extensions/pi-subagents/src/runs/shared/lane-metadata.ts +24 -3
  51. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +4 -0
  52. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -6
  53. package/dist/extensions/pi-subagents/src/runs/shared/permissions.ts +1 -0
  54. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +33 -18
  55. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +73 -37
  56. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +33 -6
  57. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +4 -2
  58. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +20 -3
  59. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +21 -7
  60. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +1 -1
  61. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +467 -63
  62. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +3 -1
  63. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +0 -12
  64. package/dist/extensions/pi-subagents/src/shared/fork-session-cwd.ts +27 -0
  65. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +3 -0
  66. package/dist/extensions/pi-subagents/src/shared/types.ts +42 -4
  67. package/dist/extensions/pi-subagents/src/shared/utils.ts +18 -7
  68. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +1 -1
  69. package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +26 -12
  70. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +61 -2
  71. package/dist/extensions/pi-subagents/src/tui/fleet.ts +12 -7
  72. package/dist/extensions/pi-subagents/src/tui/render.ts +227 -14
  73. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +56 -34
  74. package/dist/extensions/pi-subagents/src/watchdog/diff-tool.ts +77 -0
  75. package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +5 -3
  76. package/dist/extensions/pi-subagents/src/watchdog/guidance.ts +20 -0
  77. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +16 -13
  78. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +10 -9
  79. package/dist/extensions/pi-subagents/src/watchdog/render.ts +4 -5
  80. package/dist/extensions/pi-subagents/src/watchdog/review.ts +15 -4
  81. package/dist/extensions/pi-subagents/src/watchdog/rules.ts +70 -0
  82. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +75 -92
  83. package/dist/extensions/pi-subagents/src/watchdog/scope.ts +2 -1
  84. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +48 -104
  85. package/dist/extensions/pi-subagents/src/watchdog/types.ts +18 -32
  86. package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +0 -1
  87. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +3 -2
  88. package/dist/extensions/pi-subagents/src/workflows/workflow-checklist.ts +439 -0
  89. package/dist/extensions/pi-subagents/src/workflows/workflow-preflight.ts +28 -1
  90. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +335 -21
  91. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +4 -1
  92. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +1 -1
  93. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +22 -18
  94. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +0 -1
  95. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +21 -18
  96. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +307 -22
  97. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1 -0
  98. package/dist/extensions/pi-subagents/test/support/helpers.ts +25 -0
  99. package/dist/extensions/pi-subagents/test/support/isolated-temp-root.mjs +15 -4
  100. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +17 -2
  101. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +96 -15
  102. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +53 -5
  103. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +20 -0
  104. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +221 -8
  105. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +45 -24
  106. package/dist/extensions/pi-subagents/test/unit/agent-scan-dirs.test.ts +133 -0
  107. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +28 -2
  108. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +1 -1
  109. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
  110. package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +4 -1
  111. package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +18 -4
  112. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +16 -0
  113. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +38 -0
  114. package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +2 -2
  115. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +21 -0
  116. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +19 -6
  117. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +26 -0
  118. package/dist/extensions/pi-subagents/test/unit/herdr-inspector.test.ts +22 -1
  119. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +2 -6
  120. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +66 -1
  121. package/dist/extensions/pi-subagents/test/unit/pi-args-permission-system.test.ts +1 -1
  122. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +116 -10
  123. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +136 -18
  124. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +20 -0
  125. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +22 -0
  126. package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +1 -0
  127. package/dist/extensions/pi-subagents/test/unit/project-panes-public-api.test.ts +8 -0
  128. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +127 -48
  129. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +68 -0
  130. package/dist/extensions/pi-subagents/test/unit/runtime-agent-registration.test.ts +17 -0
  131. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +88 -0
  132. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -2
  133. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +9 -9
  134. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +2 -2
  135. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +9 -3
  136. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +22 -9
  137. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +91 -8
  138. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +27 -0
  139. package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +50 -0
  140. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +0 -1
  141. package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +2 -2
  142. package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +34 -0
  143. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +1 -2
  144. package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +73 -13
  145. package/dist/extensions/pi-subagents/test/unit/watchdog-diff-tool.test.ts +79 -0
  146. package/dist/extensions/pi-subagents/test/unit/watchdog-permission-arbiter.test.ts +2 -4
  147. package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +7 -8
  148. package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +48 -3
  149. package/dist/extensions/pi-subagents/test/unit/watchdog-rules.test.ts +37 -0
  150. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +36 -118
  151. package/dist/extensions/pi-subagents/test/unit/watchdog-scope.test.ts +1 -1
  152. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +37 -14
  153. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +45 -26
  154. package/dist/extensions/pi-subagents/test/unit/workflow-checklist.test.ts +223 -0
  155. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +50 -1
  156. package/dist/extensions/pi-subagents/test/unit/workflow-preflight.test.ts +22 -0
  157. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +136 -4
  158. package/dist/extensions/pi-zentui/README.md +19 -5
  159. package/dist/extensions/pi-zentui/docs/configuration.md +37 -9
  160. package/dist/extensions/pi-zentui/extensions/zentui/config.ts +29 -0
  161. package/dist/extensions/pi-zentui/extensions/zentui/index.ts +31 -2
  162. package/dist/extensions/pi-zentui/extensions/zentui/prototype-patch-registry.ts +55 -11
  163. package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +115 -3
  164. package/dist/extensions/pi-zentui/extensions/zentui/settings-previews.ts +119 -2
  165. package/dist/extensions/pi-zentui/extensions/zentui/thinking-experimental.ts +1768 -0
  166. package/dist/extensions/pi-zentui/extensions/zentui/thinking-status.ts +69 -0
  167. package/dist/extensions/pi-zentui/extensions/zentui/thinking-steps.ts +155 -0
  168. package/dist/extensions/pi-zentui/extensions/zentui/ui.ts +2 -1
  169. package/dist/extensions/pi-zentui/extensions/zentui/user-message-osc.ts +16 -2
  170. package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +1 -1
  171. package/dist/extensions/pi-zentui/test/config-load-lifecycle.test.ts +124 -0
  172. package/dist/extensions/pi-zentui/test/config.test.ts +82 -2
  173. package/dist/extensions/pi-zentui/test/editor-viewport-indicators.test.ts +19 -4
  174. package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +58 -9
  175. package/dist/extensions/pi-zentui/test/package-contents.mjs +33 -0
  176. package/dist/extensions/pi-zentui/test/prototype-patch-registry.test.ts +131 -0
  177. package/dist/extensions/pi-zentui/test/responsive-dependencies.test.ts +4 -4
  178. package/dist/extensions/pi-zentui/test/settings-command.test.ts +289 -7
  179. package/dist/extensions/pi-zentui/test/settings-previews.test.ts +106 -0
  180. package/dist/extensions/pi-zentui/test/thinking-experimental-docs.test.ts +51 -0
  181. package/dist/extensions/pi-zentui/test/thinking-experimental-loader-diagnostics.mjs +54 -0
  182. package/dist/extensions/pi-zentui/test/thinking-experimental-missing-export.test.ts +44 -0
  183. package/dist/extensions/pi-zentui/test/thinking-experimental-rows.test.ts +297 -0
  184. package/dist/extensions/pi-zentui/test/thinking-experimental-tui.mjs +2329 -0
  185. package/dist/extensions/pi-zentui/test/thinking-experimental.test.ts +1668 -0
  186. package/dist/extensions/pi-zentui/test/thinking-status.test.ts +85 -0
  187. package/dist/extensions/pi-zentui/test/thinking-steps.test.ts +122 -0
  188. package/dist/extensions/pi-zentui/test/user-message-osc.test.ts +58 -0
  189. package/dist/skills/brandkit/SKILL.md +0 -2
  190. package/dist/skills/{industrial-brutalist-ui → brutalist}/SKILL.md +1 -3
  191. package/dist/skills/gpt-taste/SKILL.md +0 -2
  192. package/dist/skills/image-to-code/SKILL.md +0 -2
  193. package/dist/skills/imagegen-frontend-mobile/SKILL.md +0 -2
  194. package/dist/skills/imagegen-frontend-web/SKILL.md +0 -2
  195. package/dist/skills/{minimalist-ui → minimalist}/SKILL.md +1 -3
  196. package/dist/skills/{full-output-enforcement → output}/SKILL.md +1 -3
  197. package/dist/skills/{redesign-existing-projects → redesign}/SKILL.md +1 -3
  198. package/dist/skills/{high-end-visual-design → soft}/SKILL.md +1 -3
  199. package/dist/skills/stitch/DESIGN.md +121 -0
  200. package/dist/skills/{stitch-design-taste → stitch}/SKILL.md +1 -3
  201. package/dist/skills/{design-taste-frontend → taste}/SKILL.md +1 -3
  202. package/dist/skills/{design-taste-frontend-v1 → taste-v1}/SKILL.md +2 -4
  203. package/package.json +1 -1
@@ -0,0 +1,1768 @@
1
+ import type { AssistantMessage } from "@earendil-works/pi-ai";
2
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import * as PiCodingAgent from "@earendil-works/pi-coding-agent";
4
+ import * as PiTui from "@earendil-works/pi-tui";
5
+ import {
6
+ type Component,
7
+ getKeybindings,
8
+ Markdown,
9
+ Spacer,
10
+ Text,
11
+ truncateToWidth,
12
+ visibleWidth,
13
+ } from "@earendil-works/pi-tui";
14
+ import type { ThinkingStepsComponentConfig, ThinkingStepsMode } from "./config";
15
+ import {
16
+ installPrototypePatch,
17
+ isPrototypePatchCurrent,
18
+ type PrototypePatchRegistration,
19
+ } from "./prototype-patch-registry";
20
+ import { formatThinkingStatus, thinkingStatusLabels } from "./thinking-status";
21
+ import { parseThinkingSteps, type ThinkingStep } from "./thinking-steps";
22
+
23
+ /*
24
+ * The rendered-row folding and lifecycle below are adapted from
25
+ * @99percentpeople/pi-thinking-fold 0.1.9 at commit 555160c.
26
+ *
27
+ * MIT License
28
+ *
29
+ * Copyright (c) 2026 Zach Yuen
30
+ *
31
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
32
+ * of this software and associated documentation files (the "Software"), to deal
33
+ * in the Software without restriction, including without limitation the rights
34
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
35
+ * copies of the Software, and to permit persons to whom the Software is
36
+ * furnished to do so, subject to the following conditions:
37
+ *
38
+ * The above copyright notice and this permission notice shall be included in all
39
+ * copies or substantial portions of the Software.
40
+ *
41
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
42
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
43
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
44
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
45
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
46
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
47
+ * SOFTWARE.
48
+ */
49
+
50
+ export const THINKING_EXPERIMENTAL_TAIL_ROWS = 5;
51
+ /** Bounds expand/refold ownership to the most recent retained session components. */
52
+ export const THINKING_EXPERIMENTAL_MAX_TRACKED_COMPONENTS = 256;
53
+ const PATCH_ADAPTER = "thinking-experimental-update-content" as const;
54
+ const MAX_TIMINGS = 256;
55
+
56
+ type PatchableAssistant = {
57
+ contentContainer?: { children?: Component[] };
58
+ hideThinkingBlock?: boolean;
59
+ isStreaming?: boolean;
60
+ };
61
+
62
+ type PrivateAssistantConstructor = {
63
+ prototype: PatchableAssistant & {
64
+ render?: unknown;
65
+ updateContent?: (this: unknown, ...args: unknown[]) => unknown;
66
+ };
67
+ };
68
+
69
+ // This renderer is private and has disappeared from published entrypoints before.
70
+ // Resolve it as optional data so a missing/renamed export cannot reject ESM linking.
71
+ const AssistantMessageComponent = (
72
+ PiCodingAgent as unknown as { AssistantMessageComponent?: PrivateAssistantConstructor }
73
+ ).AssistantMessageComponent;
74
+ const keyText = PiCodingAgent.keyText;
75
+
76
+ function requestTerminalRender(): void {
77
+ try {
78
+ // Pi has no public transcript-redraw method. Its TUI owns resize as a full redraw.
79
+ process.stdout.emit("resize");
80
+ } catch {
81
+ // The next native host update will render the already-reconciled children.
82
+ }
83
+ }
84
+
85
+ type HiddenState = { own: boolean; value: boolean | undefined };
86
+
87
+ type TrackedState = {
88
+ message: AssistantMessage;
89
+ args: unknown[];
90
+ predecessor: (this: unknown, ...args: unknown[]) => unknown;
91
+ incomplete: boolean;
92
+ nativeHidden: HiddenState;
93
+ folded: boolean;
94
+ };
95
+
96
+ type Timing = { startedAt?: number; completedAt?: number };
97
+
98
+ type KeybindingsShape = {
99
+ getDefinition(action: string): unknown;
100
+ getKeys?(action: string): string[];
101
+ matches(data: string, action: string): boolean;
102
+ };
103
+
104
+ type ToggleInput = Readonly<{
105
+ label: string;
106
+ keys: readonly string[];
107
+ matches: (data: string) => boolean;
108
+ rawFallback: boolean;
109
+ }>;
110
+
111
+ export type ThinkingExperimentalDiagnostics = Readonly<{
112
+ trackedComponents: number;
113
+ activeComponents: number;
114
+ lastTimerWork: number;
115
+ }>;
116
+
117
+ export type ThinkingExperimentalState = Readonly<{
118
+ /** Backward-compatible alias for whole renderer availability. */
119
+ available: boolean;
120
+ rendererAvailable: boolean;
121
+ streamingAvailable: boolean;
122
+ streamingPoisoned: boolean;
123
+ active: boolean;
124
+ activeMode?: ThinkingStepsMode;
125
+ startup: Readonly<ThinkingStepsComponentConfig>;
126
+ displaced: boolean;
127
+ restartRequired: boolean;
128
+ reason?: string;
129
+ }>;
130
+
131
+ export type ThinkingExperimentalApplyResult = Readonly<{
132
+ applied: boolean;
133
+ reason?: string;
134
+ }>;
135
+
136
+ function messageTimestamp(message: AssistantMessage): number | undefined {
137
+ return Number.isFinite(message.timestamp) ? message.timestamp : undefined;
138
+ }
139
+
140
+ function hasThinking(message: AssistantMessage): boolean {
141
+ return message.content.some(
142
+ (content) => content.type === "thinking" && Boolean(content.thinking.trim()),
143
+ );
144
+ }
145
+
146
+ function formatSeconds(milliseconds: number): string {
147
+ return `${(Math.max(0, milliseconds) / 1000).toFixed(1)}s`;
148
+ }
149
+
150
+ type NativeMarkdownShape = {
151
+ text: string;
152
+ paddingX: number;
153
+ paddingY: number;
154
+ theme: ConstructorParameters<typeof Markdown>[3];
155
+ defaultTextStyle?: ConstructorParameters<typeof Markdown>[4];
156
+ options?: ConstructorParameters<typeof Markdown>[5];
157
+ };
158
+
159
+ type AccentTheme = { fg(color: "accent", text: string): string };
160
+ type StructuralThinkingMode = Exclude<ThinkingStepsMode, "streaming">;
161
+
162
+ /*
163
+ * The visual title/connector language below is adapted from pi-thinking-steps d0a59a4.
164
+ *
165
+ * MIT License
166
+ *
167
+ * Copyright (c) 2026 Marc Mironescu / FluxGear
168
+ *
169
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
170
+ * of this software and associated documentation files (the "Software"), to deal
171
+ * in the Software without restriction, including without limitation the rights
172
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
173
+ * copies of the Software, and to permit persons to whom the Software is
174
+ * furnished to do so, subject to the following conditions:
175
+ *
176
+ * The above copyright notice and this permission notice shall be included in all
177
+ * copies or substantial portions of the Software.
178
+ *
179
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
180
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
181
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
182
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
183
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
184
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
185
+ * SOFTWARE.
186
+ */
187
+
188
+ function unsafeRenderedRow(source: string, row: string): boolean {
189
+ return (
190
+ /!\[[^\]]*\]\(/.test(source) ||
191
+ row.includes("\x1b_G") ||
192
+ /\x1b\]1337;File=/.test(row) ||
193
+ visibleWidth(row) === 0
194
+ );
195
+ }
196
+
197
+ function croppedMarkdownRow(markdown: Markdown, source: string, width: number): string | undefined {
198
+ if (width <= 0) return undefined;
199
+ const rows = markdown.render(width);
200
+ const first = rows[0];
201
+ if (!first || unsafeRenderedRow(source, first)) return undefined;
202
+ if (rows.length > 1) {
203
+ if (width === 1) return "…";
204
+ return `${truncateToWidth(first, width - 1, "")}…`;
205
+ }
206
+ return visibleWidth(first) <= width ? first : truncateToWidth(first, width, "…");
207
+ }
208
+
209
+ /** A width-bounded private replacement for one native contiguous thinking run. */
210
+ export class ThinkingStepsRows implements Component {
211
+ private readonly title: Markdown;
212
+ private readonly labels: Array<{ source: string; markdown: Markdown }>;
213
+
214
+ constructor(
215
+ private readonly native: Component,
216
+ private readonly shape: NativeMarkdownShape,
217
+ steps: readonly ThinkingStep[],
218
+ private readonly mode: StructuralThinkingMode,
219
+ private readonly incomplete: boolean,
220
+ private readonly getTheme: () => AccentTheme,
221
+ ) {
222
+ const selected = mode === "rail" ? steps : steps.slice(-5);
223
+ this.title = new Markdown(
224
+ "**Thinking**",
225
+ 0,
226
+ 0,
227
+ shape.theme,
228
+ shape.defaultTextStyle,
229
+ shape.options,
230
+ );
231
+ this.labels = selected.map((step) => ({
232
+ source: step.label,
233
+ markdown: new Markdown(step.label, 0, 0, shape.theme, shape.defaultTextStyle, shape.options),
234
+ }));
235
+ }
236
+
237
+ render(width: number): string[] {
238
+ try {
239
+ const outer = this.shape.paddingX;
240
+ const innerWidth = Math.floor(width) - outer * 2;
241
+ const titleConnector = this.mode === "rail" ? "│ " : "┆ ";
242
+ const titleBudget = innerWidth - visibleWidth(titleConnector);
243
+ const title = croppedMarkdownRow(this.title, "**Thinking**", titleBudget);
244
+ if (!title) return this.native.render(width);
245
+ const renderedLabels: Array<{ connector: string; label: string }> = [];
246
+ for (const [index, value] of this.labels.entries()) {
247
+ const final = index === this.labels.length - 1;
248
+ const connector =
249
+ this.mode === "rail"
250
+ ? final && this.incomplete
251
+ ? "│ • "
252
+ : "│ "
253
+ : final
254
+ ? this.incomplete
255
+ ? "└─ • "
256
+ : "└─ · "
257
+ : "├─ · ";
258
+ const budget = innerWidth - visibleWidth(connector);
259
+ const label = croppedMarkdownRow(value.markdown, value.source, budget);
260
+ if (!label) return this.native.render(width);
261
+ renderedLabels.push({ connector, label });
262
+ }
263
+ const theme = this.getTheme();
264
+ const left = " ".repeat(outer);
265
+ const right = " ".repeat(outer);
266
+ const rows = [
267
+ `${left}${theme.fg("accent", titleConnector)}${title}${right}`,
268
+ ...renderedLabels.map(
269
+ ({ connector, label }) => `${left}${theme.fg("accent", connector)}${label}${right}`,
270
+ ),
271
+ ];
272
+ return rows.every((row) => visibleWidth(row) <= width) ? rows : this.native.render(width);
273
+ } catch {
274
+ return this.native.render(width);
275
+ }
276
+ }
277
+
278
+ invalidate(): void {
279
+ this.native.invalidate();
280
+ this.title.invalidate();
281
+ for (const label of this.labels) label.markdown.invalidate();
282
+ }
283
+ }
284
+
285
+ /** Pure factory shared by settings previews and the private assistant wrapper. */
286
+ export function createThinkingStepsRows(
287
+ native: Component,
288
+ shape: NativeMarkdownShape,
289
+ steps: readonly ThinkingStep[],
290
+ mode: StructuralThinkingMode,
291
+ incomplete: boolean,
292
+ getTheme: () => AccentTheme,
293
+ ): ThinkingStepsRows {
294
+ return new ThinkingStepsRows(native, shape, steps, mode, incomplete, getTheme);
295
+ }
296
+
297
+ class FoldContext {
298
+ readonly sections: FoldedThinkingSection[] = [];
299
+ private preparedWidth: number | undefined;
300
+ private allocations = new Map<FoldedThinkingSection, string[]>();
301
+ private hidden = false;
302
+
303
+ constructor(
304
+ readonly incomplete: boolean,
305
+ readonly header: (hidden: boolean) => string,
306
+ private readonly template: NativeMarkdownShape,
307
+ ) {}
308
+
309
+ add(section: FoldedThinkingSection): void {
310
+ this.sections.push(section);
311
+ }
312
+
313
+ prepare(width: number): void {
314
+ if (this.preparedWidth === width) return;
315
+ const rendered = this.sections.map((section) => section.renderNative(width));
316
+ const total = rendered.reduce((count, rows) => count + rows.length, 0);
317
+ this.hidden = !this.incomplete || total > THINKING_EXPERIMENTAL_TAIL_ROWS;
318
+ let remaining = this.incomplete ? Math.min(total, THINKING_EXPERIMENTAL_TAIL_ROWS) : 0;
319
+ this.allocations = new Map();
320
+ for (let index = rendered.length - 1; index >= 0; index -= 1) {
321
+ const rows = rendered[index] ?? [];
322
+ const take = Math.min(remaining, rows.length);
323
+ this.allocations.set(
324
+ this.sections[index] as FoldedThinkingSection,
325
+ take > 0 ? rows.slice(-take) : [],
326
+ );
327
+ remaining -= take;
328
+ }
329
+ this.preparedWidth = width;
330
+ }
331
+
332
+ rowsFor(section: FoldedThinkingSection, width: number): string[] {
333
+ this.prepare(width);
334
+ return this.allocations.get(section) ?? [];
335
+ }
336
+
337
+ headerRows(width: number): string[] {
338
+ this.prepare(width);
339
+ return new Markdown(
340
+ this.header(this.hidden),
341
+ this.template.paddingX,
342
+ this.template.paddingY,
343
+ this.template.theme,
344
+ this.template.defaultTextStyle,
345
+ this.template.options,
346
+ ).render(width);
347
+ }
348
+
349
+ invalidate(): void {
350
+ this.preparedWidth = undefined;
351
+ this.allocations.clear();
352
+ }
353
+ }
354
+
355
+ class FoldedThinkingSection implements Component {
356
+ private nativeRows: string[] = [];
357
+ private preparedWidth: number | undefined;
358
+
359
+ constructor(
360
+ private readonly native: Markdown,
361
+ private readonly context: FoldContext,
362
+ private readonly first: boolean,
363
+ ) {
364
+ context.add(this);
365
+ }
366
+
367
+ renderNative(width: number): string[] {
368
+ if (this.preparedWidth !== width) {
369
+ this.nativeRows = this.native.render(width);
370
+ this.preparedWidth = width;
371
+ }
372
+ return this.nativeRows;
373
+ }
374
+
375
+ render(width: number): string[] {
376
+ const rows = this.context.rowsFor(this, width);
377
+ return this.first ? [...this.context.headerRows(width), ...rows] : rows;
378
+ }
379
+
380
+ invalidate(): void {
381
+ this.native.invalidate();
382
+ this.preparedWidth = undefined;
383
+ this.context.invalidate();
384
+ }
385
+ }
386
+
387
+ function markdownShape(component: Markdown): NativeMarkdownShape | undefined {
388
+ const value = component as unknown as Record<string, unknown>;
389
+ if (
390
+ typeof value.text !== "string" ||
391
+ typeof value.paddingX !== "number" ||
392
+ typeof value.paddingY !== "number" ||
393
+ !value.theme
394
+ )
395
+ return undefined;
396
+ return value as unknown as NativeMarkdownShape;
397
+ }
398
+
399
+ type NativeChildDescriptor =
400
+ | Readonly<{ kind: "markdown"; source: string; thinkingRun?: number }>
401
+ | Readonly<{ kind: "spacer" }>
402
+ | Readonly<{ kind: "text"; markers: readonly string[] }>;
403
+
404
+ type ThinkingMarkdownMatch = Readonly<{
405
+ index: number;
406
+ markdown: Markdown;
407
+ shape: NativeMarkdownShape;
408
+ run: number;
409
+ }>;
410
+
411
+ type ThinkingMarkdownLayout = Readonly<{
412
+ matches: readonly ThinkingMarkdownMatch[];
413
+ descriptors: readonly NativeChildDescriptor[];
414
+ }>;
415
+
416
+ class NativeThinkingRun implements Component {
417
+ constructor(private readonly children: readonly Component[]) {}
418
+
419
+ render(width: number): string[] {
420
+ return this.children.flatMap((child) => child.render(width));
421
+ }
422
+
423
+ invalidate(): void {
424
+ for (const child of this.children) child.invalidate();
425
+ }
426
+ }
427
+
428
+ const legacyLengthMarkers = [
429
+ "Error: Model stopped because it reached the maximum output token limit. The response may be incomplete.",
430
+ "Response was truncated before completion.",
431
+ ] as const;
432
+
433
+ function hasVisibleContentAfter(message: AssistantMessage, start: number): boolean {
434
+ return message.content
435
+ .slice(start)
436
+ .some(
437
+ (content) =>
438
+ (content.type === "text" && Boolean(content.text.trim())) ||
439
+ (content.type === "thinking" && Boolean(content.thinking.trim())),
440
+ );
441
+ }
442
+
443
+ function trailingDescriptors(message: AssistantMessage): NativeChildDescriptor[] {
444
+ if (message.stopReason === "length") {
445
+ return [{ kind: "spacer" }, { kind: "text", markers: legacyLengthMarkers }];
446
+ }
447
+ if (message.content.some((content) => content.type === "toolCall")) return [];
448
+ if (message.stopReason === "aborted") {
449
+ const marker =
450
+ message.errorMessage && message.errorMessage !== "Request was aborted"
451
+ ? message.errorMessage
452
+ : "Operation aborted";
453
+ return [{ kind: "spacer" }, { kind: "text", markers: [marker] }];
454
+ }
455
+ if (message.stopReason === "error") {
456
+ return [
457
+ { kind: "spacer" },
458
+ { kind: "text", markers: [`Error: ${message.errorMessage || "Unknown error"}`] },
459
+ ];
460
+ }
461
+ return [];
462
+ }
463
+
464
+ /**
465
+ * Mirrors the visible child layouts shipped by the supported Pi hosts. Pi 0.83+
466
+ * coalesces a contiguous thinking run; Pi 0.80.5 emitted one section per block.
467
+ * Tool calls emit no child here, but still terminate a thinking run and suppress
468
+ * trailing errors, so iteration over the original ordered content is required.
469
+ */
470
+ function nativeChildLayouts(message: AssistantMessage): NativeChildDescriptor[][] {
471
+ const build = (coalesceThinking: boolean): NativeChildDescriptor[] => {
472
+ const descriptors: NativeChildDescriptor[] = [];
473
+ let nextRun = 0;
474
+ let contiguousRun: number | undefined;
475
+ if (hasVisibleContentAfter(message, 0)) descriptors.push({ kind: "spacer" });
476
+ for (let index = 0; index < message.content.length; index += 1) {
477
+ const content = message.content[index];
478
+ if (content?.type === "text") {
479
+ contiguousRun = undefined;
480
+ const source = content.text.trim();
481
+ if (source) descriptors.push({ kind: "markdown", source });
482
+ continue;
483
+ }
484
+ if (content?.type !== "thinking") {
485
+ contiguousRun = undefined;
486
+ continue;
487
+ }
488
+ const run = contiguousRun ?? nextRun++;
489
+ contiguousRun = run;
490
+ if (!coalesceThinking) {
491
+ const source = content.thinking.trim();
492
+ if (source) {
493
+ descriptors.push({ kind: "markdown", source, thinkingRun: run });
494
+ if (hasVisibleContentAfter(message, index + 1)) descriptors.push({ kind: "spacer" });
495
+ }
496
+ continue;
497
+ }
498
+ const blocks: string[] = [];
499
+ for (; index < message.content.length; index += 1) {
500
+ const next = message.content[index];
501
+ if (next?.type !== "thinking") break;
502
+ const source = next.thinking.trim();
503
+ if (source) blocks.push(source);
504
+ }
505
+ index -= 1;
506
+ if (!blocks.length) continue;
507
+ descriptors.push({ kind: "markdown", source: blocks.join("\n\n"), thinkingRun: run });
508
+ if (hasVisibleContentAfter(message, index + 1)) descriptors.push({ kind: "spacer" });
509
+ }
510
+ return [...descriptors, ...trailingDescriptors(message)];
511
+ };
512
+ const coalesced = build(true);
513
+ const legacy = build(false);
514
+ return JSON.stringify(coalesced) === JSON.stringify(legacy) ? [coalesced] : [coalesced, legacy];
515
+ }
516
+
517
+ function plainTextMarker(text: string): string {
518
+ return text.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "").replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "");
519
+ }
520
+
521
+ function exactConstructor(component: Component, expected: { prototype: object }): boolean {
522
+ return Object.getPrototypeOf(component) === expected.prototype;
523
+ }
524
+
525
+ function matchNativeLayout(
526
+ children: Component[],
527
+ descriptors: NativeChildDescriptor[],
528
+ ): ThinkingMarkdownLayout | undefined {
529
+ if (children.length !== descriptors.length) return undefined;
530
+ const thinking: ThinkingMarkdownMatch[] = [];
531
+ for (let index = 0; index < descriptors.length; index += 1) {
532
+ const child = children[index];
533
+ const descriptor = descriptors[index];
534
+ if (!child || !descriptor) return undefined;
535
+ if (descriptor.kind === "spacer") {
536
+ if (!exactConstructor(child, Spacer) || (child as unknown as { lines?: unknown }).lines !== 1)
537
+ return undefined;
538
+ continue;
539
+ }
540
+ if (descriptor.kind === "text") {
541
+ if (!exactConstructor(child, Text)) return undefined;
542
+ const text = (child as unknown as { text?: unknown }).text;
543
+ if (typeof text !== "string") return undefined;
544
+ const marker = plainTextMarker(text);
545
+ if (!descriptor.markers.includes(marker)) return undefined;
546
+ continue;
547
+ }
548
+ if (!exactConstructor(child, Markdown)) return undefined;
549
+ const shape = markdownShape(child as Markdown);
550
+ if (!shape || shape.text !== descriptor.source) return undefined;
551
+ if (descriptor.thinkingRun !== undefined) {
552
+ thinking.push({
553
+ index,
554
+ markdown: child as Markdown,
555
+ shape,
556
+ run: descriptor.thinkingRun,
557
+ });
558
+ }
559
+ }
560
+ return { matches: thinking, descriptors };
561
+ }
562
+
563
+ function thinkingMarkdownLayout(
564
+ children: Component[],
565
+ message: AssistantMessage,
566
+ ): ThinkingMarkdownLayout | undefined {
567
+ for (const descriptors of nativeChildLayouts(message)) {
568
+ const layout = matchNativeLayout(children, descriptors);
569
+ if (layout?.matches.length) return layout;
570
+ }
571
+ return undefined;
572
+ }
573
+
574
+ function writableOwnChildren(
575
+ instance: PatchableAssistant,
576
+ ): { container: { children?: Component[] }; children: Component[] } | undefined {
577
+ const container = instance.contentContainer;
578
+ if (!container) return undefined;
579
+ const descriptor = Object.getOwnPropertyDescriptor(container, "children");
580
+ if (
581
+ !descriptor ||
582
+ !("value" in descriptor) ||
583
+ descriptor.writable !== true ||
584
+ !Array.isArray(descriptor.value)
585
+ )
586
+ return undefined;
587
+ return { container, children: descriptor.value };
588
+ }
589
+
590
+ export function hasThinkingExperimentalMarkdownIdentity(
591
+ instance: object,
592
+ message: AssistantMessage,
593
+ ): boolean {
594
+ const owned = writableOwnChildren(instance as PatchableAssistant);
595
+ return Boolean(owned && thinkingMarkdownLayout(owned.children, message) !== undefined);
596
+ }
597
+
598
+ function activeThinkingRun(message: AssistantMessage, incomplete: boolean): number | undefined {
599
+ if (!incomplete || message.content.at(-1)?.type !== "thinking") return undefined;
600
+ let nextRun = 0;
601
+ let contiguousRun: number | undefined;
602
+ for (const content of message.content) {
603
+ if (content.type !== "thinking") {
604
+ contiguousRun = undefined;
605
+ continue;
606
+ }
607
+ contiguousRun ??= nextRun++;
608
+ }
609
+ return contiguousRun;
610
+ }
611
+
612
+ function replaceThinkingChildren(
613
+ instance: PatchableAssistant,
614
+ message: AssistantMessage,
615
+ mode: ThinkingStepsMode,
616
+ incomplete: boolean,
617
+ header: (hidden: boolean) => string,
618
+ getTheme: () => AccentTheme,
619
+ ): boolean {
620
+ const owned = writableOwnChildren(instance);
621
+ if (!owned) return false;
622
+ const { container, children } = owned;
623
+ const layout = thinkingMarkdownLayout(children, message);
624
+ if (!layout) return false;
625
+ const replacements = new Map<number, Component>();
626
+ const removals = new Set<number>();
627
+ if (mode === "streaming") {
628
+ const template = layout.matches[0]?.shape;
629
+ if (!template) return false;
630
+ const context = new FoldContext(incomplete, header, template);
631
+ for (const [position, match] of layout.matches.entries()) {
632
+ replacements.set(
633
+ match.index,
634
+ new FoldedThinkingSection(match.markdown, context, position === 0),
635
+ );
636
+ }
637
+ } else {
638
+ const runs = new Map<number, ThinkingMarkdownMatch[]>();
639
+ for (const match of layout.matches) {
640
+ const matches = runs.get(match.run) ?? [];
641
+ matches.push(match);
642
+ runs.set(match.run, matches);
643
+ }
644
+ const activeRun = activeThinkingRun(message, incomplete);
645
+ for (const [run, matches] of runs) {
646
+ const first = matches[0];
647
+ const last = matches.at(-1);
648
+ if (!first || !last) continue;
649
+ const source = matches.map((match) => match.shape.text).join("\n\n");
650
+ const steps = parseThinkingSteps(source);
651
+ if (!steps?.length) continue;
652
+ const nativeChildren = children.slice(first.index, last.index + 1);
653
+ const native =
654
+ nativeChildren.length === 1 ? first.markdown : new NativeThinkingRun(nativeChildren);
655
+ replacements.set(
656
+ first.index,
657
+ createThinkingStepsRows(
658
+ native,
659
+ { ...first.shape, text: source },
660
+ steps,
661
+ mode,
662
+ run === activeRun,
663
+ getTheme,
664
+ ),
665
+ );
666
+ for (let index = first.index + 1; index <= last.index; index += 1) removals.add(index);
667
+ }
668
+ }
669
+ if (replacements.size === 0) return true;
670
+ const nextChildren = children.flatMap((child, index) => {
671
+ const replacement = replacements.get(index);
672
+ if (replacement) return [replacement];
673
+ return removals.has(index) ? [] : [child];
674
+ });
675
+ container.children = nextChildren;
676
+ return true;
677
+ }
678
+
679
+ function eventType(event: unknown): string | undefined {
680
+ if (!event || typeof event !== "object") return undefined;
681
+ const nested = (event as { assistantMessageEvent?: unknown }).assistantMessageEvent;
682
+ if (!nested || typeof nested !== "object") return undefined;
683
+ const type = (nested as { type?: unknown }).type;
684
+ return typeof type === "string" ? type : undefined;
685
+ }
686
+
687
+ function assistantMessage(event: unknown): AssistantMessage | undefined {
688
+ if (!event || typeof event !== "object") return undefined;
689
+ const message = (event as { message?: unknown }).message;
690
+ if (
691
+ !message ||
692
+ typeof message !== "object" ||
693
+ (message as { role?: unknown }).role !== "assistant"
694
+ )
695
+ return undefined;
696
+ return message as AssistantMessage;
697
+ }
698
+
699
+ function beginsThinkingPhase(type: string | undefined): boolean {
700
+ return type === "thinking_start" || type === "thinking_delta";
701
+ }
702
+
703
+ function endsThinkingPhase(type: string | undefined): boolean {
704
+ return (
705
+ type === "thinking_end" ||
706
+ type === "text_start" ||
707
+ type === "text_delta" ||
708
+ type === "toolcall_start" ||
709
+ type === "toolcall_delta"
710
+ );
711
+ }
712
+
713
+ function hiddenState(instance: PatchableAssistant): HiddenState {
714
+ return {
715
+ own: Object.hasOwn(instance, "hideThinkingBlock"),
716
+ value: typeof instance.hideThinkingBlock === "boolean" ? instance.hideThinkingBlock : undefined,
717
+ };
718
+ }
719
+
720
+ function setHiddenState(instance: PatchableAssistant, state: HiddenState): void {
721
+ if (state.own) instance.hideThinkingBlock = state.value;
722
+ else delete instance.hideThinkingBlock;
723
+ }
724
+
725
+ function renderWithHiddenState(
726
+ instance: PatchableAssistant,
727
+ predecessor: (this: unknown, ...args: unknown[]) => unknown,
728
+ args: unknown[],
729
+ hidden: HiddenState,
730
+ ): unknown {
731
+ const current = hiddenState(instance);
732
+ try {
733
+ instance.hideThinkingBlock = hidden.value;
734
+ return Reflect.apply(predecessor, instance, args);
735
+ } finally {
736
+ setHiddenState(instance, current);
737
+ }
738
+ }
739
+
740
+ function isKeyRelease(data: string): boolean {
741
+ const publicCheck = (PiTui as unknown as { isKeyRelease?: (value: string) => boolean })
742
+ .isKeyRelease;
743
+ if (typeof publicCheck === "function") {
744
+ try {
745
+ if (publicCheck(data)) return true;
746
+ } catch {
747
+ // Continue with the protocol's structural event marker.
748
+ }
749
+ }
750
+ return /^\x1b\[[0-9]+(?:;[0-9]+)?(?::3)?u$/.test(data) && /:3u$/.test(data);
751
+ }
752
+
753
+ /** Owns the opt-in private renderer wrapper for one Zentui extension/session lifecycle. */
754
+ export class ThinkingExperimentalController {
755
+ private installed = false;
756
+ private active = false;
757
+ private displaced = false;
758
+ private restartRequired = false;
759
+ private unavailableReason: string | undefined;
760
+ private streamingUnavailableReason: string | undefined;
761
+ private streamingListenerPoisoned = false;
762
+ private disposed = false;
763
+ private resourceToken = 0;
764
+ private timerToken = 0;
765
+ private patchRegistration: PrototypePatchRegistration | undefined;
766
+ private context: ExtensionContext | undefined;
767
+ private stopInput: (() => void) | undefined;
768
+ private interval: ReturnType<typeof setInterval> | undefined;
769
+ private expanded = false;
770
+ private toggleInput: ToggleInput | undefined;
771
+ private tracked = new Set<WeakRef<object>>();
772
+ private activeComponents = new Set<WeakRef<object>>();
773
+ private references = new WeakMap<object, WeakRef<object>>();
774
+ private states = new WeakMap<object, TrackedState>();
775
+ private timings = new Map<number, Timing>();
776
+ private lastTimerWork = 0;
777
+ private currentMessage: AssistantMessage | undefined;
778
+ private rerendering = false;
779
+ private shapeFailureDuringRerender: string | undefined;
780
+ private startup: ThinkingStepsComponentConfig = { enabled: false, mode: "tree" };
781
+ private activeMode: ThinkingStepsMode | undefined;
782
+
783
+ constructor(
784
+ private readonly getConfig: () => ThinkingStepsComponentConfig,
785
+ private readonly requestRender: () => void = requestTerminalRender,
786
+ private readonly now: () => number = Date.now,
787
+ private readonly getHostKeybindings:
788
+ | (() => KeybindingsShape | undefined)
789
+ | undefined = typeof getKeybindings === "function" ? getKeybindings : undefined,
790
+ private readonly getHostKeyText: ((action: string) => string) | undefined = typeof keyText ===
791
+ "function"
792
+ ? (action) => keyText(action as never)
793
+ : undefined,
794
+ ) {
795
+ const prototype = AssistantMessageComponent?.prototype;
796
+ if (
797
+ !prototype ||
798
+ typeof (prototype as { updateContent?: unknown }).updateContent !== "function" ||
799
+ typeof (prototype as { render?: unknown }).render !== "function"
800
+ ) {
801
+ this.unavailableReason = "Pi's private AssistantMessageComponent renderer is unavailable";
802
+ }
803
+ }
804
+
805
+ get state(): ThinkingExperimentalState {
806
+ this.checkDisplacement();
807
+ const rendererAvailable = !this.unavailableReason && !this.displaced;
808
+ const reason = this.displaced
809
+ ? "Private renderer patch ownership was displaced"
810
+ : (this.unavailableReason ?? this.streamingUnavailableReason);
811
+ return Object.freeze({
812
+ available: rendererAvailable,
813
+ rendererAvailable,
814
+ streamingAvailable: !this.streamingUnavailableReason,
815
+ streamingPoisoned: this.streamingListenerPoisoned,
816
+ active: this.active && !this.displaced,
817
+ ...(this.activeMode ? { activeMode: this.activeMode } : {}),
818
+ startup: Object.freeze({ ...this.startup }),
819
+ displaced: this.displaced,
820
+ restartRequired: this.restartRequired,
821
+ ...(reason ? { reason } : {}),
822
+ });
823
+ }
824
+
825
+ private failureResult(): ThinkingExperimentalApplyResult {
826
+ return {
827
+ applied: false,
828
+ reason: formatThinkingStatus(thinkingStatusLabels(this.getConfig(), this.state)),
829
+ };
830
+ }
831
+
832
+ get diagnostics(): ThinkingExperimentalDiagnostics {
833
+ return Object.freeze({
834
+ trackedComponents: this.trackedEntries().length,
835
+ activeComponents: this.activeEntries().length,
836
+ lastTimerWork: this.lastTimerWork,
837
+ });
838
+ }
839
+
840
+ /** Activates the private renderer during session startup, before transcript restoration. */
841
+ startSession(ctx: ExtensionContext): ThinkingExperimentalApplyResult {
842
+ this.disposed = false;
843
+ this.resourceToken += 1;
844
+ this.streamingUnavailableReason = undefined;
845
+ this.streamingListenerPoisoned = false;
846
+ if (ctx.mode === "tui" && ctx.hasUI) this.context = ctx;
847
+ this.startup = { ...this.getConfig() };
848
+ this.restartRequired = false;
849
+ this.activeMode = undefined;
850
+ if (!this.startup.enabled) return { applied: true };
851
+ if (!this.context) {
852
+ this.unavailableReason = "Thinking (Experimental) requires a TUI session";
853
+ this.restartRequired = true;
854
+ return this.failureResult();
855
+ }
856
+ if (this.unavailableReason) {
857
+ this.restartRequired = true;
858
+ return this.failureResult();
859
+ }
860
+ if (this.startup.mode === "streaming" && !this.resolveToggleInput()) {
861
+ this.restartRequired = true;
862
+ return this.failureResult();
863
+ }
864
+ if (!this.install()) {
865
+ this.unavailableReason ??= "Experimental renderer unavailable; using native thinking";
866
+ this.restartRequired = true;
867
+ return this.failureResult();
868
+ }
869
+ if (this.checkDisplacement()) {
870
+ this.restartRequired = true;
871
+ return this.failureResult();
872
+ }
873
+ if (this.startup.mode === "streaming") {
874
+ const acquired = this.acquireStreamingResources();
875
+ if (!acquired.applied) {
876
+ this.restartRequired = true;
877
+ return this.failureResult();
878
+ }
879
+ }
880
+ this.active = true;
881
+ this.activeMode = this.startup.mode;
882
+ return { applied: true };
883
+ }
884
+
885
+ /** Applies healthy installed transitions while preserving persistence as the authority. */
886
+ reconcile(): ThinkingExperimentalApplyResult {
887
+ const desired = this.getConfig();
888
+ if (!desired.enabled) {
889
+ this.restartRequired = false;
890
+ return this.active ? this.deactivate() : { applied: true };
891
+ }
892
+ if (this.active && this.activeMode === desired.mode) {
893
+ this.restartRequired = false;
894
+ return { applied: true };
895
+ }
896
+ if (
897
+ !this.active ||
898
+ !this.installed ||
899
+ this.displaced ||
900
+ this.unavailableReason ||
901
+ this.checkDisplacement()
902
+ ) {
903
+ this.restartRequired = true;
904
+ return this.failureResult();
905
+ }
906
+ const previousMode = this.activeMode;
907
+ if (!previousMode) {
908
+ this.restartRequired = true;
909
+ return this.failureResult();
910
+ }
911
+ if (desired.mode === "streaming") {
912
+ // Streaming owns terminal input and a timer. Never acquire either from a
913
+ // settings callback: entering Streaming is an explicit restart boundary.
914
+ this.restartRequired = true;
915
+ return this.failureResult();
916
+ }
917
+ const streamingCleanupSucceeded =
918
+ previousMode !== "streaming" || this.releaseStreamingResources();
919
+ this.expanded = false;
920
+ this.activeMode = desired.mode;
921
+ this.restartRequired = false;
922
+ this.rerenderTracked();
923
+ const result = this.liveTransitionResult(desired.mode);
924
+ if (!result.applied || streamingCleanupSucceeded) return result;
925
+ return {
926
+ applied: true,
927
+ reason: formatThinkingStatus(thinkingStatusLabels(this.getConfig(), this.state)),
928
+ };
929
+ }
930
+
931
+ private liveTransitionResult(mode: ThinkingStepsMode): ThinkingExperimentalApplyResult {
932
+ if (this.active && this.activeMode === mode && !this.displaced && !this.unavailableReason) {
933
+ return { applied: true };
934
+ }
935
+ this.restartRequired = true;
936
+ if (!this.unavailableReason && !this.streamingUnavailableReason) {
937
+ this.unavailableReason = "Experimental renderer failed during the live transition";
938
+ }
939
+ return this.failureResult();
940
+ }
941
+
942
+ private install(): boolean {
943
+ if (this.unavailableReason) return false;
944
+ if (this.installed) return !this.displaced;
945
+ if (this.displaced) return false;
946
+ const prototype = AssistantMessageComponent?.prototype;
947
+ if (!prototype) {
948
+ this.unavailableReason = "Pi's private AssistantMessageComponent renderer is unavailable";
949
+ return false;
950
+ }
951
+ try {
952
+ this.patchRegistration = installPrototypePatch(
953
+ prototype,
954
+ "updateContent",
955
+ PATCH_ADAPTER,
956
+ ({ predecessor, receiver, args }) => {
957
+ const component = receiver as object;
958
+ const dropFailedPredecessor = () => {
959
+ try {
960
+ this.dropComponent(component, true);
961
+ } catch {
962
+ // Preserve the predecessor's exact thrown value over bookkeeping failures.
963
+ }
964
+ };
965
+ const renderNative = () => {
966
+ try {
967
+ return Reflect.apply(predecessor, receiver, args);
968
+ } catch (error) {
969
+ dropFailedPredecessor();
970
+ throw error;
971
+ }
972
+ };
973
+ const renderNativeWithHiddenState = (
974
+ instance: PatchableAssistant,
975
+ hidden: HiddenState,
976
+ ) => {
977
+ try {
978
+ return renderWithHiddenState(instance, predecessor, args, hidden);
979
+ } catch (error) {
980
+ dropFailedPredecessor();
981
+ throw error;
982
+ }
983
+ };
984
+ if (!this.active || this.checkDisplacement()) return renderNative();
985
+ const instance = receiver as PatchableAssistant;
986
+ const message = args[0] as AssistantMessage | undefined;
987
+ if (!message || !Array.isArray(message.content) || !hasThinking(message)) {
988
+ const nativeResult = renderNative();
989
+ this.declineOwnership(component);
990
+ return nativeResult;
991
+ }
992
+ // Host-driven calls are the authority for Pi's latest hidden preference.
993
+ // Controller rerenders bypass this wrapper and therefore cannot overwrite it.
994
+ const nativeHidden = hiddenState(instance);
995
+ const mode = this.activeMode;
996
+ if (!mode) {
997
+ const nativeResult = renderNative();
998
+ this.declineOwnership(component);
999
+ return nativeResult;
1000
+ }
1001
+ // The predecessor is outside decoration error containment. Its exact exception,
1002
+ // including object identity, remains authoritative to the host.
1003
+ const nativeResult =
1004
+ mode === "streaming"
1005
+ ? renderNativeWithHiddenState(instance, {
1006
+ own: true,
1007
+ value: false,
1008
+ })
1009
+ : renderNative();
1010
+ if (mode !== "streaming" && nativeHidden.value === true) {
1011
+ this.declineOwnership(component);
1012
+ return nativeResult;
1013
+ }
1014
+ try {
1015
+ const timestamp = messageTimestamp(message);
1016
+ const timing = timestamp === undefined ? undefined : this.timings.get(timestamp);
1017
+ const streamingArgument = args[1];
1018
+ const streaming =
1019
+ typeof streamingArgument === "boolean"
1020
+ ? streamingArgument
1021
+ : instance.isStreaming === true ||
1022
+ message.stopReason === undefined ||
1023
+ (message.stopReason as string) === "pending" ||
1024
+ (timing?.startedAt !== undefined && timing.completedAt === undefined);
1025
+ const incomplete = streaming && timing?.completedAt === undefined;
1026
+ if (!writableOwnChildren(instance)) {
1027
+ this.failShape("Pi's private assistant renderer shape is incompatible");
1028
+ return nativeResult;
1029
+ }
1030
+ const trackedState = this.track(
1031
+ component,
1032
+ message,
1033
+ args,
1034
+ predecessor,
1035
+ incomplete,
1036
+ nativeHidden,
1037
+ );
1038
+ // Tracking can synchronously acquire the first Streaming timer. If that
1039
+ // transaction fails, failActiveStreaming has already restored native UI.
1040
+ if (
1041
+ !this.active ||
1042
+ this.activeMode !== mode ||
1043
+ this.displaced ||
1044
+ this.unavailableReason ||
1045
+ this.states.get(component) !== trackedState ||
1046
+ (mode === "streaming" && (this.streamingListenerPoisoned || !this.stopInput))
1047
+ )
1048
+ return nativeResult;
1049
+ if (mode === "streaming" && this.expanded) return nativeResult;
1050
+ if (
1051
+ !replaceThinkingChildren(
1052
+ instance,
1053
+ message,
1054
+ mode,
1055
+ incomplete,
1056
+ (hidden) => this.headerFor(message, incomplete, hidden),
1057
+ () => {
1058
+ const theme = this.context?.ui.theme as unknown as AccentTheme | undefined;
1059
+ if (!theme || typeof theme.fg !== "function")
1060
+ throw new Error("theme unavailable");
1061
+ return theme;
1062
+ },
1063
+ )
1064
+ ) {
1065
+ this.failShape("Pi's private assistant renderer shape is incompatible");
1066
+ return nativeResult;
1067
+ }
1068
+ const state = this.states.get(component);
1069
+ if (state) state.folded = true;
1070
+ return nativeResult;
1071
+ } catch {
1072
+ this.declineOwnership(component);
1073
+ return nativeResult;
1074
+ }
1075
+ },
1076
+ () => {
1077
+ this.checkDisplacement();
1078
+ },
1079
+ );
1080
+ this.installed = true;
1081
+ return true;
1082
+ } catch (error) {
1083
+ this.unavailableReason =
1084
+ error instanceof Error ? error.message : "Private renderer patch installation failed";
1085
+ return false;
1086
+ }
1087
+ }
1088
+
1089
+ private track(
1090
+ component: object,
1091
+ message: AssistantMessage,
1092
+ args: unknown[],
1093
+ predecessor: (this: unknown, ...args: unknown[]) => unknown,
1094
+ incomplete: boolean,
1095
+ nativeHidden: HiddenState,
1096
+ ): TrackedState {
1097
+ const trackedState: TrackedState = {
1098
+ message,
1099
+ args: [...args],
1100
+ predecessor,
1101
+ incomplete,
1102
+ nativeHidden,
1103
+ folded: false,
1104
+ };
1105
+ this.states.set(component, trackedState);
1106
+ let reference = this.references.get(component);
1107
+ if (!reference) {
1108
+ reference = new WeakRef(component);
1109
+ this.references.set(component, reference);
1110
+ this.tracked.add(reference);
1111
+ }
1112
+ if (incomplete) this.activeComponents.add(reference);
1113
+ else this.activeComponents.delete(reference);
1114
+ this.enforceTrackedCapacity();
1115
+ const timestamp = messageTimestamp(message);
1116
+ if (timestamp !== undefined) {
1117
+ const timing = this.timings.get(timestamp);
1118
+ if (timing?.completedAt !== undefined) {
1119
+ const state = this.states.get(component);
1120
+ if (state) state.incomplete = false;
1121
+ }
1122
+ if (incomplete && timing?.completedAt === undefined && !timing?.startedAt) {
1123
+ this.setTiming(timestamp, { ...timing, startedAt: this.now() });
1124
+ }
1125
+ if (!incomplete && timing?.startedAt !== undefined && timing.completedAt === undefined) {
1126
+ this.setTiming(timestamp, { ...timing, completedAt: this.now() });
1127
+ }
1128
+ }
1129
+ this.pruneTimings();
1130
+ this.reconcileTimer();
1131
+ return trackedState;
1132
+ }
1133
+
1134
+ private headerFor(message: AssistantMessage, incomplete: boolean, hidden: boolean): string {
1135
+ const timestamp = messageTimestamp(message);
1136
+ const timing = timestamp === undefined ? undefined : this.timings.get(timestamp);
1137
+ const hint = hidden ? ` (${this.toggleKey()} to expand)` : "";
1138
+ if (incomplete) {
1139
+ const duration = timing?.startedAt === undefined ? 0 : this.now() - timing.startedAt;
1140
+ return `Thinking ${formatSeconds(duration)}${hint}`;
1141
+ }
1142
+ if (timing?.startedAt !== undefined && timing.completedAt !== undefined) {
1143
+ return `Thought for ${formatSeconds(timing.completedAt - timing.startedAt)}${hint}`;
1144
+ }
1145
+ return `Thought${hint}`;
1146
+ }
1147
+
1148
+ private toggleKey(): string {
1149
+ return this.toggleInput?.label ?? "configured thinking toggle";
1150
+ }
1151
+
1152
+ private resolveToggleInput(): boolean {
1153
+ if (this.streamingListenerPoisoned) return false;
1154
+ if (this.toggleInput) return true;
1155
+ const action = "app.thinking.toggle";
1156
+ if (!this.getHostKeybindings) {
1157
+ this.toggleInput = {
1158
+ label: "ctrl+t",
1159
+ keys: ["ctrl+t"],
1160
+ matches: (data) => {
1161
+ const parseKey = (PiTui as unknown as { parseKey?: (value: string) => string }).parseKey;
1162
+ return data === "\x14" || (typeof parseKey === "function" && parseKey(data) === "ctrl+t");
1163
+ },
1164
+ rawFallback: true,
1165
+ };
1166
+ return true;
1167
+ }
1168
+ let keybindings: KeybindingsShape | undefined;
1169
+ try {
1170
+ keybindings = this.getHostKeybindings();
1171
+ } catch {
1172
+ return this.failToggleCapability("Pi's thinking-toggle keybinding discovery failed");
1173
+ }
1174
+ if (!keybindings) {
1175
+ this.toggleInput = {
1176
+ label: "ctrl+t",
1177
+ keys: ["ctrl+t"],
1178
+ matches: (data) => data === "\x14",
1179
+ rawFallback: true,
1180
+ };
1181
+ return true;
1182
+ }
1183
+ if (
1184
+ typeof keybindings.getDefinition !== "function" ||
1185
+ typeof keybindings.matches !== "function"
1186
+ )
1187
+ return this.failToggleCapability("Pi's thinking-toggle keybinding APIs are incompatible");
1188
+ let definition: unknown;
1189
+ try {
1190
+ definition = keybindings.getDefinition(action);
1191
+ } catch {
1192
+ return this.failToggleCapability("Pi's thinking-toggle action discovery failed");
1193
+ }
1194
+ // Hosts predating the action have no discoverable action API, so retain their raw default.
1195
+ if (definition === undefined || definition === null) {
1196
+ this.toggleInput = {
1197
+ label: "ctrl+t",
1198
+ keys: ["ctrl+t"],
1199
+ matches: (data) => data === "\x14",
1200
+ rawFallback: true,
1201
+ };
1202
+ return true;
1203
+ }
1204
+ if (typeof keybindings.getKeys !== "function")
1205
+ return this.failToggleCapability("Pi's thinking-toggle configured bindings are unavailable");
1206
+ let keys: string[];
1207
+ let label: string;
1208
+ try {
1209
+ keys = keybindings.getKeys(action);
1210
+ label = this.getHostKeyText?.(action) ?? "";
1211
+ } catch {
1212
+ return this.failToggleCapability("Pi's thinking-toggle binding discovery failed");
1213
+ }
1214
+ if (
1215
+ !Array.isArray(keys) ||
1216
+ keys.length === 0 ||
1217
+ keys.some((key) => typeof key !== "string" || key.length === 0)
1218
+ )
1219
+ return this.failToggleCapability("Pi's thinking-toggle has no usable configured binding");
1220
+ this.toggleInput = {
1221
+ label: label.trim() || keys.join("/"),
1222
+ keys: [...keys],
1223
+ matches: (data) => keybindings?.matches(data, action) === true,
1224
+ rawFallback: false,
1225
+ };
1226
+ return true;
1227
+ }
1228
+
1229
+ private failToggleCapability(reason: string): false {
1230
+ this.poisonStreamingListener(reason);
1231
+ return false;
1232
+ }
1233
+
1234
+ private poisonStreamingListener(
1235
+ reason = "Pi's terminal input listener cleanup is unavailable",
1236
+ ): void {
1237
+ this.streamingListenerPoisoned = true;
1238
+ this.streamingUnavailableReason = reason;
1239
+ this.toggleInput = undefined;
1240
+ }
1241
+
1242
+ private cleanupInput(stopInput: (() => void) | undefined): boolean {
1243
+ if (!stopInput) return true;
1244
+ try {
1245
+ stopInput();
1246
+ return true;
1247
+ } catch {
1248
+ this.poisonStreamingListener();
1249
+ return false;
1250
+ }
1251
+ }
1252
+
1253
+ private failActiveStreaming(reason: string): void {
1254
+ // Invalidate first so a retained or currently executing callback can never act again.
1255
+ this.resourceToken += 1;
1256
+ this.releaseStreamingResources();
1257
+ this.poisonStreamingListener(reason);
1258
+ this.active = false;
1259
+ this.activeMode = undefined;
1260
+ this.restartRequired = true;
1261
+ this.rerenderTracked(true, true);
1262
+ this.clearTrackedSnapshots();
1263
+ this.clearLiveState();
1264
+ }
1265
+
1266
+ private acquireStreamingResources(): { applied: boolean; reason?: string } {
1267
+ if (this.stopInput || this.interval) {
1268
+ return { applied: false, reason: "Experimental input resources are already owned" };
1269
+ }
1270
+ if (this.streamingListenerPoisoned) {
1271
+ return {
1272
+ applied: false,
1273
+ reason:
1274
+ this.streamingUnavailableReason ?? "Pi's terminal input listener cleanup is unavailable",
1275
+ };
1276
+ }
1277
+ if (!this.context || !this.toggleInput) {
1278
+ return { applied: false, reason: "Experimental input handling is unavailable" };
1279
+ }
1280
+ const token = ++this.resourceToken;
1281
+ let stopInput: unknown;
1282
+ try {
1283
+ stopInput = this.context.ui.onTerminalInput((data) => {
1284
+ if (
1285
+ this.disposed ||
1286
+ token !== this.resourceToken ||
1287
+ !this.active ||
1288
+ this.activeMode !== "streaming" ||
1289
+ this.checkDisplacement()
1290
+ )
1291
+ return;
1292
+ const input = this.toggleInput;
1293
+ if (!input) return;
1294
+ let matches = false;
1295
+ try {
1296
+ matches = input.matches(data);
1297
+ if (!matches && !input.rawFallback && isKeyRelease(data)) {
1298
+ const parseKey = (PiTui as unknown as { parseKey?: (value: string) => string })
1299
+ .parseKey;
1300
+ const parsed = typeof parseKey === "function" ? parseKey(data) : undefined;
1301
+ matches = parsed !== undefined && input.keys.includes(parsed);
1302
+ }
1303
+ } catch {
1304
+ this.failActiveStreaming("Pi's thinking-toggle matcher failed at runtime");
1305
+ return;
1306
+ }
1307
+ if (!matches) return;
1308
+ if (!isKeyRelease(data)) {
1309
+ this.expanded = !this.expanded;
1310
+ this.rerenderTracked();
1311
+ }
1312
+ return { consume: true };
1313
+ });
1314
+ } catch {
1315
+ // Registration may retain the callback before throwing. Invalidate it and never
1316
+ // attempt another registration in this session because ownership is unknowable.
1317
+ this.resourceToken += 1;
1318
+ this.poisonStreamingListener("Pi's terminal input listener registration is unavailable");
1319
+ return { applied: false, reason: this.streamingUnavailableReason };
1320
+ }
1321
+ if (typeof stopInput !== "function") {
1322
+ // The host may have retained the callback without giving us a release mechanism.
1323
+ // Poison Streaming for this session and leave that callback permanently inert.
1324
+ this.resourceToken += 1;
1325
+ this.poisonStreamingListener();
1326
+ return { applied: false, reason: this.streamingUnavailableReason };
1327
+ }
1328
+ const releaseInput = stopInput as () => void;
1329
+ let interval: ReturnType<typeof setInterval> | undefined;
1330
+ try {
1331
+ if (this.activeEntries().length > 0) interval = this.createStreamingTimer(token);
1332
+ } catch {
1333
+ // Registration succeeded but the acquisition transaction did not. Invalidate the
1334
+ // candidate callback before cleanup and poison Streaming even when cleanup succeeds.
1335
+ this.resourceToken += 1;
1336
+ this.cleanupInput(releaseInput);
1337
+ this.poisonStreamingListener("Pi's thinking timer is unavailable");
1338
+ return { applied: false, reason: this.streamingUnavailableReason };
1339
+ }
1340
+ this.stopInput = releaseInput;
1341
+ this.interval = interval;
1342
+ return { applied: true };
1343
+ }
1344
+
1345
+ private createStreamingTimer(resourceToken = this.resourceToken): ReturnType<typeof setInterval> {
1346
+ const timerToken = ++this.timerToken;
1347
+ return setInterval(() => {
1348
+ // Timer/resource release invalidates callbacks before attempting host cleanup.
1349
+ // A host that throws from clearInterval may retain this callback, so stale
1350
+ // callbacks must be wholly inert while a later release retries the same handle.
1351
+ if (
1352
+ this.disposed ||
1353
+ resourceToken !== this.resourceToken ||
1354
+ timerToken !== this.timerToken ||
1355
+ !this.active ||
1356
+ this.activeMode !== "streaming"
1357
+ )
1358
+ return;
1359
+ if (this.checkDisplacement()) return;
1360
+ this.rerenderActive();
1361
+ this.reconcileTimer();
1362
+ }, 1000);
1363
+ }
1364
+
1365
+ private releaseStreamingResources(): boolean {
1366
+ this.resourceToken += 1;
1367
+ const timerCleanupSucceeded = this.stopTimer();
1368
+ const stopInput = this.stopInput;
1369
+ this.stopInput = undefined;
1370
+ const inputCleanupSucceeded = this.cleanupInput(stopInput);
1371
+ return timerCleanupSucceeded && inputCleanupSucceeded;
1372
+ }
1373
+
1374
+ private trackedEntries(): Array<[object, TrackedState]> {
1375
+ const entries: Array<[object, TrackedState]> = [];
1376
+ for (const reference of this.tracked) {
1377
+ const component = reference.deref();
1378
+ if (!component) {
1379
+ this.tracked.delete(reference);
1380
+ this.activeComponents.delete(reference);
1381
+ continue;
1382
+ }
1383
+ const state = this.states.get(component);
1384
+ if (state) entries.push([component, state]);
1385
+ else {
1386
+ this.tracked.delete(reference);
1387
+ this.activeComponents.delete(reference);
1388
+ this.references.delete(component);
1389
+ }
1390
+ }
1391
+ return entries;
1392
+ }
1393
+
1394
+ private activeEntries(): Array<[object, TrackedState]> {
1395
+ const entries: Array<[object, TrackedState]> = [];
1396
+ for (const reference of this.activeComponents) {
1397
+ const component = reference.deref();
1398
+ if (!component) {
1399
+ this.activeComponents.delete(reference);
1400
+ this.tracked.delete(reference);
1401
+ continue;
1402
+ }
1403
+ const state = this.states.get(component);
1404
+ if (state?.incomplete) entries.push([component, state]);
1405
+ else this.activeComponents.delete(reference);
1406
+ }
1407
+ return entries;
1408
+ }
1409
+
1410
+ private declineOwnership(component: object): void {
1411
+ try {
1412
+ this.dropComponent(component, true);
1413
+ } catch {
1414
+ // Native already completed; private bookkeeping must fail open.
1415
+ }
1416
+ }
1417
+
1418
+ private dropComponent(component: object, dropTiming = false): void {
1419
+ const state = this.states.get(component);
1420
+ const reference = this.references.get(component);
1421
+ if (reference) {
1422
+ this.tracked.delete(reference);
1423
+ this.activeComponents.delete(reference);
1424
+ }
1425
+ this.references.delete(component);
1426
+ this.states.delete(component);
1427
+ if (!dropTiming || !state) return;
1428
+ const timestamp = messageTimestamp(state.message);
1429
+ if (timestamp === undefined) return;
1430
+ const stillReferenced = this.trackedEntries().some(
1431
+ ([, tracked]) => messageTimestamp(tracked.message) === timestamp,
1432
+ );
1433
+ if (!stillReferenced) this.timings.delete(timestamp);
1434
+ if (this.currentMessage && messageTimestamp(this.currentMessage) === timestamp) {
1435
+ this.currentMessage = undefined;
1436
+ }
1437
+ this.reconcileTimer();
1438
+ }
1439
+
1440
+ private enforceTrackedCapacity(): void {
1441
+ while (this.tracked.size > THINKING_EXPERIMENTAL_MAX_TRACKED_COMPONENTS) {
1442
+ const reference = this.tracked.values().next().value as WeakRef<object> | undefined;
1443
+ if (!reference) break;
1444
+ const component = reference.deref();
1445
+ const state = component ? this.states.get(component) : undefined;
1446
+ if (component && state?.folded) {
1447
+ this.restoreNative([[component, state]]);
1448
+ this.requestHostRender();
1449
+ }
1450
+ if (component) this.dropComponent(component);
1451
+ else {
1452
+ this.tracked.delete(reference);
1453
+ this.activeComponents.delete(reference);
1454
+ }
1455
+ }
1456
+ }
1457
+
1458
+ private restoreNative(
1459
+ entries: Array<[object, TrackedState]>,
1460
+ preserveVisibleUnfolded = false,
1461
+ ): number {
1462
+ let processed = 0;
1463
+ for (const [component, state] of entries) {
1464
+ processed += 1;
1465
+ const instance = component as PatchableAssistant;
1466
+ try {
1467
+ // A visible native predecessor has already produced the authoritative children
1468
+ // when first-message timer acquisition fails inside track(). Preserve their exact
1469
+ // identity rather than invoking the predecessor a second time.
1470
+ if (!preserveVisibleUnfolded || state.folded || state.nativeHidden.value === true)
1471
+ renderWithHiddenState(instance, state.predecessor, state.args, state.nativeHidden);
1472
+ } catch {
1473
+ // A stale transcript component must never break restoration of later components.
1474
+ } finally {
1475
+ setHiddenState(instance, state.nativeHidden);
1476
+ state.folded = false;
1477
+ }
1478
+ }
1479
+ return processed;
1480
+ }
1481
+
1482
+ private rerenderEntries(entries: Array<[object, TrackedState]>, nativeOnly = false): number {
1483
+ let processed = 0;
1484
+ if (nativeOnly) return this.restoreNative(entries);
1485
+ this.rerendering = true;
1486
+ this.shapeFailureDuringRerender = undefined;
1487
+ try {
1488
+ for (const [component, state] of entries) {
1489
+ processed += 1;
1490
+ const instance = component as PatchableAssistant;
1491
+ try {
1492
+ const mode = this.activeMode;
1493
+ if (!mode) continue;
1494
+ if (mode === "streaming") {
1495
+ renderWithHiddenState(instance, state.predecessor, state.args, {
1496
+ own: true,
1497
+ value: false,
1498
+ });
1499
+ } else {
1500
+ renderWithHiddenState(instance, state.predecessor, state.args, state.nativeHidden);
1501
+ if (state.nativeHidden.value === true) continue;
1502
+ }
1503
+ state.folded = false;
1504
+ if (
1505
+ this.active &&
1506
+ (mode !== "streaming" || !this.expanded) &&
1507
+ hasThinking(state.message)
1508
+ ) {
1509
+ if (
1510
+ !replaceThinkingChildren(
1511
+ instance,
1512
+ state.message,
1513
+ mode,
1514
+ state.incomplete,
1515
+ (hidden) => this.headerFor(state.message, state.incomplete, hidden),
1516
+ () => {
1517
+ const theme = this.context?.ui.theme as unknown as AccentTheme | undefined;
1518
+ if (!theme || typeof theme.fg !== "function")
1519
+ throw new Error("theme unavailable");
1520
+ return theme;
1521
+ },
1522
+ )
1523
+ ) {
1524
+ this.failShape("Pi's private assistant renderer shape is incompatible");
1525
+ break;
1526
+ }
1527
+ state.folded = true;
1528
+ }
1529
+ } catch {
1530
+ this.failShape("Pi's private assistant renderer failed during a live rerender");
1531
+ }
1532
+ if (this.shapeFailureDuringRerender) break;
1533
+ }
1534
+ } finally {
1535
+ this.rerendering = false;
1536
+ }
1537
+ if (this.shapeFailureDuringRerender) {
1538
+ // The failed fold may already have revealed one component. Abort the outer
1539
+ // iteration and restore every original hidden state in one dedicated pass.
1540
+ processed = this.restoreNative(this.trackedEntries());
1541
+ this.shapeFailureDuringRerender = undefined;
1542
+ this.clearTrackedSnapshots();
1543
+ }
1544
+ return processed;
1545
+ }
1546
+
1547
+ private rerenderTracked(nativeOnly = false, preserveVisibleUnfolded = false): void {
1548
+ const entries = this.trackedEntries();
1549
+ const processed = nativeOnly
1550
+ ? this.restoreNative(entries, preserveVisibleUnfolded)
1551
+ : this.rerenderEntries(entries);
1552
+ if (processed > 0) this.requestHostRender();
1553
+ }
1554
+
1555
+ private rerenderActive(): void {
1556
+ const entries = this.activeEntries();
1557
+ this.lastTimerWork = this.rerenderEntries(entries);
1558
+ if (this.lastTimerWork > 0) this.requestHostRender();
1559
+ }
1560
+
1561
+ private requestHostRender(): void {
1562
+ this.requestRender();
1563
+ }
1564
+
1565
+ private reconcileTimer(): void {
1566
+ if (!this.active || this.activeMode !== "streaming" || !this.stopInput) {
1567
+ this.stopTimer();
1568
+ return;
1569
+ }
1570
+ if (this.activeEntries().length === 0) {
1571
+ this.stopTimer();
1572
+ return;
1573
+ }
1574
+ if (!this.interval && !this.streamingListenerPoisoned) {
1575
+ try {
1576
+ this.interval = this.createStreamingTimer();
1577
+ } catch {
1578
+ this.failActiveStreaming("Pi's thinking timer is unavailable");
1579
+ }
1580
+ }
1581
+ }
1582
+
1583
+ private stopTimer(): boolean {
1584
+ const interval = this.interval;
1585
+ if (!interval) return true;
1586
+ this.timerToken += 1;
1587
+ try {
1588
+ clearInterval(interval);
1589
+ this.interval = undefined;
1590
+ return true;
1591
+ } catch {
1592
+ // Keep the exact handle so shutdown or another release can retry it. Streaming
1593
+ // remains poisoned, preventing any replacement timer/listener acquisition.
1594
+ this.poisonStreamingListener("Pi's thinking timer cleanup is unavailable");
1595
+ return false;
1596
+ }
1597
+ }
1598
+
1599
+ private checkDisplacement(): boolean {
1600
+ if (
1601
+ this.installed &&
1602
+ (!AssistantMessageComponent ||
1603
+ !isPrototypePatchCurrent(
1604
+ AssistantMessageComponent.prototype,
1605
+ "updateContent",
1606
+ PATCH_ADAPTER,
1607
+ this.patchRegistration?.token,
1608
+ ))
1609
+ ) {
1610
+ if (!this.displaced) {
1611
+ this.displaced = true;
1612
+ this.active = false;
1613
+ this.restartRequired = true;
1614
+ // Restore every component synchronously before relinquishing input/timer ownership.
1615
+ this.rerenderTracked(true);
1616
+ this.clearTrackedSnapshots();
1617
+ this.releaseStreamingResources();
1618
+ }
1619
+ }
1620
+ return this.displaced;
1621
+ }
1622
+
1623
+ private failShape(reason: string): void {
1624
+ this.unavailableReason = reason;
1625
+ this.active = false;
1626
+ this.activeMode = undefined;
1627
+ this.restartRequired = true;
1628
+ this.releaseStreamingResources();
1629
+ if (this.rerendering) {
1630
+ this.shapeFailureDuringRerender = reason;
1631
+ return;
1632
+ }
1633
+ this.rerenderTracked(true);
1634
+ this.clearTrackedSnapshots();
1635
+ }
1636
+
1637
+ private clearTrackedSnapshots(): void {
1638
+ this.tracked.clear();
1639
+ this.activeComponents.clear();
1640
+ this.references = new WeakMap();
1641
+ this.states = new WeakMap();
1642
+ this.lastTimerWork = 0;
1643
+ }
1644
+
1645
+ private clearLiveState(): void {
1646
+ this.timings.clear();
1647
+ this.currentMessage = undefined;
1648
+ }
1649
+
1650
+ private deactivate(): ThinkingExperimentalApplyResult {
1651
+ this.active = false;
1652
+ this.activeMode = undefined;
1653
+ this.expanded = false;
1654
+ const streamingCleanupSucceeded = this.releaseStreamingResources();
1655
+ this.rerenderTracked(true);
1656
+ // Cached host components stay native until Pi next rebuilds/updates them. Dropping
1657
+ // snapshots is intentional: reactivation must never replay pre-disable content.
1658
+ this.clearTrackedSnapshots();
1659
+ this.clearLiveState();
1660
+ this.toggleInput = undefined;
1661
+ if (streamingCleanupSucceeded) return { applied: true };
1662
+ return {
1663
+ applied: true,
1664
+ reason: formatThinkingStatus(thinkingStatusLabels(this.getConfig(), this.state)),
1665
+ };
1666
+ }
1667
+
1668
+ beginMessage(event: unknown): void {
1669
+ if (!this.active) return;
1670
+ const message = assistantMessage(event);
1671
+ if (!message) return;
1672
+ this.currentMessage = message;
1673
+ const timestamp = messageTimestamp(message);
1674
+ if (timestamp !== undefined) this.setTiming(timestamp, { startedAt: this.now() });
1675
+ }
1676
+
1677
+ updateMessage(event: unknown): void {
1678
+ if (!this.active) return;
1679
+ const message = assistantMessage(event);
1680
+ if (!message) return;
1681
+ this.currentMessage = message;
1682
+ const type = eventType(event);
1683
+ if (beginsThinkingPhase(type)) this.reopen(message);
1684
+ else if (endsThinkingPhase(type)) this.complete(message);
1685
+ }
1686
+
1687
+ endMessage(event: unknown): void {
1688
+ if (!this.active) return;
1689
+ const message = assistantMessage(event) ?? this.currentMessage;
1690
+ if (message) this.complete(message);
1691
+ this.currentMessage = undefined;
1692
+ }
1693
+
1694
+ endAgent(): void {
1695
+ if (!this.active) return;
1696
+ if (this.currentMessage) this.complete(this.currentMessage);
1697
+ this.currentMessage = undefined;
1698
+ }
1699
+
1700
+ private reopen(message: AssistantMessage): void {
1701
+ const timestamp = messageTimestamp(message);
1702
+ if (timestamp === undefined) return;
1703
+ const timing = this.timings.get(timestamp);
1704
+ if (timing?.completedAt === undefined) return;
1705
+ this.setTiming(timestamp, { startedAt: timing.startedAt ?? this.now() });
1706
+ const reopenedEntries = this.trackedEntries().filter(([, state]) => {
1707
+ if (messageTimestamp(state.message) !== timestamp) return false;
1708
+ state.incomplete = true;
1709
+ return true;
1710
+ });
1711
+ for (const [component] of reopenedEntries) {
1712
+ const reference = this.references.get(component);
1713
+ if (reference) this.activeComponents.add(reference);
1714
+ }
1715
+ const processed = this.rerenderEntries(reopenedEntries);
1716
+ if (processed > 0) this.requestHostRender();
1717
+ this.reconcileTimer();
1718
+ }
1719
+
1720
+ private complete(message: AssistantMessage): void {
1721
+ const timestamp = messageTimestamp(message);
1722
+ if (timestamp === undefined) return;
1723
+ const timing = this.timings.get(timestamp);
1724
+ if (timing?.completedAt !== undefined) return;
1725
+ this.setTiming(timestamp, {
1726
+ ...timing,
1727
+ completedAt: this.now(),
1728
+ });
1729
+ const completedEntries = this.activeEntries().filter(([, state]) => {
1730
+ if (messageTimestamp(state.message) !== timestamp) return false;
1731
+ state.incomplete = false;
1732
+ return true;
1733
+ });
1734
+ for (const [component] of completedEntries) {
1735
+ const reference = this.references.get(component);
1736
+ if (reference) this.activeComponents.delete(reference);
1737
+ }
1738
+ const processed = this.rerenderEntries(completedEntries);
1739
+ if (processed > 0) this.requestHostRender();
1740
+ this.reconcileTimer();
1741
+ }
1742
+
1743
+ private setTiming(timestamp: number, timing: Timing): void {
1744
+ this.timings.set(timestamp, timing);
1745
+ this.pruneTimings();
1746
+ }
1747
+
1748
+ private pruneTimings(): void {
1749
+ while (this.timings.size > MAX_TIMINGS) {
1750
+ const oldest = this.timings.keys().next().value;
1751
+ if (oldest === undefined) break;
1752
+ this.timings.delete(oldest);
1753
+ }
1754
+ }
1755
+
1756
+ shutdown(): void {
1757
+ this.disposed = true;
1758
+ this.deactivate();
1759
+ this.startup = { enabled: false, mode: "tree" };
1760
+ this.restartRequired = false;
1761
+ this.patchRegistration?.();
1762
+ this.patchRegistration = undefined;
1763
+ this.installed = false;
1764
+ this.context = undefined;
1765
+ this.clearTrackedSnapshots();
1766
+ this.clearLiveState();
1767
+ }
1768
+ }