@janvitos/pi-plan-build 0.1.47 → 0.1.49

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.
package/README.md CHANGED
@@ -25,8 +25,9 @@ Optionally keep the approved plan visible in a docked side panel while implement
25
25
  ## Features
26
26
 
27
27
  - New sessions start in **Build** mode.
28
- - Bare `Tab` cycles **Build → Plan → Build**, while active autocomplete dropdowns retain Pi's normal Tab completion.
29
- - The composer uses OpenCode prompt-inspired blue/orange mode colors on the rounded top-left border and left rail, complemented by Pi's border color on the right rail and rounded bottom-right border; rounded corners inherit their vertical rail colors while horizontal `╌` segments bridge the borders at both junctions, paired with a light vertical `┆` at the top right and a mode-specific bottom-left transition: thin `┆` in Plan and heavy `┇` in Build. User messages carry a heavy dashed `┇` transcript rail in the mode used to submit them—using the active Pi theme's `warning` color in Plan and `thinkingLow` color in Buildand retain that color after mode changes and session restores. It also includes a mode/model/thinking metadata row; cycling the thinking level updates this row without adding a duplicate status above the composer. The model is shown as `model-id [provider]` (for example, `gpt-5.6-luna [openai]`), with the model ID inheriting the terminal foreground like unselected entries in `/model` and the provider using the footer's dim text color. The footer keeps the remaining path and usage stats without duplicating model metadata.
28
+ - `Ctrl+Tab` cycles **Build → Plan → Build** in every TUI setup. With Pi Plan Build's custom composer active, bare `Tab` also cycles modes while active autocomplete dropdowns retain Pi's normal Tab completion.
29
+ - The custom composer uses OpenCode prompt-inspired blue/orange mode colors on the rounded top-left border and left rail, complemented by Pi's border color on the right rail and rounded bottom-right border; rounded corners inherit their vertical rail colors while horizontal `╌` segments bridge the borders at both junctions, paired with a light vertical `┆` at the top right and a mode-specific bottom-left transition: thin `┆` in Plan and heavy `┇` in Build. The active composer and submitted user messages use a continuous solid thin `│` left rail in both modes. These rails use the active Pi theme's `warning` color in Plan and `thinkingLow` color in Build, and submitted messages retain their original mode after mode changes and session restores. Its metadata row shows mode, model, provider, and thinking level; cycling the thinking level updates that row directly.
30
+ - Pi Plan Build leaves the footer untouched; Pi or another installed extension remains responsible for path, usage, model, provider, thinking, and extension-status information.
30
31
  - `/plan`, `/build`, and the `--plan` startup flag.
31
32
  - Per-session plans at `~/.pi/agent/plans/<session-id>.md`.
32
33
  - In Plan mode, built-in `edit` and `write` are restricted to the exact plan file.
@@ -37,10 +38,11 @@ Optionally keep the approved plan visible in a docked side panel while implement
37
38
  - **Switch to Build and implement here**
38
39
  - **Start fresh and implement**
39
40
  - **Stay in Plan mode**
40
- - **Experimental:** In fullscreen TUI, valid checklist plans also offer **Implement step by step**: a passive, non-overlapping docked right panel keeps the plan visible while natural-language prompts gate steps and report completed work. The panel is visual-only and never captures keyboard input. This feature is still under active development.
41
+ - **Experimental:** In fullscreen TUI, valid checklist plans also offer **Implement step by step** when Pi Plan Build owns the optional fullscreen layout: a passive, non-overlapping docked right panel keeps the plan visible while natural-language prompts gate steps and report completed work. The panel is visual-only and never captures keyboard input. This feature is still under active development.
42
+ - Compatible editor decorators can wrap Pi Plan Build's editor without disabling its composer. If another extension replaces rather than invokes that editor, already owns the editor before Plan Build starts, or replaces the fullscreen layout, Plan Build automatically uses reduced UI: it keeps the core Plan/Build workflow and mode status but does not replace that editor or offer a new step-by-step panel.
41
43
  - Staying in Plan mode—or pressing Escape in the approval dialog—produces a durable acknowledgement and stops the run until the user responds.
