@quandev104/pi-style 0.2.0 → 0.2.1
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/CHANGELOG.md +6 -0
- package/dist/extensions/pi-style.js +691 -296
- package/dist/extensions/pi-style.js.map +1 -1
- package/extension-src/pi-style/app/index.ts +3 -2
- package/extension-src/pi-style/app/runtime.ts +72 -84
- package/extension-src/pi-style/app/snapshot.ts +41 -2
- package/extension-src/pi-style/features/editor/index.ts +80 -61
- package/extension-src/pi-style/features/messages/index.ts +194 -56
- package/extension-src/pi-style/features/tools/bash-execution.ts +12 -1
- package/extension-src/pi-style/features/tools/boxed/bash.ts +99 -31
- package/extension-src/pi-style/features/tools/boxed/batch.ts +44 -10
- package/extension-src/pi-style/features/tools/boxed/edit.ts +30 -15
- package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +31 -15
- package/extension-src/pi-style/features/tools/boxed/session-config.ts +45 -14
- package/extension-src/pi-style/features/tools/boxed/shared.ts +25 -0
- package/extension-src/pi-style/pi/compatibility-probe.ts +1 -1
- package/extension-src/pi-style/pi/index.ts +26 -13
- package/extension-src/pi-style/pi/session-usage.ts +204 -21
- package/extension-src/pi-style/shared/box.ts +13 -2
- package/package.json +1 -1
|
@@ -96,6 +96,7 @@ export interface PiStyleApp {
|
|
|
96
96
|
update(
|
|
97
97
|
values: import("../domain/status.js").StatusSnapshot,
|
|
98
98
|
kind?: import("./render-scheduler.js").UpdateClass,
|
|
99
|
+
options?: import("./runtime.js").RuntimeUpdateOptions,
|
|
99
100
|
): void;
|
|
100
101
|
}
|
|
101
102
|
|
|
@@ -268,10 +269,10 @@ export function createPiStyleApp(
|
|
|
268
269
|
sessionShutdown() {
|
|
269
270
|
runtime.stop();
|
|
270
271
|
},
|
|
271
|
-
update(values, kind = "coalesced") {
|
|
272
|
+
update(values, kind = "coalesced", options) {
|
|
272
273
|
const active = runtime.current;
|
|
273
274
|
if (!active) return;
|
|
274
|
-
active.update(values);
|
|
275
|
+
if (!active.update(values, options)) return;
|
|
275
276
|
active.scheduler.schedule(kind);
|
|
276
277
|
},
|
|
277
278
|
reload() {
|
|
@@ -2,7 +2,7 @@ import type { ExtensionContext, ExtensionUIContext } from "@earendil-works/pi-co
|
|
|
2
2
|
import { diffConfig } from "../domain/config-diff.js";
|
|
3
3
|
import type { NormalizedPiStyleConfig } from "../domain/config-types.js";
|
|
4
4
|
import type { GitCommandRunner } from "../domain/providers.js";
|
|
5
|
-
import type { StatusSnapshot } from "../domain/status.js";
|
|
5
|
+
import type { ContextSnapshot, StatusSnapshot } from "../domain/status.js";
|
|
6
6
|
import { normalizeThinkingLevel } from "../domain/status.js";
|
|
7
7
|
import { installEditor } from "../features/editor/index.js";
|
|
8
8
|
import {
|
|
@@ -24,6 +24,11 @@ export interface RuntimeInstallationState {
|
|
|
24
24
|
readonly startup: "installed" | "disabled" | "failed";
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
export interface RuntimeUpdateOptions {
|
|
28
|
+
readonly refreshContextUsage?: boolean;
|
|
29
|
+
readonly refreshExtensionStatuses?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
export interface PiStyleRuntime {
|
|
28
33
|
generation: number;
|
|
29
34
|
readonly providerIdentity: { git: object; context: object; usage: object };
|
|
@@ -34,8 +39,8 @@ export interface PiStyleRuntime {
|
|
|
34
39
|
snapshot: UiSnapshot;
|
|
35
40
|
disposables: DisposableStore;
|
|
36
41
|
scheduler: RenderScheduler;
|
|
37
|
-
update(values: StatusSnapshot):
|
|
38
|
-
updateStartupResources(resources: StartupResources):
|
|
42
|
+
update(values: StatusSnapshot, options?: RuntimeUpdateOptions): boolean;
|
|
43
|
+
updateStartupResources(resources: StartupResources): boolean;
|
|
39
44
|
dismissStartup(): void;
|
|
40
45
|
invalidateGit(): void;
|
|
41
46
|
configure(config: NormalizedPiStyleConfig): void;
|
|
@@ -64,7 +69,7 @@ export interface RuntimeHost {
|
|
|
64
69
|
export function createPiStyleRuntime(
|
|
65
70
|
host: RuntimeHost,
|
|
66
71
|
generation: number,
|
|
67
|
-
requestRender: () => void = () => {},
|
|
72
|
+
requestRender: () => void = host.requestRender ?? (() => {}),
|
|
68
73
|
): PiStyleRuntime {
|
|
69
74
|
let disposed = false;
|
|
70
75
|
const extensionStatuses = (): readonly import("../domain/status.js").ExtensionStatus[] | undefined => {
|
|
@@ -76,13 +81,22 @@ export function createPiStyleRuntime(
|
|
|
76
81
|
return [];
|
|
77
82
|
}
|
|
78
83
|
};
|
|
84
|
+
const contextSnapshot = (): ContextSnapshot | undefined => {
|
|
85
|
+
const usage = host.getContextUsage?.();
|
|
86
|
+
if (!usage) return undefined;
|
|
87
|
+
return {
|
|
88
|
+
...(usage.tokens !== null ? { currentTokens: usage.tokens } : {}),
|
|
89
|
+
windowTokens: usage.contextWindow,
|
|
90
|
+
...(usage.percent !== null ? { percent: usage.percent } : {}),
|
|
91
|
+
};
|
|
92
|
+
};
|
|
79
93
|
let currentConfig = host.config;
|
|
80
94
|
const disposables = new DisposableStore();
|
|
81
95
|
const scheduler = new RenderScheduler({ requestRender }, generation, () => !disposed);
|
|
82
96
|
const git = new CachedGitProvider(host.gitRunner);
|
|
83
97
|
const contextProvider = new InMemoryContextProvider();
|
|
84
98
|
const usageProvider = new InMemoryUsageProvider();
|
|
85
|
-
const
|
|
99
|
+
const initialContext = contextSnapshot();
|
|
86
100
|
const initialStatuses = extensionStatuses();
|
|
87
101
|
const initialValues = {
|
|
88
102
|
...(initialStatuses ? { extensionStatuses: initialStatuses } : {}),
|
|
@@ -91,15 +105,7 @@ export function createPiStyleRuntime(
|
|
|
91
105
|
...(host.model?.reasoning !== undefined ? { reasoning: host.model.reasoning } : {}),
|
|
92
106
|
...(host.thinkingLevel ? { thinkingLevel: normalizeThinkingLevel(host.thinkingLevel) } : {}),
|
|
93
107
|
...(host.cwd ? { cwd: host.cwd } : {}),
|
|
94
|
-
...(
|
|
95
|
-
? {
|
|
96
|
-
context: {
|
|
97
|
-
...(usage.tokens !== null ? { currentTokens: usage.tokens } : {}),
|
|
98
|
-
windowTokens: usage.contextWindow,
|
|
99
|
-
...(usage.percent !== null ? { percent: usage.percent } : {}),
|
|
100
|
-
},
|
|
101
|
-
}
|
|
102
|
-
: {}),
|
|
108
|
+
...(initialContext ? { context: initialContext } : {}),
|
|
103
109
|
};
|
|
104
110
|
let currentSnapshot = createSnapshot(generation, 0, initialValues);
|
|
105
111
|
let statusLine: ReturnType<typeof installStatusLine> | undefined;
|
|
@@ -110,14 +116,41 @@ export function createPiStyleRuntime(
|
|
|
110
116
|
editor: "disabled",
|
|
111
117
|
startup: "disabled",
|
|
112
118
|
};
|
|
113
|
-
const
|
|
119
|
+
const snapshotValues = (snapshot: UiSnapshot): StatusSnapshot => {
|
|
120
|
+
const { generation: _generation, revision: _revision, ...values } = snapshot;
|
|
121
|
+
return values;
|
|
122
|
+
};
|
|
123
|
+
const withSnapshotPatch = (
|
|
124
|
+
patch: Partial<Record<keyof StatusSnapshot, StatusSnapshot[keyof StatusSnapshot] | undefined>>,
|
|
125
|
+
): StatusSnapshot => {
|
|
126
|
+
const next = { ...snapshotValues(currentSnapshot) } as Record<string, unknown>;
|
|
127
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
128
|
+
if (value === undefined) delete next[key];
|
|
129
|
+
else next[key] = value;
|
|
130
|
+
}
|
|
131
|
+
return next as StatusSnapshot;
|
|
132
|
+
};
|
|
133
|
+
const createStartupSnapshot = (
|
|
134
|
+
config: NormalizedPiStyleConfig,
|
|
135
|
+
resources: StartupResources | undefined = host.resources,
|
|
136
|
+
): StartupSnapshot => ({
|
|
114
137
|
...currentSnapshot,
|
|
115
138
|
reason: host.startupReason ?? "startup",
|
|
116
139
|
...(host.provider ? { startupProvider: host.provider } : {}),
|
|
117
140
|
...(host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {}),
|
|
118
141
|
preset: config.preset,
|
|
119
|
-
...(
|
|
142
|
+
...(resources ? { resources } : {}),
|
|
120
143
|
});
|
|
144
|
+
const applySnapshot = (nextSnapshot: UiSnapshot): boolean => {
|
|
145
|
+
if (nextSnapshot === currentSnapshot) return false;
|
|
146
|
+
currentSnapshot = nextSnapshot;
|
|
147
|
+
statusLine?.update(currentSnapshot);
|
|
148
|
+
editor?.update(currentSnapshot);
|
|
149
|
+
startup?.update(createStartupSnapshot(currentConfig));
|
|
150
|
+
return true;
|
|
151
|
+
};
|
|
152
|
+
const updateSnapshot = (values: StatusSnapshot): boolean =>
|
|
153
|
+
applySnapshot(replaceSnapshot(currentSnapshot, generation, values));
|
|
121
154
|
const installStatus = () => {
|
|
122
155
|
if (!host.hasUI || host.mode !== "tui" || !host.ui || !currentConfig.enabled || !currentConfig.statusLine.enabled) {
|
|
123
156
|
installationState = { ...installationState, status: "disabled" };
|
|
@@ -186,7 +219,7 @@ export function createPiStyleRuntime(
|
|
|
186
219
|
startup = installStartup({
|
|
187
220
|
host: { ...(host.ui as unknown as StartupHost), mode: host.mode, hasUI: host.hasUI },
|
|
188
221
|
config: currentConfig,
|
|
189
|
-
snapshot:
|
|
222
|
+
snapshot: createStartupSnapshot(currentConfig),
|
|
190
223
|
generation,
|
|
191
224
|
requestRender,
|
|
192
225
|
timeoutMs: 3000,
|
|
@@ -235,27 +268,10 @@ export function createPiStyleRuntime(
|
|
|
235
268
|
if (host.cwd && currentConfig.enabled && currentConfig.statusLine.enabled) {
|
|
236
269
|
void git.get(host.cwd).then((value) => {
|
|
237
270
|
if (disposed) return;
|
|
238
|
-
|
|
239
|
-
statusLine?.update(currentSnapshot);
|
|
240
|
-
editor?.update(currentSnapshot);
|
|
241
|
-
startup?.update({
|
|
242
|
-
...currentSnapshot,
|
|
243
|
-
reason: host.startupReason ?? "startup",
|
|
244
|
-
...(host.provider ? { startupProvider: host.provider } : {}),
|
|
245
|
-
...(host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {}),
|
|
246
|
-
preset: currentConfig.preset,
|
|
247
|
-
...(host.resources ? { resources: host.resources } : {}),
|
|
248
|
-
});
|
|
249
|
-
requestRender();
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
if (usage) {
|
|
253
|
-
contextProvider.set("active", {
|
|
254
|
-
...(usage.tokens !== null ? { currentTokens: usage.tokens } : {}),
|
|
255
|
-
windowTokens: usage.contextWindow,
|
|
256
|
-
...(usage.percent !== null ? { percent: usage.percent } : {}),
|
|
271
|
+
if (updateSnapshot(withSnapshotPatch({ git: value }))) requestRender();
|
|
257
272
|
});
|
|
258
273
|
}
|
|
274
|
+
if (initialContext) contextProvider.set("active", initialContext);
|
|
259
275
|
return {
|
|
260
276
|
generation,
|
|
261
277
|
providerIdentity: { git, context: contextProvider, usage: usageProvider },
|
|
@@ -264,51 +280,33 @@ export function createPiStyleRuntime(
|
|
|
264
280
|
},
|
|
265
281
|
mode: host.mode,
|
|
266
282
|
hasUI: host.hasUI,
|
|
267
|
-
snapshot
|
|
283
|
+
get snapshot() {
|
|
284
|
+
return currentSnapshot;
|
|
285
|
+
},
|
|
268
286
|
disposables,
|
|
269
287
|
scheduler,
|
|
270
288
|
updateStartupResources(resources) {
|
|
271
|
-
if (disposed) return;
|
|
272
|
-
startup
|
|
273
|
-
...currentSnapshot,
|
|
274
|
-
reason: host.startupReason ?? "startup",
|
|
275
|
-
...(host.provider ? { startupProvider: host.provider } : {}),
|
|
276
|
-
...(host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {}),
|
|
277
|
-
preset: currentConfig.preset,
|
|
278
|
-
resources,
|
|
279
|
-
});
|
|
289
|
+
if (disposed || !startup) return false;
|
|
290
|
+
startup.update(createStartupSnapshot(currentConfig, resources));
|
|
280
291
|
requestRender();
|
|
292
|
+
return true;
|
|
281
293
|
},
|
|
282
294
|
dismissStartup() {
|
|
283
295
|
startup?.dismiss();
|
|
284
296
|
},
|
|
285
|
-
update(values) {
|
|
286
|
-
if (disposed) return;
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
...values,
|
|
299
|
-
...(context ? { context } : {}),
|
|
300
|
-
...(statuses ? { extensionStatuses: statuses } : {}),
|
|
301
|
-
});
|
|
302
|
-
statusLine?.update(currentSnapshot);
|
|
303
|
-
editor?.update(currentSnapshot);
|
|
304
|
-
startup?.update({
|
|
305
|
-
...currentSnapshot,
|
|
306
|
-
reason: host.startupReason ?? "startup",
|
|
307
|
-
...(host.provider ? { startupProvider: host.provider } : {}),
|
|
308
|
-
...(host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {}),
|
|
309
|
-
preset: currentConfig.preset,
|
|
310
|
-
...(host.resources ? { resources: host.resources } : {}),
|
|
311
|
-
});
|
|
297
|
+
update(values, options = {}) {
|
|
298
|
+
if (disposed) return false;
|
|
299
|
+
const patch: Partial<Record<keyof StatusSnapshot, StatusSnapshot[keyof StatusSnapshot] | undefined>> = {
|
|
300
|
+
...(values as Partial<Record<keyof StatusSnapshot, StatusSnapshot[keyof StatusSnapshot] | undefined>>),
|
|
301
|
+
};
|
|
302
|
+
if (options.refreshContextUsage) {
|
|
303
|
+
const context = contextSnapshot();
|
|
304
|
+
patch.context = context;
|
|
305
|
+
if (context) contextProvider.set("active", context);
|
|
306
|
+
else contextProvider.clear();
|
|
307
|
+
}
|
|
308
|
+
if (options.refreshExtensionStatuses) patch.extensionStatuses = extensionStatuses();
|
|
309
|
+
return updateSnapshot(withSnapshotPatch(patch));
|
|
312
310
|
},
|
|
313
311
|
configure(nextConfig) {
|
|
314
312
|
if (disposed) return;
|
|
@@ -350,17 +348,7 @@ export function createPiStyleRuntime(
|
|
|
350
348
|
git.invalidate(host.cwd);
|
|
351
349
|
void git.get(host.cwd).then((value) => {
|
|
352
350
|
if (disposed) return;
|
|
353
|
-
|
|
354
|
-
statusLine?.update(currentSnapshot);
|
|
355
|
-
editor?.update(currentSnapshot);
|
|
356
|
-
startup?.update({
|
|
357
|
-
...currentSnapshot,
|
|
358
|
-
reason: host.startupReason ?? "startup",
|
|
359
|
-
...(host.provider ? { startupProvider: host.provider } : {}),
|
|
360
|
-
...(host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {}),
|
|
361
|
-
preset: currentConfig.preset,
|
|
362
|
-
});
|
|
363
|
-
requestRender();
|
|
351
|
+
if (updateSnapshot(withSnapshotPatch({ git: value }))) requestRender();
|
|
364
352
|
});
|
|
365
353
|
},
|
|
366
354
|
get disposed() {
|
|
@@ -6,10 +6,49 @@ export interface UiSnapshot extends StatusSnapshot {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export function createSnapshot(generation: number, revision = 0, values: StatusSnapshot = {}): UiSnapshot {
|
|
9
|
-
return Object.freeze({ generation, revision
|
|
9
|
+
return Object.freeze({ ...values, generation, revision });
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function replaceSnapshot(current: UiSnapshot, generation: number, values: StatusSnapshot): UiSnapshot {
|
|
13
13
|
if (current.generation !== generation) return current;
|
|
14
|
-
return createSnapshot(generation, current.revision + 1, values);
|
|
14
|
+
return equalStatusSnapshot(current, values) ? current : createSnapshot(generation, current.revision + 1, values);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function equalStatusSnapshot(current: StatusSnapshot, next: StatusSnapshot): boolean {
|
|
18
|
+
const currentEntries = Object.entries(current).filter(([key]) => key !== "generation" && key !== "revision");
|
|
19
|
+
const nextEntries = Object.entries(next).filter(([key]) => key !== "generation" && key !== "revision");
|
|
20
|
+
if (currentEntries.length !== nextEntries.length) return false;
|
|
21
|
+
for (const [key, value] of nextEntries) {
|
|
22
|
+
if (!hasOwn(current, key)) return false;
|
|
23
|
+
if (!equalValue((current as Record<string, unknown>)[key], value)) return false;
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function equalValue(left: unknown, right: unknown): boolean {
|
|
29
|
+
if (Object.is(left, right)) return true;
|
|
30
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
31
|
+
if (left.length !== right.length) return false;
|
|
32
|
+
for (let index = 0; index < left.length; index++) {
|
|
33
|
+
if (!equalValue(left[index], right[index])) return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (!isPlainRecord(left) || !isPlainRecord(right)) return false;
|
|
38
|
+
const leftKeys = Object.keys(left);
|
|
39
|
+
const rightKeys = Object.keys(right);
|
|
40
|
+
if (leftKeys.length !== rightKeys.length) return false;
|
|
41
|
+
for (const key of rightKeys) {
|
|
42
|
+
if (!hasOwn(left, key)) return false;
|
|
43
|
+
if (!equalValue(left[key], right[key])) return false;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function hasOwn(value: object, key: string): boolean {
|
|
49
|
+
return Object.hasOwn(value, key);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
53
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
54
|
}
|
|
@@ -39,6 +39,19 @@ interface EditorOptions {
|
|
|
39
39
|
onSnapshot: (snapshot: StatusSnapshot) => void;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
interface RenderPlan {
|
|
43
|
+
readonly style: "compact" | "boxed" | "dock" | "native";
|
|
44
|
+
readonly kind: "compact" | "boxed" | "outline" | "rounded" | "native";
|
|
45
|
+
readonly prompt: string;
|
|
46
|
+
readonly promptWidth: number;
|
|
47
|
+
readonly padding: number;
|
|
48
|
+
readonly sideReserve: number;
|
|
49
|
+
readonly renderWidth: number;
|
|
50
|
+
readonly innerWidth: number;
|
|
51
|
+
readonly prefix: string;
|
|
52
|
+
readonly continuation: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
const widthOf = visibleWidth;
|
|
43
56
|
|
|
44
57
|
function widthSafe(value: string, width: number): string {
|
|
@@ -130,6 +143,7 @@ export class StyledEditor extends CustomEditor implements EditorComponent {
|
|
|
130
143
|
private readonly onSnapshot: (snapshot: StatusSnapshot) => void;
|
|
131
144
|
private semantic: ResolvedTheme;
|
|
132
145
|
private disposed = false;
|
|
146
|
+
private renderPlanCache: { key: string; plan: RenderPlan } | undefined;
|
|
133
147
|
|
|
134
148
|
constructor(tui: Tui, theme: PiEditorTheme, keybindings: Keybindings, options: EditorOptions) {
|
|
135
149
|
super(tui, theme, keybindings);
|
|
@@ -164,88 +178,37 @@ export class StyledEditor extends CustomEditor implements EditorComponent {
|
|
|
164
178
|
override invalidate(): void {
|
|
165
179
|
super.invalidate();
|
|
166
180
|
this.semantic = semanticTheme(this.piTheme, this.config);
|
|
181
|
+
this.renderPlanCache = undefined;
|
|
167
182
|
this.tui.requestRender();
|
|
168
183
|
}
|
|
169
184
|
|
|
170
185
|
override render(width: number): string[] {
|
|
171
186
|
if (width <= 0) return [];
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
// widgets (status line) down. Re-frame the native output so the dropdown
|
|
177
|
-
// lives INSIDE the input box, keeping the footer directly below the input.
|
|
178
|
-
// Native layout: [top border, text lines, bottom border, dropdown lines...].
|
|
179
|
-
if ((this as unknown as { autocompleteState?: unknown }).autocompleteState) {
|
|
180
|
-
const prompt = this.prompt();
|
|
181
|
-
const padding = this.paddingFor(width, style);
|
|
182
|
-
const promptWidth = widthOf(prompt) + 1;
|
|
183
|
-
const prefix = `${" ".repeat(padding)}${prompt} `;
|
|
184
|
-
const continuation = " ".repeat(padding + promptWidth);
|
|
185
|
-
const borderIndex = nativeLines.slice(1).findIndex((line) => isNativeBorderLine(line));
|
|
186
|
-
const split = borderIndex >= 0 ? borderIndex + 1 : nativeLines.length;
|
|
187
|
-
const body = nativeLines.slice(1, split);
|
|
188
|
-
const dropdown = nativeLines.slice(split);
|
|
189
|
-
const border = this.borderFor();
|
|
190
|
-
const kind = this.frameKind(style);
|
|
191
|
-
const renderWidth = width - (kind === "rounded" ? 2 : 0);
|
|
192
|
-
const sideColor = kind === "rounded" ? this.borderColorFor() : undefined;
|
|
193
|
-
const wrap = (line: string) =>
|
|
194
|
-
kind === "rounded" && sideColor ? `${sideColor("│")}${line}${sideColor("│")}` : line;
|
|
195
|
-
const bashHidden = this.bashHiddenCount();
|
|
196
|
-
const renderedBody = body.map((line, index) => {
|
|
197
|
-
const source = index === 0 && bashHidden > 0 ? stripLeadingVisibleChars(line, bashHidden) : line;
|
|
198
|
-
return wrap(widthSafe(`${index === 0 ? prefix : continuation}${source}`, renderWidth));
|
|
199
|
-
});
|
|
200
|
-
const dropdownLines = dropdown.map((line) => wrap(widthSafe(line, renderWidth)));
|
|
201
|
-
if (kind === "rounded") {
|
|
202
|
-
return [
|
|
203
|
-
border(`╭${"─".repeat(Math.max(0, width - 2))}╮`),
|
|
204
|
-
...renderedBody,
|
|
205
|
-
...dropdownLines,
|
|
206
|
-
border(`╰${"─".repeat(Math.max(0, width - 2))}╯`),
|
|
207
|
-
];
|
|
208
|
-
}
|
|
209
|
-
return [border("─".repeat(width)), ...renderedBody, ...dropdownLines, border("─".repeat(width))];
|
|
210
|
-
}
|
|
211
|
-
if (style === "native") return nativeLines.map((line) => widthSafe(line, width));
|
|
187
|
+
const plan = this.renderPlan(width);
|
|
188
|
+
const autocompleteState = (this as unknown as { autocompleteState?: unknown }).autocompleteState;
|
|
189
|
+
if (plan.style === "native") return super.render(width).map((line) => widthSafe(line, width));
|
|
190
|
+
if (autocompleteState) return this.renderAutocompleteFrame(width, plan);
|
|
212
191
|
|
|
213
|
-
const
|
|
214
|
-
const promptWidth = widthOf(prompt) + 1;
|
|
215
|
-
const padding = this.paddingFor(width, style);
|
|
216
|
-
const kind = this.frameKind(style);
|
|
217
|
-
const sideReserve = kind === "rounded" ? 2 : 0;
|
|
218
|
-
const renderWidth = Math.max(1, width - sideReserve);
|
|
219
|
-
const innerWidth = Math.max(1, renderWidth - promptWidth - padding * 2);
|
|
220
|
-
const innerLines = super.render(innerWidth);
|
|
192
|
+
const innerLines = super.render(plan.innerWidth);
|
|
221
193
|
if (innerLines.length === 0) return [];
|
|
222
|
-
|
|
223
194
|
const body = innerLines.slice(1, -1);
|
|
224
|
-
const prefix = `${" ".repeat(padding)}${prompt} `;
|
|
225
|
-
const continuation = " ".repeat(padding + promptWidth);
|
|
226
195
|
const hint = this.config.editor.hint;
|
|
227
196
|
const showHint = hint !== "" && this.getText() === "";
|
|
228
197
|
const bashHidden = this.bashHiddenCount();
|
|
229
198
|
const renderedBody = body.map((line, index) => {
|
|
230
|
-
const lead = index === 0 ? prefix : continuation;
|
|
199
|
+
const lead = index === 0 ? plan.prefix : plan.continuation;
|
|
231
200
|
const source = index === 0 && bashHidden > 0 ? stripLeadingVisibleChars(line, bashHidden) : line;
|
|
232
201
|
let content = `${lead}${source}`;
|
|
233
|
-
// Empty-input hint: the cursor block (first cell of the native empty
|
|
234
|
-
// line) stays at the input position, the dim hint trails it. The native
|
|
235
|
-
// line is pre-padded to renderWidth with literal spaces; drop them from
|
|
236
|
-
// the raw end (safe: no ANSI follows the padding) before appending the
|
|
237
|
-
// hint, or the hint is truncated away by widthSafe. Typing any character
|
|
238
|
-
// makes the text non-empty and the hint disappears.
|
|
239
202
|
if (showHint && index === 0 && line) {
|
|
240
203
|
let end = content.length;
|
|
241
204
|
while (end > 0 && content[end - 1] === " ") end--;
|
|
242
205
|
if (end < content.length) content = content.slice(0, end);
|
|
243
206
|
content += this.semantic.apply("hint", hint);
|
|
244
207
|
}
|
|
245
|
-
return widthSafe(content, renderWidth);
|
|
208
|
+
return widthSafe(content, plan.renderWidth);
|
|
246
209
|
});
|
|
247
|
-
const metadata = this.metadata(width, style);
|
|
248
|
-
const framed = this.frame(width, style, renderedBody, metadata);
|
|
210
|
+
const metadata = this.metadata(width, plan.style);
|
|
211
|
+
const framed = this.frame(width, plan.style, renderedBody, metadata);
|
|
249
212
|
return framed.map((line) => widthSafe(line, width));
|
|
250
213
|
}
|
|
251
214
|
|
|
@@ -255,6 +218,62 @@ export class StyledEditor extends CustomEditor implements EditorComponent {
|
|
|
255
218
|
this.invalidate();
|
|
256
219
|
}
|
|
257
220
|
|
|
221
|
+
private renderPlan(width: number): RenderPlan {
|
|
222
|
+
const style = this.styleFor(width);
|
|
223
|
+
const prompt = this.prompt();
|
|
224
|
+
const kind = this.frameKind(style);
|
|
225
|
+
const key = `${width}:${style}:${kind}:${prompt}`;
|
|
226
|
+
if (this.renderPlanCache?.key === key) return this.renderPlanCache.plan;
|
|
227
|
+
const promptWidth = widthOf(prompt) + 1;
|
|
228
|
+
const padding = this.paddingFor(width, style);
|
|
229
|
+
const sideReserve = kind === "rounded" ? 2 : 0;
|
|
230
|
+
const renderWidth = Math.max(1, width - sideReserve);
|
|
231
|
+
const innerWidth = Math.max(1, renderWidth - promptWidth - padding * 2);
|
|
232
|
+
const prefix = `${" ".repeat(padding)}${prompt} `;
|
|
233
|
+
const continuation = " ".repeat(padding + promptWidth);
|
|
234
|
+
const plan = {
|
|
235
|
+
style,
|
|
236
|
+
kind,
|
|
237
|
+
prompt,
|
|
238
|
+
promptWidth,
|
|
239
|
+
padding,
|
|
240
|
+
sideReserve,
|
|
241
|
+
renderWidth,
|
|
242
|
+
innerWidth,
|
|
243
|
+
prefix,
|
|
244
|
+
continuation,
|
|
245
|
+
};
|
|
246
|
+
this.renderPlanCache = { key, plan };
|
|
247
|
+
return plan;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private renderAutocompleteFrame(width: number, plan: RenderPlan): string[] {
|
|
251
|
+
const nativeLines = super.render(width);
|
|
252
|
+
const borderIndex = nativeLines.slice(1).findIndex((line) => isNativeBorderLine(line));
|
|
253
|
+
const split = borderIndex >= 0 ? borderIndex + 1 : nativeLines.length;
|
|
254
|
+
const body = nativeLines.slice(1, split);
|
|
255
|
+
const dropdown = nativeLines.slice(split);
|
|
256
|
+
const border = this.borderFor();
|
|
257
|
+
const sideColor = plan.kind === "rounded" ? this.borderColorFor() : undefined;
|
|
258
|
+
const wrap = (line: string) =>
|
|
259
|
+
plan.kind === "rounded" && sideColor ? `${sideColor("│")}${line}${sideColor("│")}` : line;
|
|
260
|
+
const bashHidden = this.bashHiddenCount();
|
|
261
|
+
const renderedBody = body.map((line, index) => {
|
|
262
|
+
const source = index === 0 && bashHidden > 0 ? stripLeadingVisibleChars(line, bashHidden) : line;
|
|
263
|
+
return wrap(widthSafe(`${index === 0 ? plan.prefix : plan.continuation}${source}`, plan.renderWidth));
|
|
264
|
+
});
|
|
265
|
+
const dropdownLines = dropdown.map((line) => wrap(widthSafe(line, plan.renderWidth)));
|
|
266
|
+
if (plan.kind === "rounded") {
|
|
267
|
+
return [
|
|
268
|
+
border(`╭${"─".repeat(Math.max(0, width - 2))}╮`),
|
|
269
|
+
...renderedBody,
|
|
270
|
+
...dropdownLines,
|
|
271
|
+
border(`╰${"─".repeat(Math.max(0, width - 2))}╯`),
|
|
272
|
+
];
|
|
273
|
+
}
|
|
274
|
+
return [border("─".repeat(width)), ...renderedBody, ...dropdownLines, border("─".repeat(width))];
|
|
275
|
+
}
|
|
276
|
+
|
|
258
277
|
private prompt(): string {
|
|
259
278
|
if (this.isBashMode()) {
|
|
260
279
|
// Bash mode (`!` prefix): the prompt glyph becomes the bash icon and the
|