@janvitos/pi-plan-build 0.1.46 → 0.1.48
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 +2 -2
- package/index.ts +32 -5
- package/package.json +3 -2
- package/user-message-rail.ts +187 -0
- package/utils.ts +23 -14
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ Optionally keep the approved plan visible in a docked side panel while implement
|
|
|
26
26
|
|
|
27
27
|
- New sessions start in **Build** mode.
|
|
28
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. 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.
|
|
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. 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.
|
|
30
30
|
- `/plan`, `/build`, and the `--plan` startup flag.
|
|
31
31
|
- Per-session plans at `~/.pi/agent/plans/<session-id>.md`.
|
|
32
32
|
- In Plan mode, built-in `edit` and `write` are restricted to the exact plan file.
|
|
@@ -148,7 +148,7 @@ Once the request is sufficiently understood and the agent is ready to present th
|
|
|
148
148
|
|
|
149
149
|
## Design and attribution
|
|
150
150
|
|
|
151
|
-
Pi Plan & Build is an independent extension with its own workflow and UI behavior. Its conversational read-only lifecycle follows OpenCode’s standard Plan agent, while persisted finalization and approval are adapted for Pi. Earlier prompt and transition semantics were informed by OpenCode 1.18.16, and clean-session implementation ideas were informed by the former `pi-plan-mode` extension. This project is not affiliated with either project.
|
|
151
|
+
Pi Plan & Build is an independent extension with its own workflow and UI behavior. Its conversational read-only lifecycle follows OpenCode’s standard Plan agent, while persisted finalization and approval are adapted for Pi. Earlier prompt and transition semantics were informed by OpenCode 1.18.16, and clean-session implementation ideas were informed by the former `pi-plan-mode` extension. The mode-colored transcript rail decorates Pi's exported `UserMessageComponent` because Pi does not currently expose a built-in user-message renderer hook; this compatibility layer is guarded against duplicate installation on reload. This project is not affiliated with either project.
|
|
152
152
|
|
|
153
153
|
The Plan workflow uses Pi's native exploration tools directly and does not bundle or require subagents.
|
|
154
154
|
|
package/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { CustomEditor, getAgentDir, getMarkdownTheme, type EntryRenderer, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { CustomEditor, getAgentDir, getMarkdownTheme, parseSkillBlock, type EntryRenderer, type ExtensionAPI, type ExtensionContext, UserMessageComponent } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { HStack, Key, Markdown, matchesKey, Text, truncateToWidth, visibleWidth, isViewportTUI, type Component, type TUI, type ViewportTUI } from "@earendil-works/pi-tui";
|
|
6
6
|
import { Type } from "typebox";
|
|
7
7
|
import { registerQuestionTool } from "./question-ui.ts";
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
type PlanExecutionState,
|
|
29
29
|
} from "./plan-execution.ts";
|
|
30
30
|
import { PlanPanel } from "./plan-panel.ts";
|
|
31
|
+
import { collectTranscriptModeRecords, extractUserMessageText, installUserMessageRail } from "./user-message-rail.ts";
|
|
31
32
|
import {
|
|
32
33
|
applyManualSelection,
|
|
33
34
|
buildFreshImplementationHandoff,
|
|
@@ -105,6 +106,23 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
105
106
|
let originalLayoutRoot: Component | undefined;
|
|
106
107
|
let panelLayoutRoot: Component | undefined;
|
|
107
108
|
let fullscreenPanelCapable = false;
|
|
109
|
+
const displayUserMessageText = (text: string): string | undefined => {
|
|
110
|
+
const skillBlock = parseSkillBlock(text);
|
|
111
|
+
return skillBlock ? skillBlock.userMessage || undefined : text || undefined;
|
|
112
|
+
};
|
|
113
|
+
const userMessageRail = installUserMessageRail(UserMessageComponent, {
|
|
114
|
+
formatRail: (mode, glyph) => currentContext ? formatModeRail(mode, currentContext.ui.theme, glyph) : glyph,
|
|
115
|
+
getFallbackMode: () => runMode ?? selectedMode,
|
|
116
|
+
});
|
|
117
|
+
const restoreUserMessageRails = (entries: readonly unknown[]) => {
|
|
118
|
+
userMessageRail.setTranscript(
|
|
119
|
+
collectTranscriptModeRecords(entries, {
|
|
120
|
+
stateTypes: new Set([STATE_TYPE, LEGACY_STATE_TYPE]),
|
|
121
|
+
decodeState: decodeModeState,
|
|
122
|
+
displayText: displayUserMessageText,
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
};
|
|
108
126
|
|
|
109
127
|
pi.registerFlag("plan", {
|
|
110
128
|
description: "Start in Plan mode",
|
|
@@ -665,7 +683,14 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
665
683
|
if (execution && execution.status !== "completed") ensurePanelLayout();
|
|
666
684
|
});
|
|
667
685
|
|
|
686
|
+
pi.on("message_start", (event) => {
|
|
687
|
+
if (event.message.role !== "user") return;
|
|
688
|
+
const text = displayUserMessageText(extractUserMessageText(event.message.content));
|
|
689
|
+
if (text) userMessageRail.addMessage(text, runMode ?? selectedMode);
|
|
690
|
+
});
|
|
691
|
+
|
|
668
692
|
pi.on("session_start", async (event, ctx) => {
|
|
693
|
+
userMessageRail.activate();
|
|
669
694
|
currentContext = ctx;
|
|
670
695
|
const entries = ctx.sessionManager.getEntries();
|
|
671
696
|
const latest = entries
|
|
@@ -679,6 +704,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
679
704
|
const raw = latest?.data as StoredState | undefined;
|
|
680
705
|
execution = decodePlanExecution(raw?.execution);
|
|
681
706
|
selectedMode = decoded?.selectedMode ?? (pi.getFlag("plan") === true ? "plan" : "build");
|
|
707
|
+
restoreUserMessageRails(ctx.sessionManager.getBranch());
|
|
682
708
|
pendingReminder = raw?.pendingReminder ?? (decoded ? undefined : pi.getFlag("plan") === true ? "plan" : undefined);
|
|
683
709
|
toolsBeforeModes = Array.isArray(raw?.toolsBeforeModes)
|
|
684
710
|
? raw.toolsBeforeModes.filter((name): name is string => typeof name === "string" && !MANAGED_TOOLS.has(name))
|
|
@@ -787,10 +813,10 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
787
813
|
const lines = super.render(width);
|
|
788
814
|
if (paddingWidth !== railWidth) return lines;
|
|
789
815
|
|
|
790
|
-
const leftRail = `${formatModeRail(selectedMode)} `;
|
|
816
|
+
const leftRail = `${formatModeRail(selectedMode, ctx.ui.theme)} `;
|
|
791
817
|
const rightRail = this.borderColor("│");
|
|
792
818
|
const topRightVerticalTransition = this.borderColor("┆");
|
|
793
|
-
const bottomLeftVerticalTransition = formatModeRail(selectedMode, selectedMode === "build" ? "┇" : "┆");
|
|
819
|
+
const bottomLeftVerticalTransition = formatModeRail(selectedMode, ctx.ui.theme, selectedMode === "build" ? "┇" : "┆");
|
|
794
820
|
const metadata = truncateToWidth(
|
|
795
821
|
formatModeMetadata(selectedMode, pi.getThinkingLevel(), ctx.ui.theme, this.borderColor, {
|
|
796
822
|
modelName: ctx.model?.id ?? "no-model",
|
|
@@ -800,7 +826,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
800
826
|
width - 1,
|
|
801
827
|
"",
|
|
802
828
|
);
|
|
803
|
-
const topBorder = formatModeTopBorder(selectedMode, width, this.borderColor("╮"));
|
|
829
|
+
const topBorder = formatModeTopBorder(selectedMode, width, this.borderColor("╮"), ctx.ui.theme);
|
|
804
830
|
return renderModeComposer(
|
|
805
831
|
lines,
|
|
806
832
|
topBorder,
|
|
@@ -808,7 +834,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
808
834
|
rightRail,
|
|
809
835
|
topRightVerticalTransition,
|
|
810
836
|
metadata,
|
|
811
|
-
formatModeRail(selectedMode, "╰"),
|
|
837
|
+
formatModeRail(selectedMode, ctx.ui.theme, "╰"),
|
|
812
838
|
railWidth,
|
|
813
839
|
width,
|
|
814
840
|
{
|
|
@@ -863,6 +889,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
863
889
|
});
|
|
864
890
|
|
|
865
891
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
892
|
+
userMessageRail.deactivate();
|
|
866
893
|
removePanelLayout();
|
|
867
894
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
868
895
|
ctx.ui.setFooter(undefined);
|
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.48",
|
|
4
4
|
"description": "Plan safely, approve explicitly, then implement here or in a clean session.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"question-ui.ts",
|
|
26
26
|
"plan-execution.ts",
|
|
27
27
|
"plan-panel.ts",
|
|
28
|
+
"user-message-rail.ts",
|
|
28
29
|
"utils.ts",
|
|
29
30
|
"docs/images"
|
|
30
31
|
],
|
|
@@ -32,7 +33,7 @@
|
|
|
32
33
|
"access": "public"
|
|
33
34
|
},
|
|
34
35
|
"scripts": {
|
|
35
|
-
"test": "node --experimental-strip-types --test utils.test.ts question-ui.test.ts plan-execution.test.ts plan-panel.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",
|
|
36
37
|
"prepublishOnly": "npm test"
|
|
37
38
|
},
|
|
38
39
|
"peerDependencies": {
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { Mode } from "./utils.ts";
|
|
2
|
+
|
|
3
|
+
const OSC133_PREFIX = /^((?:\x1b\]133;[ABC]\x07)*)/u;
|
|
4
|
+
const PATCH_KEY = Symbol.for("@janvitos/pi-plan-build:user-message-rail");
|
|
5
|
+
|
|
6
|
+
export interface TranscriptModeRecord {
|
|
7
|
+
text: string;
|
|
8
|
+
mode: Mode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface UserMessageLike {
|
|
12
|
+
text?: unknown;
|
|
13
|
+
render(width: number): string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface UserMessageClass {
|
|
17
|
+
prototype: UserMessageLike;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface RailPatchState {
|
|
21
|
+
originalRender: (this: UserMessageLike, width: number) => string[];
|
|
22
|
+
componentModes: WeakMap<object, Mode>;
|
|
23
|
+
resolver: TranscriptModeResolver;
|
|
24
|
+
formatRail: (mode: Mode, glyph: string) => string;
|
|
25
|
+
getFallbackMode: () => Mode;
|
|
26
|
+
glyph: string;
|
|
27
|
+
owner?: symbol;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface UserMessageRailController {
|
|
31
|
+
activate(): void;
|
|
32
|
+
setTranscript(records: readonly TranscriptModeRecord[]): void;
|
|
33
|
+
addMessage(text: string, mode: Mode): void;
|
|
34
|
+
deactivate(): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class TranscriptModeResolver {
|
|
38
|
+
private records: TranscriptModeRecord[] = [];
|
|
39
|
+
private assigned = 0;
|
|
40
|
+
|
|
41
|
+
setTranscript(records: readonly TranscriptModeRecord[]): void {
|
|
42
|
+
this.records = [...records];
|
|
43
|
+
this.assigned = 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
addMessage(text: string, mode: Mode): void {
|
|
47
|
+
if (!text) return;
|
|
48
|
+
this.records.push({ text, mode });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
resolve(text: string, fallback: Mode): Mode {
|
|
52
|
+
if (this.records.length === 0) {
|
|
53
|
+
this.assigned++;
|
|
54
|
+
return fallback;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const start = this.assigned % this.records.length;
|
|
58
|
+
for (let offset = 0; offset < this.records.length; offset++) {
|
|
59
|
+
const index = (start + offset) % this.records.length;
|
|
60
|
+
const record = this.records[index]!;
|
|
61
|
+
if (record.text !== text) continue;
|
|
62
|
+
this.assigned += offset + 1;
|
|
63
|
+
return record.mode;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.assigned++;
|
|
67
|
+
return fallback;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function extractUserMessageText(content: unknown): string {
|
|
72
|
+
if (typeof content === "string") return content;
|
|
73
|
+
if (!Array.isArray(content)) return "";
|
|
74
|
+
return content
|
|
75
|
+
.filter(
|
|
76
|
+
(block): block is { type: "text"; text: string } =>
|
|
77
|
+
!!block &&
|
|
78
|
+
typeof block === "object" &&
|
|
79
|
+
(block as { type?: unknown }).type === "text" &&
|
|
80
|
+
typeof (block as { text?: unknown }).text === "string",
|
|
81
|
+
)
|
|
82
|
+
.map((block) => block.text)
|
|
83
|
+
.join("");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function collectTranscriptModeRecords(
|
|
87
|
+
entries: readonly unknown[],
|
|
88
|
+
options: {
|
|
89
|
+
initialMode?: Mode;
|
|
90
|
+
stateTypes: ReadonlySet<string>;
|
|
91
|
+
decodeState(data: unknown): { selectedMode: Mode } | undefined;
|
|
92
|
+
displayText(text: string): string | undefined;
|
|
93
|
+
},
|
|
94
|
+
): TranscriptModeRecord[] {
|
|
95
|
+
let mode = options.initialMode ?? "build";
|
|
96
|
+
const records: TranscriptModeRecord[] = [];
|
|
97
|
+
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
if (!entry || typeof entry !== "object") continue;
|
|
100
|
+
const candidate = entry as {
|
|
101
|
+
type?: unknown;
|
|
102
|
+
customType?: unknown;
|
|
103
|
+
data?: unknown;
|
|
104
|
+
message?: { role?: unknown; content?: unknown };
|
|
105
|
+
};
|
|
106
|
+
if (
|
|
107
|
+
candidate.type === "custom" &&
|
|
108
|
+
typeof candidate.customType === "string" &&
|
|
109
|
+
options.stateTypes.has(candidate.customType)
|
|
110
|
+
) {
|
|
111
|
+
mode = options.decodeState(candidate.data)?.selectedMode ?? mode;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (candidate.type !== "message" || candidate.message?.role !== "user") continue;
|
|
115
|
+
const displayText = options.displayText(extractUserMessageText(candidate.message.content));
|
|
116
|
+
if (displayText) records.push({ text: displayText, mode });
|
|
117
|
+
}
|
|
118
|
+
return records;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function prependRail(line: string, rail: string): string {
|
|
122
|
+
return line.replace(OSC133_PREFIX, `$1${rail}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function installUserMessageRail(
|
|
126
|
+
UserMessageComponent: UserMessageClass,
|
|
127
|
+
options: {
|
|
128
|
+
formatRail: (mode: Mode, glyph: string) => string;
|
|
129
|
+
getFallbackMode: () => Mode;
|
|
130
|
+
},
|
|
131
|
+
): UserMessageRailController {
|
|
132
|
+
const globalState = globalThis as typeof globalThis & { [PATCH_KEY]?: RailPatchState };
|
|
133
|
+
let state = globalState[PATCH_KEY];
|
|
134
|
+
if (!state) {
|
|
135
|
+
const originalRender = UserMessageComponent.prototype.render;
|
|
136
|
+
state = {
|
|
137
|
+
originalRender,
|
|
138
|
+
componentModes: new WeakMap(),
|
|
139
|
+
resolver: new TranscriptModeResolver(),
|
|
140
|
+
formatRail: options.formatRail,
|
|
141
|
+
getFallbackMode: options.getFallbackMode,
|
|
142
|
+
glyph: "│",
|
|
143
|
+
};
|
|
144
|
+
globalState[PATCH_KEY] = state;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const owner = Symbol("pi-plan-build-user-message-rail-owner");
|
|
148
|
+
state.owner = owner;
|
|
149
|
+
state.formatRail = options.formatRail;
|
|
150
|
+
state.getFallbackMode = options.getFallbackMode;
|
|
151
|
+
state.glyph = "│";
|
|
152
|
+
// Reinstall from the preserved original on every extension load. This migrates
|
|
153
|
+
// already-running processes away from stale decorator code without stacking wrappers.
|
|
154
|
+
UserMessageComponent.prototype.render = function renderWithModeRail(width: number): string[] {
|
|
155
|
+
const active = globalState[PATCH_KEY];
|
|
156
|
+
if (!active?.owner || width <= 1) return state.originalRender.call(this, width);
|
|
157
|
+
let mode = active.componentModes.get(this);
|
|
158
|
+
if (!mode) {
|
|
159
|
+
const text = typeof this.text === "string" ? this.text : "";
|
|
160
|
+
mode = active.resolver.resolve(text, active.getFallbackMode());
|
|
161
|
+
active.componentModes.set(this, mode);
|
|
162
|
+
}
|
|
163
|
+
const rail = active.formatRail(mode, active.glyph);
|
|
164
|
+
return active.originalRender.call(this, width - 1).map((line) => prependRail(line, rail));
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
activate() {
|
|
169
|
+
const active = globalState[PATCH_KEY];
|
|
170
|
+
if (active) active.owner = owner;
|
|
171
|
+
},
|
|
172
|
+
setTranscript(records) {
|
|
173
|
+
const active = globalState[PATCH_KEY];
|
|
174
|
+
if (active?.owner !== owner) return;
|
|
175
|
+
active.componentModes = new WeakMap();
|
|
176
|
+
active.resolver.setTranscript(records);
|
|
177
|
+
},
|
|
178
|
+
addMessage(text, mode) {
|
|
179
|
+
const active = globalState[PATCH_KEY];
|
|
180
|
+
if (active?.owner === owner) active.resolver.addMessage(text, mode);
|
|
181
|
+
},
|
|
182
|
+
deactivate() {
|
|
183
|
+
const active = globalState[PATCH_KEY];
|
|
184
|
+
if (active?.owner === owner) active.owner = undefined;
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
package/utils.ts
CHANGED
|
@@ -2,15 +2,16 @@ import path, { isAbsolute, relative, resolve, sep } from "node:path";
|
|
|
2
2
|
|
|
3
3
|
export type Mode = "build" | "plan";
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
build: { color: "38;2;92;156;245", text: "build" },
|
|
5
|
+
const MODE_LABELS: Record<Mode, string> = {
|
|
6
|
+
plan: "plan",
|
|
7
|
+
build: "build",
|
|
9
8
|
};
|
|
10
9
|
|
|
10
|
+
type ModeThemeColor = "warning" | "thinkingLow";
|
|
11
|
+
|
|
11
12
|
export interface ModeStatusTheme {
|
|
12
13
|
bold(text: string): string;
|
|
13
|
-
fg(color: "dim", text: string): string;
|
|
14
|
+
fg(color: "dim" | ModeThemeColor, text: string): string;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export interface PromptMetadataOptions {
|
|
@@ -36,17 +37,26 @@ export function nextThinkingLevel(
|
|
|
36
37
|
return available[(currentIndex + 1) % available.length];
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
function
|
|
40
|
-
return
|
|
40
|
+
function modeThemeColor(mode: Mode): ModeThemeColor {
|
|
41
|
+
return mode === "plan" ? "warning" : "thinkingLow";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatModeColor(mode: Mode, text: string, theme: ModeStatusTheme): string {
|
|
45
|
+
return theme.fg(modeThemeColor(mode), text);
|
|
41
46
|
}
|
|
42
47
|
|
|
43
|
-
export function formatModeRail(mode: Mode, glyph = "│"): string {
|
|
44
|
-
return formatModeColor(mode, glyph);
|
|
48
|
+
export function formatModeRail(mode: Mode, theme: ModeStatusTheme, glyph = "│"): string {
|
|
49
|
+
return formatModeColor(mode, glyph, theme);
|
|
45
50
|
}
|
|
46
51
|
|
|
47
|
-
export function formatModeTopBorder(
|
|
52
|
+
export function formatModeTopBorder(
|
|
53
|
+
mode: Mode,
|
|
54
|
+
width: number,
|
|
55
|
+
topRightCorner: string,
|
|
56
|
+
theme: ModeStatusTheme,
|
|
57
|
+
): string {
|
|
48
58
|
if (width <= 2) return "";
|
|
49
|
-
return `${formatModeColor(mode, `╭${"─".repeat(width - 3)}
|
|
59
|
+
return `${formatModeColor(mode, `╭${"─".repeat(width - 3)}╌`, theme)}${topRightCorner}`;
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
export function formatModeMetadata(
|
|
@@ -56,15 +66,14 @@ export function formatModeMetadata(
|
|
|
56
66
|
thinkingColor: (text: string) => string,
|
|
57
67
|
options?: PromptMetadataOptions,
|
|
58
68
|
): string {
|
|
59
|
-
const
|
|
60
|
-
const modeText = `\x1b[${label.color}m${theme.bold(label.text)}${ANSI_RESET}`;
|
|
69
|
+
const modeText = formatModeColor(mode, theme.bold(MODE_LABELS[mode]), theme);
|
|
61
70
|
const modelText = options
|
|
62
71
|
? `${theme.fg("dim", " • ")}${options.modelName}${
|
|
63
72
|
options.modelProvider ? theme.fg("dim", ` [${options.modelProvider}]`) : ""
|
|
64
73
|
}`
|
|
65
74
|
: "";
|
|
66
75
|
const thinkingSeparator = " • ";
|
|
67
|
-
return `${options?.rail ?? formatModeRail(mode)} ${modeText}${modelText}${theme.fg("dim", thinkingSeparator)}${thinkingColor(thinkingLevel)}`;
|
|
76
|
+
return `${options?.rail ?? formatModeRail(mode, theme)} ${modeText}${modelText}${theme.fg("dim", thinkingSeparator)}${thinkingColor(thinkingLevel)}`;
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
export function formatTokens(count: number): string {
|