42
44
  - Mode state survives reloads, resumes, and forks.
43
- - When Pi recreates the custom editor, the latest 100 user prompts from the active session branch are restored for Up/Down history navigation.
45
+ - When Pi Plan Build's custom editor is active and Pi recreates it, the latest 100 user prompts from the active session branch are restored for Up/Down history navigation.
44
46
 
45
47
  ## Requirements
46
48
 
@@ -78,7 +80,8 @@ Do not install more than one npm, Git, or local copy at the same time; duplicate
78
80
 
79
81
  | Action | Result |
80
82
  | --- | --- |
81
- | `Tab` | Cycle Build and Plan, or accept an active autocomplete selection |
83
+ | `Ctrl+Tab` | Cycle Build and Plan in any TUI setup |
84
+ | `Tab` | With the custom composer active, cycle modes or accept an active autocomplete selection |
82
85
  | `/plan` | Select Plan mode |
83
86
  | `/build` | Select Build mode |
84
87
  | `pi --plan` | Start a new session in Plan mode |
@@ -93,7 +96,7 @@ When planning is complete, `plan_exit` displays the entire persisted plan and as
93
96
  1. implement in the current session;
94
97
  2. start a clean linked implementation session;
95
98
  3. stay in Plan mode; or
96
- 4. **experimentally implement step by step** in fullscreen TUI when the plan contains a valid checklist.
99
+ 4. **experimentally implement step by step** in fullscreen TUI when the plan contains a valid checklist and no other extension owns the optional editor/layout UI.
97
100
 
98
101
  Selecting **Start fresh and implement** stops the current run and automatically dispatches `/build-fresh`. Pi 0.84.2 or newer is required for extension command dispatch from an injected user message. The command creates a linked child session, copies the approved plan to its canonical plan file, preserves the model and thinking level selected for the action, switches it to Build, and starts implementation without transferring the planning conversation.
99
102
 
@@ -107,7 +110,7 @@ Both actions leave Plan mode active, stop the agent, and wait for the next user
107
110
 
108
111
  > **Experimental feature:** Step-by-step execution is still being developed. Expect UI and workflow changes, and please report issues or unexpected behavior.
109
112
 
110
- When Pi uses `"tuiMode": "fullscreen"` and the saved plan contains top-level `- [ ]` items under `## Implementation Steps`, `plan_exit` offers the additional **Implement step by step** approval action. This is opt-in per plan; it does not replace either one-shot implementation option or **Stay in Plan mode**.
113
+ When Pi uses `"tuiMode": "fullscreen"`, Pi Plan Build still owns its custom editor/layout UI, and the saved plan contains top-level `- [ ]` items under `## Implementation Steps`, `plan_exit` offers the additional **Implement step by step** approval action. This is opt-in per plan; it does not replace either one-shot implementation option or **Stay in Plan mode**.
111
114
 
112
115
  The passive 64-column right panel reserves terminal columns, so the transcript and editor reflow instead of being covered. Long step instructions and panel guidance wrap instead of being clipped. It never accepts focus or keyboard input and collapses below 132 terminal columns. The panel is a visual status aid only; all control happens through ordinary prompts. Its guidance remains visible, and the main transcript says, “Awaiting your instructions.” The agent interprets intent contextually, so these are examples rather than required commands:
113
116
 
@@ -127,7 +130,7 @@ Enable fullscreen in `~/.pi/agent/settings.json` and restart Pi:
127
130
  }
