@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,69 @@
1
+ import type { ThinkingStepsComponentConfig, ThinkingStepsMode } from "./config";
2
+
3
+ export type ThinkingStatusState = Readonly<{
4
+ active: boolean;
5
+ activeMode?: ThinkingStepsMode;
6
+ restartRequired: boolean;
7
+ /** Whole private renderer capability, independent of mode-specific resources. */
8
+ rendererAvailable?: boolean;
9
+ /** Streaming input/timer capability for this session. */
10
+ streamingAvailable?: boolean;
11
+ /** Irreversible session-local acquisition/callback poison. */
12
+ streamingPoisoned?: boolean;
13
+ /** Legacy whole-renderer capability accepted by settings adapters. */
14
+ available?: boolean;
15
+ reason?: string;
16
+ }>;
17
+
18
+ export type ThinkingStatusLabels = Readonly<{
19
+ saved: ThinkingStepsMode | "disabled";
20
+ active: ThinkingStepsMode | "native";
21
+ rendererUnavailable: boolean;
22
+ streamingUnavailable: boolean;
23
+ streamingPoisoned: boolean;
24
+ restartRequired: boolean;
25
+ reason?: string;
26
+ }>;
27
+
28
+ function cleanReason(value: string | undefined): string | undefined {
29
+ const reason = value
30
+ ?.replace(/(?:[;,·]\s*)?restart required\b[.!]?/gi, "")
31
+ .replace(/^restart pi to apply(?: saved thinking changes)?[.!]?$/i, "")
32
+ .replace(/^[\s;,.·]+|[\s;,.·]+$/g, "")
33
+ .trim();
34
+ if (!reason || /^(?:renderer|streaming) unavailable$/i.test(reason)) return undefined;
35
+ return reason;
36
+ }
37
+
38
+ /** Builds independent status facts once so every consumer reports the same state. */
39
+ export function thinkingStatusLabels(
40
+ config: ThinkingStepsComponentConfig,
41
+ state: ThinkingStatusState,
42
+ ): ThinkingStatusLabels {
43
+ const rendererAvailable = state.rendererAvailable ?? state.available ?? true;
44
+ const reason = cleanReason(state.reason);
45
+ return {
46
+ saved: config.enabled ? config.mode : "disabled",
47
+ active: state.active && state.activeMode ? state.activeMode : "native",
48
+ rendererUnavailable: !rendererAvailable,
49
+ streamingUnavailable: state.streamingAvailable === false || state.streamingPoisoned === true,
50
+ streamingPoisoned: state.streamingPoisoned === true,
51
+ restartRequired: state.restartRequired,
52
+ ...(reason ? { reason } : {}),
53
+ };
54
+ }
55
+
56
+ const modeLabel = (mode: ThinkingStepsMode | "disabled" | "native"): string =>
57
+ `${mode.charAt(0).toUpperCase()}${mode.slice(1)}`;
58
+
59
+ /** Formats the canonical complete status line used by settings, previews, and apply failures. */
60
+ export function formatThinkingStatus(facts: ThinkingStatusLabels): string {
61
+ return [
62
+ `Saved: ${modeLabel(facts.saved)}`,
63
+ `Active: ${modeLabel(facts.active)}`,
64
+ ...(facts.rendererUnavailable ? ["Renderer unavailable"] : []),
65
+ ...(facts.streamingUnavailable ? ["Streaming unavailable"] : []),
66
+ ...(facts.restartRequired ? ["restart required"] : []),
67
+ ...(facts.reason ? [facts.reason] : []),
68
+ ].join(" · ");
69
+ }
@@ -0,0 +1,155 @@
1
+ import { sanitizeSgrOnlySourceText } from "./user-message-osc";
2
+
3
+ /** Hard limits keep private transcript parsing synchronous and bounded. Limits are inclusive. */
4
+ export const THINKING_STEPS_MAX_INPUT_LENGTH = 65_536;
5
+ export const THINKING_STEPS_MAX_STEPS = 128;
6
+ export const THINKING_STEPS_MAX_LABEL_LENGTH = 512;
7
+
8
+ export type ThinkingStep = Readonly<{
9
+ number: number;
10
+ label: string;
11
+ body: string;
12
+ }>;
13
+
14
+ type MutableStep = {
15
+ number: number;
16
+ label: string;
17
+ bodyLines: string[];
18
+ };
19
+
20
+ type StructuralLabel = { label: string };
21
+ type OpaqueBlock = { end: number; malformed: boolean };
22
+
23
+ const meaningfulLabelPattern = /[\p{L}\p{N}\p{Extended_Pictographic}]/u;
24
+
25
+ function headingLabel(line: string): StructuralLabel | undefined {
26
+ const match = /^(#{1,6})[\t ]+(.+?)\s*$/.exec(line);
27
+ if (!match) return undefined;
28
+ const label = (match[2] ?? "").replace(/[\t ]+#+[\t ]*$/, "").trim();
29
+ return label ? { label } : undefined;
30
+ }
31
+
32
+ function listLabel(line: string): StructuralLabel | undefined {
33
+ const match = /^(?:[-+*]|\d{1,9}[.)])[\t ]+(.+?)\s*$/.exec(line);
34
+ const label = match?.[1]?.trim() ?? "";
35
+ return label ? { label } : undefined;
36
+ }
37
+
38
+ function structuralLabel(line: string): StructuralLabel | undefined {
39
+ return headingLabel(line) ?? listLabel(line);
40
+ }
41
+
42
+ function fenceBlock(lines: readonly string[], start: number): OpaqueBlock | undefined {
43
+ const opening = /^ {0,3}(`{3,}|~{3,})([^\r\n]*)$/.exec(lines[start] ?? "");
44
+ if (!opening) return undefined;
45
+ const marker = opening[1] ?? "";
46
+ const info = opening[2] ?? "";
47
+ if (marker[0] === "`" && info.includes("`")) return { end: start, malformed: true };
48
+ const closing = new RegExp(`^ {0,3}${marker[0]}{${marker.length},}[\\t ]*$`);
49
+ for (let index = start + 1; index < lines.length; index += 1) {
50
+ if (closing.test(lines[index] ?? "")) return { end: index, malformed: false };
51
+ }
52
+ return { end: lines.length - 1, malformed: true };
53
+ }
54
+
55
+ function mathBlock(lines: readonly string[], start: number): OpaqueBlock | undefined {
56
+ const line = lines[start] ?? "";
57
+ if (/^ {0,3}\$\$.+\$\$[\t ]*$/.test(line)) return { end: start, malformed: false };
58
+ const close = /^ {0,3}\$\$[\t ]*$/.test(line)
59
+ ? /^(?: {0,3})\$\$[\t ]*$/
60
+ : /^ {0,3}\\\[[\t ]*$/.test(line)
61
+ ? /^(?: {0,3})\\\][\t ]*$/
62
+ : undefined;
63
+ if (!close) return undefined;
64
+ for (let index = start + 1; index < lines.length; index += 1) {
65
+ if (close.test(lines[index] ?? "")) return { end: index, malformed: false };
66
+ }
67
+ return { end: lines.length - 1, malformed: true };
68
+ }
69
+
70
+ function isTopLevelProse(line: string): boolean {
71
+ if (!line.trim() || /^[\t ]/.test(line)) return false;
72
+ if (/^(?: {0,3})(?:`{3,}|~{3,}|\$\$|\\\[|>)/.test(line)) return false;
73
+ return !/^(?:#{1,6}(?:[\t ]|$)|[-+*](?:[\t ]|$)|\d{1,9}[.)](?:[\t ]|$))/.test(line);
74
+ }
75
+
76
+ function safeLabel(label: string): string | undefined {
77
+ const trimmed = label.trim();
78
+ if (
79
+ !trimmed ||
80
+ trimmed.length > THINKING_STEPS_MAX_LABEL_LENGTH ||
81
+ !meaningfulLabelPattern.test(trimmed) ||
82
+ trimmed.includes("\t")
83
+ ) {
84
+ return undefined;
85
+ }
86
+ return trimmed;
87
+ }
88
+
89
+ function finishSteps(steps: MutableStep[]): ThinkingStep[] | undefined {
90
+ const finished: ThinkingStep[] = [];
91
+ for (const step of steps) {
92
+ const label = safeLabel(step.label);
93
+ if (!label) return undefined;
94
+ let first = 0;
95
+ let last = step.bodyLines.length;
96
+ while (first < last && !(step.bodyLines[first] ?? "").trim()) first += 1;
97
+ while (last > first && !(step.bodyLines[last - 1] ?? "").trim()) last -= 1;
98
+ finished.push(
99
+ Object.freeze({
100
+ number: step.number,
101
+ label,
102
+ body: step.bodyLines.slice(first, last).join("\n"),
103
+ }),
104
+ );
105
+ }
106
+ return finished;
107
+ }
108
+
109
+ /** Parse source-level structure; fenced and display-math blocks remain opaque bodies. */
110
+ export function parseThinkingSteps(markdown: string): readonly ThinkingStep[] | undefined {
111
+ if (!markdown || markdown.length > THINKING_STEPS_MAX_INPUT_LENGTH) return undefined;
112
+ const source = sanitizeSgrOnlySourceText(markdown.replace(/\r\n/g, "\n"));
113
+ if (source === undefined || !source.trim()) return undefined;
114
+
115
+ const lines = source.split("\n");
116
+ const steps: MutableStep[] = [];
117
+ let current: MutableStep | undefined;
118
+ let paragraphBoundary = true;
119
+ const startStep = (label: string) => {
120
+ if (steps.length >= THINKING_STEPS_MAX_STEPS) return false;
121
+ current = { number: steps.length + 1, label, bodyLines: [] };
122
+ steps.push(current);
123
+ paragraphBoundary = false;
124
+ return true;
125
+ };
126
+
127
+ for (let index = 0; index < lines.length; index += 1) {
128
+ const line = lines[index] ?? "";
129
+ if (!line.trim()) {
130
+ if (current) current.bodyLines.push(line);
131
+ paragraphBoundary = true;
132
+ continue;
133
+ }
134
+ const opaque = fenceBlock(lines, index) ?? mathBlock(lines, index);
135
+ if (opaque) {
136
+ if (opaque.malformed || !current) return undefined;
137
+ current.bodyLines.push(...lines.slice(index, opaque.end + 1));
138
+ index = opaque.end;
139
+ continue;
140
+ }
141
+ const structural = structuralLabel(line);
142
+ if (structural) {
143
+ if (!startStep(structural.label)) return undefined;
144
+ continue;
145
+ }
146
+ if (isTopLevelProse(line) && (!current || paragraphBoundary)) {
147
+ if (!startStep(line.trim())) return undefined;
148
+ continue;
149
+ }
150
+ if (!current) return undefined;
151
+ current.bodyLines.push(line);
152
+ }
153
+ if (steps.length === 0) return undefined;
154
+ return finishSteps(steps);
155
+ }
@@ -602,7 +602,7 @@ function renderAccentRailFrameFromBase({
602
602
  above: parsedTop?.count,
603
603
  below: parsedBottom?.count,
604
604
  };
605
- const lines = renderAccentRailEditorFrame({
605
+ const renderedLines = renderAccentRailEditorFrame({
606
606
  width,
607
607
  editorLines,
608
608
  autocompleteLines,
@@ -610,6 +610,7 @@ function renderAccentRailFrameFromBase({
610
610
  uiTheme,
611
611
  config,
612
612
  });
613
+ const lines = renderedLines.length === 1 ? ["", ...renderedLines, ""] : renderedLines;
613
614
  POLISHED_FRAME_SPLITS.set(lines, {
614
615
  rows: Object.freeze([...lines]),
615
616
  split: { editorLines, trailingLines: autocompleteLines, viewport },
@@ -221,15 +221,29 @@ function filterBalancedUserMessageTerminalText(text: string, options: FilterOpti
221
221
  return filterUserMessageTerminalText(text, { ...options, preserveValidOsc8: false }).text;
222
222
  }
223
223
 
224
+ const sourceFilterOptions = {
225
+ preserveValidOsc8: false,
226
+ preserveSourceWhitespace: true,
227
+ } as const;
228
+
224
229
  /** Strip every terminal control from untrusted source text before Markdown rendering. */
225
230
  export function sanitizeUserMessageSourceText(text: string): string {
226
231
  return filterBalancedUserMessageTerminalText(text, {
227
- preserveValidOsc8: false,
228
- preserveSourceWhitespace: true,
232
+ ...sourceFilterOptions,
229
233
  preserveSgr: false,
230
234
  });
231
235
  }
232
236
 
237
+ /** Strip strict 7-bit CSI SGR, rejecting source that contains any other terminal control. */
238
+ export function sanitizeSgrOnlySourceText(text: string): string | undefined {
239
+ const sgrPreserved = filterBalancedUserMessageTerminalText(text, {
240
+ ...sourceFilterOptions,
241
+ preserveSgr: true,
242
+ });
243
+ if (sgrPreserved !== text) return undefined;
244
+ return sanitizeUserMessageSourceText(text);
245
+ }
246
+
233
247
  /**
234
248
  * Sanitize a predecessor-rendered row. Pi formatting needs only SGR and
235
249
  * structurally valid 7-bit OSC 8; every other terminal control is removed.
@@ -24,7 +24,7 @@ function themeFg(theme: Theme | undefined, color: ThemeColor, text: string): str
24
24
  return theme ? theme.fg(color, text) : text;
25
25
  }
26
26
 
27
- function makeMarkdownTheme(theme: Theme | undefined): MarkdownTheme {
27
+ export function makeMarkdownTheme(theme: Theme | undefined): MarkdownTheme {
28
28
  return {
29
29
  heading: (text) => themeFg(theme, "mdHeading", text),
30
30
  link: (text) => themeFg(theme, "mdLink", text),
@@ -0,0 +1,124 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { AssistantMessageComponent, initTheme } from "@earendil-works/pi-coding-agent";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import type { PolishedTuiConfig } from "../extensions/zentui/config";
5
+
6
+ const mocks = vi.hoisted(() => ({
7
+ config: undefined as unknown as PolishedTuiConfig,
8
+ loadConfig: vi.fn(() => mocks.config),
9
+ }));
10
+
11
+ vi.mock("../extensions/zentui/config", async (importOriginal) => {
12
+ const actual = await importOriginal<typeof import("../extensions/zentui/config")>();
13
+ return { ...actual, loadConfig: mocks.loadConfig };
14
+ });
15
+
16
+ import { defaultConfig } from "../extensions/zentui/config";
17
+ import zentui from "../extensions/zentui/index";
18
+
19
+ initTheme("dark", false);
20
+
21
+ type Handler = (event: unknown, ctx: unknown) => unknown | Promise<unknown>;
22
+ type Command = { handler(args: string, ctx: unknown): Promise<void> };
23
+ type SettingsComponent = { render(width: number): string[]; handleInput(data: string): void };
24
+
25
+ function config(enabled: boolean): PolishedTuiConfig {
26
+ const value = structuredClone(defaultConfig);
27
+ value.projectRefreshIntervalMs = 0;
28
+ value.components.editor.enabled = false;
29
+ value.components.userMessages.enabled = false;
30
+ value.components.selectorBorders.enabled = false;
31
+ value.components.footer.style = "native";
32
+ value.components.workingLine.enabled = false;
33
+ value.components.thinkingSteps = { enabled, mode: "rail" };
34
+ return value;
35
+ }
36
+
37
+ function theme(): Theme {
38
+ return {
39
+ fg: (_color: string, text: string) => text,
40
+ bold: (text: string) => text,
41
+ italic: (text: string) => text,
42
+ underline: (text: string) => text,
43
+ strikethrough: (text: string) => text,
44
+ getThinkingBorderColor: () => (text: string) => text,
45
+ } as unknown as Theme;
46
+ }
47
+
48
+ function context() {
49
+ let editor: unknown;
50
+ let settings: SettingsComponent | undefined;
51
+ const ctx = {
52
+ mode: "tui",
53
+ hasUI: true,
54
+ cwd: process.cwd(),
55
+ ui: {
56
+ theme: theme(),
57
+ setFooter() {},
58
+ setEditorComponent(value: unknown) {
59
+ editor = value;
60
+ },
61
+ getEditorComponent: () => editor,
62
+ getEditorText: () => "",
63
+ setEditorText() {},
64
+ onTerminalInput: () => () => {},
65
+ notify() {},
66
+ async custom(factory: (...args: unknown[]) => unknown) {
67
+ settings = factory({ requestRender() {} }, theme(), {}, () => {}) as SettingsComponent;
68
+ },
69
+ },
70
+ sessionManager: {
71
+ getBranch: () => [],
72
+ getEntries: () => [],
73
+ getSessionName: () => undefined,
74
+ },
75
+ getContextUsage: () => undefined,
76
+ };
77
+ return { ctx, settings: () => settings };
78
+ }
79
+
80
+ async function emit(handlers: Map<string, Handler[]>, event: string, ctx: unknown) {
81
+ for (const handler of handlers.get(event) ?? []) await handler({}, ctx);
82
+ }
83
+
84
+ describe("session config loading", () => {
85
+ it("does not read at registration, loads exactly once per start, and observes external changes", async () => {
86
+ mocks.loadConfig.mockClear();
87
+ mocks.config = config(false);
88
+ const handlers = new Map<string, Handler[]>();
89
+ let command: Command | undefined;
90
+ zentui({
91
+ on(event: string, handler: Handler) {
92
+ handlers.set(event, [...(handlers.get(event) ?? []), handler]);
93
+ },
94
+ registerCommand(name: string, value: Command) {
95
+ if (name === "zentui") command = value;
96
+ },
97
+ registerEntryRenderer() {},
98
+ getThinkingLevel: () => "off",
99
+ } as never);
100
+ expect(mocks.loadConfig).not.toHaveBeenCalled();
101
+
102
+ const harness = context();
103
+ const native = AssistantMessageComponent.prototype.updateContent;
104
+ await emit(handlers, "session_start", harness.ctx);
105
+ expect(mocks.loadConfig).toHaveBeenCalledTimes(1);
106
+ expect(AssistantMessageComponent.prototype.updateContent).toBe(native);
107
+ await emit(handlers, "session_shutdown", harness.ctx);
108
+
109
+ mocks.config = config(true);
110
+ await emit(handlers, "session_start", harness.ctx);
111
+ expect(mocks.loadConfig).toHaveBeenCalledTimes(2);
112
+ expect(AssistantMessageComponent.prototype.updateContent).not.toBe(native);
113
+ expect(command).toBeDefined();
114
+ await command?.handler("", harness.ctx);
115
+ const settingsComponent = harness.settings();
116
+ for (let index = 0; index < 3; index += 1) settingsComponent?.handleInput("\t");
117
+ const settings = settingsComponent?.render(160).join("\n") ?? "";
118
+ expect(settings).toContain("Thinking (Experimental)");
119
+ expect(settings).toMatch(/Enabled.*enabled/);
120
+ expect(settings).toMatch(/Mode.*Rail/);
121
+ await emit(handlers, "session_shutdown", harness.ctx);
122
+ expect(AssistantMessageComponent.prototype.updateContent).toBe(native);
123
+ });
124
+ });
@@ -52,6 +52,7 @@ import {
52
52
  saveSelectorBordersComponentPatch,
53
53
  saveSeparatorPatch,
54
54
  saveStarshipFooterStylePatch,
55
+ saveThinkingStepsComponentPatch,
55
56
  saveUiFeaturesPatch,
56
57
  saveUserMessagesComponentPatch,
57
58
  saveWorkingLineComponentPatch,
@@ -132,6 +133,7 @@ describe("canonical config resolution", () => {
132
133
  colorSource: "theme",
133
134
  styles: { framed: {}, "framed-copy-friendly": {}, compact: {}, labeled: {} },
134
135
  },
136
+ thinkingSteps: { enabled: false, mode: "tree" },
135
137
  workingLine: {
136
138
  enabled: true,
137
139
  turnSummary: true,
@@ -569,6 +571,84 @@ describe("canonical config resolution", () => {
569
571
  });
570
572
  });
571
573
 
574
+ describe("thinking-steps config", () => {
575
+ it("defaults independently and normalizes invalid canonical leaves", () => {
576
+ const first = mergeConfig({});
577
+ const second = mergeConfig({});
578
+ expect(first.components.thinkingSteps).toEqual({ enabled: false, mode: "tree" });
579
+ expect(first.components.thinkingSteps).not.toBe(second.components.thinkingSteps);
580
+ expect(
581
+ mergeConfig({ components: { thinkingSteps: { enabled: true, mode: "rail" } } }).components
582
+ .thinkingSteps,
583
+ ).toEqual({ enabled: true, mode: "rail" });
584
+ expect(
585
+ mergeConfig({
586
+ components: {
587
+ thinkingSteps: { enabled: true, mode: "streaming" },
588
+ },
589
+ }).components.thinkingSteps,
590
+ ).toEqual({ enabled: true, mode: "streaming" });
591
+ expect(
592
+ mergeConfig({
593
+ components: {
594
+ thinkingSteps: { enabled: true, mode: "streaming-experimental" },
595
+ },
596
+ }).components.thinkingSteps,
597
+ ).toEqual({ enabled: true, mode: "streaming" });
598
+ for (const mode of [undefined, "native", "Rail"]) {
599
+ expect(
600
+ mergeConfig({ components: { thinkingSteps: { enabled: "yes", mode } } }).components
601
+ .thinkingSteps,
602
+ ).toEqual({ enabled: false, mode: "tree" });
603
+ }
604
+ });
605
+
606
+ it("persists atomic component patches and preserves unknown keys", () => {
607
+ withConfig(
608
+ {
609
+ futureTop: { keep: true },
610
+ components: { thinkingSteps: { future: { keep: true } } },
611
+ },
612
+ (path) => {
613
+ const saved = saveThinkingStepsComponentPatch({ enabled: true, mode: "rail" }, path);
614
+ const raw = readRaw(path);
615
+ expect(saved.components.thinkingSteps).toEqual({ enabled: true, mode: "rail" });
616
+ expect(raw.components.thinkingSteps).toMatchObject({
617
+ enabled: true,
618
+ mode: "rail",
619
+ future: { keep: true },
620
+ });
621
+ expect(raw.futureTop).toEqual({ keep: true });
622
+ expect(configTempFiles(join(path, ".."))).toEqual([]);
623
+ },
624
+ );
625
+ });
626
+
627
+ it("normalizes the one-window Streaming migration alias on the next save", () => {
628
+ withConfig(
629
+ { components: { thinkingSteps: { enabled: false, mode: "streaming-experimental" } } },
630
+ (path) => {
631
+ const saved = saveThinkingStepsComponentPatch({ enabled: true }, path);
632
+ expect(saved.components.thinkingSteps).toEqual({ enabled: true, mode: "streaming" });
633
+ expect(readRaw(path).components.thinkingSteps).toMatchObject({
634
+ enabled: true,
635
+ mode: "streaming",
636
+ });
637
+ },
638
+ );
639
+ });
640
+
641
+ it("refuses to overwrite corrupt files", () => {
642
+ withConfig(undefined, (path) => {
643
+ writeFileSync(path, "{broken");
644
+ expect(() => saveThinkingStepsComponentPatch({ enabled: true }, path)).toThrow(
645
+ /Refusing to save Zentui config/,
646
+ );
647
+ expect(readFileSync(path, "utf8")).toBe("{broken");
648
+ });
649
+ });
650
+ });
651
+
572
652
  describe("working-line config", () => {
573
653
  it("defaults to cloned custom presets and migrates legacy modes without losing values", () => {
574
654
  const first = mergeConfig({}).components.workingLine.messages;
@@ -1387,6 +1467,7 @@ describe("compatibility saver recipes", () => {
1387
1467
  "editor",
1388
1468
  "footer",
1389
1469
  "selectorBorders",
1470
+ "thinkingSteps",
1390
1471
  "userMessages",
1391
1472
  "workingLine",
1392
1473
  ]);
@@ -1758,8 +1839,7 @@ describe("mergeConfig", () => {
1758
1839
  mergeConfig({
1759
1840
  components: { footer: { styles: { starship: { pathDisplay: { mode: "repository" } } } } },
1760
1841
  }).components.footer.styles.starship.pathDisplay,
1761
- ).toEqual({ mode: "repository", depth: 2 });
1762
- expect(mergeConfig({ pathDisplay: { mode: "fish", depth: -3 } }).pathDisplay).toEqual({
1842
+ ).toEqual({ mode: "repository", depth: 2 }); expect(mergeConfig({ pathDisplay: { mode: "fish", depth: -3 } }).pathDisplay).toEqual({
1763
1843
  mode: "basename",
1764
1844
  depth: 2,
1765
1845
  });
@@ -976,7 +976,7 @@ describe("same-render autocomplete capture", () => {
976
976
  });
977
977
 
978
978
  describe("accent rail editor integration", () => {
979
- it("decorates trusted wrapped input and autocomplete rows", () => {
979
+ it("decorates trusted wrapped input and preserves multi-row autocomplete output", () => {
980
980
  const editor = wrapped(baseEditor({ autocomplete: ["one", "two"] }), {
981
981
  style: "accent-rail",
982
982
  });
@@ -987,6 +987,17 @@ describe("accent rail editor integration", () => {
987
987
  expect(lines.slice(1).map((line) => line.trimEnd())).toEqual(["one", "two"]);
988
988
  });
989
989
 
990
+ it("places one reserved row on each side of a one-row rail", () => {
991
+ const editor = wrapped(baseEditor({}), { style: "accent-rail" });
992
+ const lines = editor.render(24);
993
+
994
+ expect(lines).toHaveLength(3);
995
+ expect(lines[0]).toBe("");
996
+ expect(lines[1]).toMatch(/^▎ typed text/);
997
+ expect(lines[2]).toBe("");
998
+ expect([...lines, "footer"].slice(-3)).toEqual([lines[1], "", "footer"]);
999
+ });
1000
+
990
1001
  it("fails open from the reduced same-render rows for untrusted third-party output", () => {
991
1002
  const base = baseEditor({ malformedTop: "third-party header" });
992
1003
  const editor = wrapped(base, { style: "accent-rail" });
@@ -995,9 +1006,11 @@ describe("accent rail editor integration", () => {
995
1006
  expect(lines.join("\n")).not.toContain("▎");
996
1007
  });
997
1008
 
998
- it("renders the standalone editor with the same accent rail", () => {
1009
+ it("renders the standalone editor with the same reserved rows and accent rail", () => {
999
1010
  const lines = standalone("accent-rail").render(24);
1000
- expect(lines.some((line) => line.startsWith("▎ "))).toBe(true);
1011
+ expect(lines[0]).toBe("");
1012
+ expect(lines[1]).toMatch(/^▎ /);
1013
+ expect(lines[2]).toBe("");
1001
1014
  expect(lines.every((line) => visibleWidth(line) <= 24)).toBe(true);
1002
1015
  });
1003
1016
  });
@@ -1063,7 +1076,9 @@ describe("minimalist editor integration", () => {
1063
1076
  expect(rawWidths).toEqual([36, 38, 36, 34, 36]);
1064
1077
  expect(polished.join("\n").match(/provider/g)).toHaveLength(1);
1065
1078
  expect(lowRail.join("\n").match(/provider/g)).toHaveLength(1);
1066
- expect(accentRail[0]).toMatch(/^▎ typed text/);
1079
+ expect(accentRail[0]).toBe("");
1080
+ expect(accentRail[1]).toMatch(/^▎ typed text/);
1081
+ expect(accentRail[2]).toBe("");
1067
1082
  expect(accentRail.join("\n")).not.toContain("provider");
1068
1083
  expect(minimalist[0]).toMatch(/^╭.*╮$/);
1069
1084
  expect(minimalist.at(-1)).toMatch(/^╰.*╯$/);