@janvitos/pi-plan-build 0.1.48 → 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 +12 -9
- package/index.ts +100 -93
- package/package.json +2 -2
- package/utils.ts +5 -16
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
|
-
-
|
|
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. 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.
|
|
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
|
|
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
|
|
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
|
|
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"
|
|
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
|
|
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 (
|
|
167
|
-
panelLayoutRoot
|
|
168
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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": {
|
package/utils.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import 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
|
|
80
|
-
|
|
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
|
|
88
|
-
|
|
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 {
|