128
131
  ```
129
132
 
130
- The integration uses Pi 0.84.2's public fullscreen layout primitives plus a guarded read of its runtime layout root because the current extension API exposes `setLayoutRoot()` but not a corresponding getter.
133
+ The integration uses Pi 0.84.2's public fullscreen layout primitives plus a guarded read of its runtime layout root because the current extension API exposes `setLayoutRoot()` but not a corresponding getter. Before installing or removing the panel, Pi Plan Build verifies that it still owns the relevant layout slot. For editor composition, it recognizes a later factory as compatible when that factory invokes Plan Build's editor factory; decorators such as global-history wrappers therefore retain the full composer. A non-composing editor replacement or conflicting layout shows one warning, reports the current mode through Pi's keyed extension status, and disables only the custom composer and new step-by-step panel. Existing restored step progress is retained for prompt-based completion or cancellation.
131
134
 
132
135
  ## Plan-mode permissions
133
136
 
@@ -159,7 +162,7 @@ npm test
159
162
  npm pack --dry-run
160
163
  ```
161
164
 
162
- The tests cover state decoding, safe plan paths, mutation restrictions, deferred transitions, mode and provider rendering, conversational Plan guidance, session-based prompt history restoration, complete plan rendering, approval decisions, stop behavior, fresh-session settings and handoff content, question formatting and cancellation, structured checklist parsing, step state transitions, safe instruction revisions, and responsive panel rendering.
165
+ The tests cover state decoding, safe plan paths, mutation restrictions, deferred transitions, mode/provider/thinking rendering and cycling, optional-UI ownership decisions, conversational Plan guidance, session-based prompt history restoration, complete plan rendering, approval decisions, stop behavior, fresh-session settings and handoff content, question formatting and cancellation, structured checklist parsing, step state transitions, safe instruction revisions, and responsive panel rendering.
163
166
 
164
167
  ### Publishing
165
168
 
package/index.ts CHANGED
@@ -40,15 +40,14 @@ import {
40
40
  type FreshImplementationRequest,
41
41
  decodeModeState,
42
42
  extractPromptHistory,
43
- formatFooterCwd,
44
43
  formatModeMetadata,
45
44
  formatModeRail,
46
45
  formatModeTopBorder,
47
- formatTokens,
48
46
  isAllowedPlanMutation,
49
47
  makePlanPath,
50
48
  nextMode,
51
49
  nextThinkingLevel,
50
+ ownsUiSlot,
52
51
  normalizePlanExitChoice,
53
52
  PLAN_EXIT_APPROVE_CHOICE,
54
53
  PLAN_EXIT_FRESH_CHOICE,
@@ -56,6 +55,7 @@ import {
56
55
  PLAN_EXIT_STAY_CHOICE,
57
56
  PLAN_STEP_READY_ACKNOWLEDGEMENT,
58
57
  renderModeComposer,
58
+ shouldReduceOptionalUi,
59
59
  type Mode,
60
60
  unique,
61
61
  } from "./utils.ts";
@@ -76,6 +76,7 @@ const MODE_ADDED_TOOLS = new Set([...MANAGED_TOOLS, "edit", "write"]);
76
76
  const EMPTY_PARAMETERS = Type.Object({});
77
77
 
78
78
  type PendingReminder = "plan" | "build" | undefined;
79
+ type EditorFactory = NonNullable<ReturnType<ExtensionContext["ui"]["getEditorComponent"]>>;
79
80
  interface StoredState {
80
81
  version: 1;
81
82
  selectedMode: Mode;
@@ -105,7 +106,12 @@ export default function planBuildModes(pi: ExtensionAPI): void {
105
106
  let panelTui: (TUI & Partial<ViewportTUI>) | undefined;
106
107
  let originalLayoutRoot: Component | undefined;
107
108
  let panelLayoutRoot: Component | undefined;
109
+ let panelLayoutToken: { enabled: boolean } | undefined;
110
+ let installedEditorFactory: EditorFactory | undefined;
111
+ let composerMountingEditorFactory: EditorFactory | undefined;
108
112
  let fullscreenPanelCapable = false;
113
+ let reducedOptionalUi = false;
114
+ let reducedUiNoticeShown = false;
109
115
  const displayUserMessageText = (text: string): string | undefined => {
110
116
  const skillBlock = parseSkillBlock(text);
111
117
  return skillBlock ? skillBlock.userMessage || undefined : text || undefined;
@@ -162,13 +168,61 @@ export default function planBuildModes(pi: ExtensionAPI): void {
162
168
  panelTui?.requestRender();
163
169
  }
164
170
 
171
+ function currentLayoutRoot(): Component | undefined {
172
+ return (panelTui as (TUI & { layoutRoot?: Component }) | undefined)?.layoutRoot;
173
+ }
174
+
165
175
  function removePanelLayout(): void {
166
- if (panelLayoutRoot && panelTui && originalLayoutRoot) panelTui.setLayoutRoot?.(originalLayoutRoot);
167
- panelLayoutRoot = undefined;
168
- panel = undefined;
176
+ if (panelLayoutToken) panelLayoutToken.enabled = false;
177
+ if (!panelLayoutRoot) {
178
+ panel = undefined;
179
+ panelLayoutToken = undefined;
180
+ return;
181
+ }
182
+ if (panelTui && originalLayoutRoot && ownsUiSlot(currentLayoutRoot(), panelLayoutRoot)) {
183
+ panelTui.setLayoutRoot?.(originalLayoutRoot);
184
+ panelLayoutRoot = undefined;
185
+ panel = undefined;
186
+ panelLayoutToken = undefined;
187
+ }
169
188
  panelTui?.requestRender();
170
189
  }
171
190
 
191
+ function setReducedModeStatus(ctx: ExtensionContext): void {
192
+ ctx.ui.setStatus(STATUS_KEY, formatModeRail(selectedMode, ctx.ui.theme, ctx.ui.theme.bold(selectedMode)));
193
+ }
194
+
195
+ function enterReducedOptionalUi(ctx: ExtensionContext): void {
196
+ if (!reducedOptionalUi) {
197
+ reducedOptionalUi = true;
198
+ removePanelLayout();
199
+ fullscreenPanelCapable = false;
200
+ }
201
+ setReducedModeStatus(ctx);
202
+ if (!reducedUiNoticeShown) {
203
+ reducedUiNoticeShown = true;
204
+ ctx.ui.notify(
205
+ "Another extension owns Pi's custom editor or fullscreen layout. Pi Plan Build disabled its custom composer and experimental step-by-step panel; Plan and Build workflows remain available through Ctrl+Tab, /plan, and /build.",
206
+ "warning",
207
+ );
208
+ }
209
+ }
210
+
211
+ function detectOptionalUiConflict(ctx: ExtensionContext): boolean {
212
+ if (reducedOptionalUi) return true;
213
+ const editorConflict = shouldReduceOptionalUi(
214
+ ctx.ui.getEditorComponent(),
215
+ composerMountingEditorFactory ?? installedEditorFactory,
216
+ );
217
+ const expectedRoot = panelLayoutRoot ?? originalLayoutRoot;
218
+ const layoutConflict = expectedRoot !== undefined && !ownsUiSlot(currentLayoutRoot(), expectedRoot);
219
+ if (editorConflict || layoutConflict) {
220
+ enterReducedOptionalUi(ctx);
221
+ return true;
222
+ }
223
+ return false;
224
+ }
225
+
172
226
  function cancelPlanExecution(): void {
173
227
  execution = undefined;
174
228
  removePanelLayout();
@@ -191,9 +245,17 @@ export default function planBuildModes(pi: ExtensionAPI): void {
191
245
 
192
246
  function ensurePanelLayout(): boolean {
193
247
  if (!execution || !fullscreenPanelCapable || !panelTui || !originalLayoutRoot || !currentContext) return false;
248
+ if (detectOptionalUiConflict(currentContext)) return false;
249
+ const expectedRoot = panelLayoutRoot ?? originalLayoutRoot;
250
+ if (!ownsUiSlot(currentLayoutRoot(), expectedRoot)) {
251
+ enterReducedOptionalUi(currentContext);
252
+ return false;
253
+ }
194
254
  if (!panel) panel = new PlanPanel(execution, currentContext.ui.theme);
195
255
  else panel.setState(execution);
196
256
  if (!panelLayoutRoot) {
257
+ const layoutToken = { enabled: true };
258
+ panelLayoutToken = layoutToken;
197
259
  panelLayoutRoot = new HStack([
198
260
  { component: originalLayoutRoot, basis: 0, grow: 1, shrink: 1, minSize: 58 },
199
261
  {
@@ -203,7 +265,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
203
265
  shrink: 0,
204
266
  minSize: PANEL_WIDTH,
205
267
  maxSize: PANEL_WIDTH,
206
- visible: (viewport) => execution?.panelVisible !== false && viewport.width >= PANEL_MIN_TERMINAL_WIDTH,
268
+ visible: (viewport) => layoutToken.enabled && !reducedOptionalUi && execution !== undefined && execution.panelVisible !== false && viewport.width >= PANEL_MIN_TERMINAL_WIDTH,
207
269
  },
208
270
  ]);
209
271
  panelTui.setLayoutRoot?.(panelLayoutRoot);
@@ -213,7 +275,10 @@ export default function planBuildModes(pi: ExtensionAPI): void {
213
275
  }
214
276
 
215
277
  function updateModeIndicator(ctx: ExtensionContext): void {
216
- // Clear the legacy footer status; the mode is now rendered inside the editor.
278
+ if (detectOptionalUiConflict(ctx)) {
279
+ setReducedModeStatus(ctx);
280
+ return;
281
+ }
217
282
  ctx.ui.setStatus(STATUS_KEY, undefined);
218
283
  requestEditorRender?.();
219
284
  }
@@ -278,6 +343,10 @@ export default function planBuildModes(pi: ExtensionAPI): void {
278
343
  description: "Switch to Build mode",
279
344
  handler: async (_args, ctx) => selectMode("build", ctx, "manual"),
280
345
  });
346
+ pi.registerShortcut("ctrl+tab", {
347
+ description: "Cycle Plan and Build modes",
348
+ handler: async (ctx) => selectMode(nextMode(selectedMode), ctx, "manual"),
349
+ });
281
350
  pi.registerCommand("build-fresh", {
282
351
  description: "Start a clean linked session and implement the plan selected in plan_exit",
283
352
  handler: async (_args, ctx) => {
@@ -446,6 +515,9 @@ export default function planBuildModes(pi: ExtensionAPI): void {
446
515
  return finish("Step-by-step execution was cancelled. The panel and execution guards were removed; the saved plan file remains available.");
447
516
  }
448
517
  if (params.action === "hide" || params.action === "show") {
518
+ if (params.action === "show" && reducedOptionalUi) {
519
+ throw new Error("The visual plan panel is disabled because another extension owns Pi's optional editor or fullscreen layout UI");
520
+ }
449
521
  updateExecution({ ...execution, panelVisible: params.action === "show" });
450
522
  if (params.action === "show") ensurePanelLayout();
451
523
  return finish(`The visual plan panel is now ${params.action === "show" ? "visible" : "hidden"}. Progress is unchanged.`);
@@ -548,9 +620,10 @@ export default function planBuildModes(pi: ExtensionAPI): void {
548
620
  if (!plan.trim()) throw new Error("Cannot request plan approval because the plan file is empty");
549
621
  pi.appendEntry(PLAN_REVIEW_ENTRY_TYPE, { plan, planPath });
550
622
  const displayPath = shorten(planPath, ctx.cwd);
623
+ detectOptionalUiConflict(ctx);
551
624
  let stepExecution: PlanExecutionState | undefined;
552
625
  let checklistError: string | undefined;
553
- const panelAvailable = fullscreenPanelCapable && (panelTui?.terminal.columns ?? 0) >= PANEL_MIN_TERMINAL_WIDTH;
626
+ const panelAvailable = !reducedOptionalUi && fullscreenPanelCapable && (panelTui?.terminal.columns ?? 0) >= PANEL_MIN_TERMINAL_WIDTH;
554
627
  if (panelAvailable) {
555
628
  try {
556
629
  stepExecution = createPlanExecution(plan);
@@ -655,6 +728,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
655
728
  });
656
729
 
657
730
  pi.on("before_agent_start", async (_event, ctx) => {
731
+ detectOptionalUiConflict(ctx);
658
732
  runMode = selectedMode;
659
733
  applyTools(runMode);
660
734
  let content: string | undefined;
@@ -692,6 +766,10 @@ export default function planBuildModes(pi: ExtensionAPI): void {
692
766
  pi.on("session_start", async (event, ctx) => {
693
767
  userMessageRail.activate();
694
768
  currentContext = ctx;
769
+ installedEditorFactory = undefined;
770
+ composerMountingEditorFactory = undefined;
771
+ reducedOptionalUi = false;
772
+ reducedUiNoticeShown = false;
695
773
  const entries = ctx.sessionManager.getEntries();
696
774
  const latest = entries
697
775
  .filter(
@@ -715,85 +793,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
715
793
  applyTools(selectedMode);
716
794
  updateModeIndicator(ctx);
717
795
 
718
- if (ctx.mode === "tui") {
719
- ctx.ui.setFooter((tui, theme, footerData) => {
720
- const unsubscribe = footerData.onBranchChange(() => tui.requestRender());
721
- return {
722
- dispose: unsubscribe,
723
- invalidate() {},
724
- render(width: number): string[] {
725
- let input = 0;
726
- let output = 0;
727
- let cacheRead = 0;
728
- let cacheWrite = 0;
729
- let cost = 0;
730
- let latestCacheHitRate: number | undefined;
731
- for (const entry of ctx.sessionManager.getEntries()) {
732
- const usage =
733
- entry.type === "message" &&
734
- (entry.message.role === "assistant" || entry.message.role === "toolResult")
735
- ? entry.message.usage
736
- : (entry.type === "branch_summary" || entry.type === "compaction")
737
- ? entry.usage
738
- : undefined;
739
- if (!usage) continue;
740
- input += usage.input;
741
- output += usage.output;
742
- cacheRead += usage.cacheRead;
743
- cacheWrite += usage.cacheWrite;
744
- cost += usage.cost.total;
745
- if (entry.type === "message" && entry.message.role === "assistant") {
746
- const promptTokens = usage.input + usage.cacheRead + usage.cacheWrite;
747
- latestCacheHitRate = promptTokens > 0 ? (usage.cacheRead / promptTokens) * 100 : undefined;
748
- }
749
- }
750
-
751
- let cwd = formatFooterCwd(ctx.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);
752
- const branch = footerData.getGitBranch();
753
- if (branch) cwd += ` (${branch})`;
754
- const sessionName = ctx.sessionManager.getSessionName();
755
- if (sessionName) cwd += ` • ${sessionName}`;
756
-
757
- const stats: string[] = [];
758
- if (input) stats.push(`↑${formatTokens(input)}`);
759
- if (output) stats.push(`↓${formatTokens(output)}`);
760
- if (cacheRead) stats.push(`R${formatTokens(cacheRead)}`);
761
- if (cacheWrite) stats.push(`W${formatTokens(cacheWrite)}`);
762
- if ((cacheRead || cacheWrite) && latestCacheHitRate !== undefined) {
763
- stats.push(`CH${latestCacheHitRate.toFixed(1)}%`);
764
- }
765
- const usingSubscription = ctx.model
766
- ? ctx.model.provider === "kimi-coding" || ctx.modelRegistry.isUsingOAuth(ctx.model)
767
- : false;
768
- if (cost || usingSubscription) {
769
- stats.push(`$${cost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`);
770
- }
771
-
772
- const contextUsage = ctx.getContextUsage();
773
- const contextWindow = contextUsage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
774
- const contextPercent = contextUsage?.percent;
775
- const contextText = `${contextPercent === null || contextPercent === undefined ? "?" : `${contextPercent.toFixed(1)}%`}/${formatTokens(contextWindow)} (auto)`;
776
- stats.push(
777
- contextPercent !== null && contextPercent !== undefined && contextPercent > 90
778
- ? theme.fg("error", contextText)
779
- : contextPercent !== null && contextPercent !== undefined && contextPercent > 70
780
- ? theme.fg("warning", contextText)
781
- : contextText,
782
- );
783
-
784
- const lines = [
785
- truncateToWidth(theme.fg("dim", cwd), width, theme.fg("dim", "...")),
786
- truncateToWidth(theme.fg("dim", stats.join(" ")), width, theme.fg("dim", "...")),
787
- ];
788
- const statuses = Array.from(footerData.getExtensionStatuses().entries())
789
- .sort(([a], [b]) => a.localeCompare(b))
790
- .map(([, text]) => text.replace(/[\r\n\t]/g, " ").replace(/ +/g, " ").trim());
791
- if (statuses.length) lines.push(truncateToWidth(statuses.join(" "), width, theme.fg("dim", "...")));
792
- return lines;
793
- },
794
- };
795
- });
796
-
796
+ if (ctx.mode === "tui" && !reducedOptionalUi) {
797
797
  // Startup history is populated after session_start; replacement flows recreate the editor after that step.
798
798
  const promptHistory = event.reason === "startup" ? [] : extractPromptHistory(ctx.sessionManager.getBranch());
799
799
  class ModeEditor extends CustomEditor {
@@ -806,6 +806,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
806
806
  }
807
807
 
808
808
  override render(width: number): string[] {
809
+ if (reducedOptionalUi) return super.render(width);
809
810
  const railWidth = 2;
810
811
  const paddingWidth = Math.min(railWidth, Math.max(0, Math.floor((width - 1) / 2)));
811
812
  if (this.getPaddingX() !== railWidth) this.setPaddingX(railWidth);
@@ -845,19 +846,20 @@ export default function planBuildModes(pi: ExtensionAPI): void {
845
846
  }
846
847
 
847
848
  override handleInput(data: string): void {
848
- if (this.matchesThinkingCycle?.(data)) {
849
+ if (!reducedOptionalUi && this.matchesThinkingCycle?.(data)) {
849
850
  if (this.onExtensionShortcut?.(data)) return;
850
851
  this.onCycleThinking?.();
851
852
  return;
852
853
  }
853
- if (matchesKey(data, Key.tab) && !this.isShowingAutocomplete()) {
854
+ if (!reducedOptionalUi && matchesKey(data, Key.tab) && !this.isShowingAutocomplete()) {
854
855
  this.onCycle?.();
855
856
  return;
856
857
  }
857
858
  super.handleInput(data);
858
859
  }
859
860
  }
860
- ctx.ui.setEditorComponent((tui, theme, keybindings) => {
861
+ installedEditorFactory = (tui, theme, keybindings) => {
862
+ composerMountingEditorFactory = ctx.ui.getEditorComponent() ?? installedEditorFactory;
861
863
  const editor = new ModeEditor(tui, theme, keybindings);
862
864
  for (const prompt of promptHistory) editor.addToHistory(prompt);
863
865
  requestEditorRender = () => editor.requestModeRender();
@@ -881,7 +883,8 @@ export default function planBuildModes(pi: ExtensionAPI): void {
881
883
  editor.requestModeRender();
882
884
  };
883
885
  return editor;
884
- });
886
+ };
887
+ ctx.ui.setEditorComponent(installedEditorFactory);
885
888
  if (execution && !fullscreenPanelCapable) {
886
889
  ctx.ui.notify("Step-by-step progress was restored, but its plan panel requires fullscreen TUI mode. Progress is preserved.", "warning");
887
890
  }
@@ -892,14 +895,18 @@ export default function planBuildModes(pi: ExtensionAPI): void {
892
895
  userMessageRail.deactivate();
893
896
  removePanelLayout();
894
897
  ctx.ui.setStatus(STATUS_KEY, undefined);
895
- ctx.ui.setFooter(undefined);
896
- ctx.ui.setEditorComponent(undefined);
898
+ if (ownsUiSlot(ctx.ui.getEditorComponent(), installedEditorFactory)) ctx.ui.setEditorComponent(undefined);
897
899
  requestEditorRender = undefined;
900
+ installedEditorFactory = undefined;
901
+ composerMountingEditorFactory = undefined;
898
902
  panel = undefined;
899
903
  panelTui = undefined;
900
904
  panelLayoutRoot = undefined;
905
+ panelLayoutToken = undefined;
901
906
  originalLayoutRoot = undefined;
902
907
  fullscreenPanelCapable = false;
908
+ reducedOptionalUi = false;
909
+ reducedUiNoticeShown = false;
903
910
  currentContext = undefined;
904
911
  });
905
912
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@janvitos/pi-plan-build",
3
- "version": "0.1.47",
3
+ "version": "0.1.49",
4
4
  "description": "Plan safely, approve explicitly, then implement here or in a clean session.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,7 +33,7 @@
33
33
  "access": "public"
34
34
  },
35
35
  "scripts": {
36
- "test": "node --experimental-strip-types --test utils.test.ts question-ui.test.ts plan-execution.test.ts plan-panel.test.ts user-message-rail.test.ts",
36
+ "test": "node --experimental-strip-types --test utils.test.ts question-ui.test.ts plan-execution.test.ts plan-panel.test.ts user-message-rail.test.ts ui-compat.test.ts",
37
37
  "prepublishOnly": "npm test"
38
38
  },
39
39
  "peerDependencies": {
@@ -139,7 +139,7 @@ export function installUserMessageRail(
139
139
  resolver: new TranscriptModeResolver(),
140
140
  formatRail: options.formatRail,
141
141
  getFallbackMode: options.getFallbackMode,
142
- glyph: "",
142
+ glyph: "",
143
143
  };
144
144
  globalState[PATCH_KEY] = state;
145
145
  }
@@ -148,7 +148,7 @@ export function installUserMessageRail(
148
148
  state.owner = owner;
149
149
  state.formatRail = options.formatRail;
150
150
  state.getFallbackMode = options.getFallbackMode;
151
- state.glyph = "";
151
+ state.glyph = "";
152
152
  // Reinstall from the preserved original on every extension load. This migrates
153
153
  // already-running processes away from stale decorator code without stacking wrappers.
154
154
  UserMessageComponent.prototype.render = function renderWithModeRail(width: number): string[] {
package/utils.ts CHANGED
@@ -1,4 +1,4 @@
1
- import path, { isAbsolute, relative, resolve, sep } from "node:path";
1
+ import path from "node:path";
2
2
 
3
3
  export type Mode = "build" | "plan";
4
4
 
@@ -76,23 +76,12 @@ export function formatModeMetadata(
76
76
  return `${options?.rail ?? formatModeRail(mode, theme)} ${modeText}${modelText}${theme.fg("dim", thinkingSeparator)}${thinkingColor(thinkingLevel)}`;
77
77
  }
78
78
 
79
- export function formatTokens(count: number): string {
80
- if (count < 1000) return count.toString();
81
- if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
82
- if (count < 1000000) return `${Math.round(count / 1000)}k`;
83
- if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;
84
- return `${Math.round(count / 1000000)}M`;
79
+ export function shouldReduceOptionalUi(currentOwner: unknown, acceptedOwner: unknown): boolean {
80
+ return acceptedOwner === undefined ? currentOwner !== undefined : currentOwner !== acceptedOwner;
85
81
  }
86
82
 
87
- export function formatFooterCwd(cwd: string, home: string | undefined): string {
88
- if (!home) return cwd;
89
- const resolvedCwd = resolve(cwd);
90
- const relativeToHome = relative(resolve(home), resolvedCwd);
91
- const isInsideHome =
92
- relativeToHome === "" ||
93
- (relativeToHome !== ".." && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));
94
- if (!isInsideHome) return cwd;
95
- return relativeToHome === "" ? "~" : `~${sep}${relativeToHome}`;
83
+ export function ownsUiSlot(currentOwner: unknown, installedOwner: unknown): boolean {
84
+ return installedOwner !== undefined && currentOwner === installedOwner;
96
85
  }
97
86
 
98
87
  export interface LineWidthTools {