pi-one-ui 0.7.0 → 0.7.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 +25 -1
- package/README.en.md +19 -6
- package/README.md +19 -6
- package/extensions/app/config/context.ts +42 -0
- package/extensions/app/config/editor.ts +80 -0
- package/extensions/app/config/footer.ts +291 -0
- package/extensions/app/config/panel.ts +1 -5
- package/extensions/app/config/renderer.ts +1 -6
- package/extensions/app/config/shell.ts +358 -1435
- package/extensions/app/config/store.ts +22 -1
- package/extensions/app/config/values.ts +33 -0
- package/extensions/app/config/working-line.ts +100 -0
- package/extensions/app/overlay/input-router.ts +56 -32
- package/extensions/app/overlay/overlay-manager.ts +183 -21
- package/extensions/app/runtime/event-coordinator.ts +90 -42
- package/extensions/{features → app/runtime}/flush-docked-bash.ts +12 -2
- package/extensions/app/runtime/render-scheduler.ts +35 -31
- package/extensions/app/runtime/tui-runtime.ts +315 -312
- package/extensions/app/settings/controller.ts +202 -0
- package/extensions/app/settings/panel.ts +81 -0
- package/extensions/app/settings/presets.ts +43 -0
- package/extensions/features/context-inspector/index.ts +83 -662
- package/extensions/layouts/context/index.ts +58 -161
- package/extensions/layouts/context/message/user-message.ts +1 -1
- package/extensions/layouts/context/renderer/index.ts +31 -13
- package/extensions/layouts/context/renderer/mouse/interaction.ts +29 -14
- package/extensions/layouts/editor/controller.ts +9 -18
- package/extensions/layouts/editor/factory.ts +3 -3
- package/extensions/layouts/footer/controller.ts +30 -19
- package/extensions/layouts/footer/footer.ts +2 -2
- package/extensions/layouts/overlay/context-inspector.ts +497 -0
- package/extensions/{app → layouts}/overlay/selector-border.ts +5 -5
- package/extensions/{app → layouts}/overlay/selector-controller.ts +1 -1
- package/extensions/layouts/overlay/settings-panel.ts +359 -0
- package/extensions/{app/commands → layouts/overlay}/settings-previews.ts +4 -4
- package/extensions/layouts/working-line/controller.ts +3 -29
- package/extensions/layouts/working-line/working-line.ts +17 -114
- package/extensions/shared/working-line-text.ts +108 -0
- package/package.json +1 -1
- package/extensions/app/host/pi-extension-port.ts +0 -25
- package/extensions/app/host/pi-ui-port.ts +0 -52
- package/extensions/app/host/tui-capabilities.ts +0 -27
- package/extensions/app/ownership/layout-registry.ts +0 -65
- package/extensions/app/ownership.ts +0 -32
- package/extensions/app/panel.ts +0 -770
- package/extensions/app/presets.ts +0 -35
- package/extensions/app/runtime/runtime-state.ts +0 -83
- /package/extensions/{layouts/working-line → app/config}/working-line-messages.ts +0 -0
- /package/extensions/app/{ownership → runtime}/prototype-patch-registry.ts +0 -0
|
@@ -4,6 +4,10 @@ import {
|
|
|
4
4
|
hasUnsupportedComponentStyle,
|
|
5
5
|
type PolishedTuiConfig,
|
|
6
6
|
} from "../../app/config/shell.ts";
|
|
7
|
+
import type {
|
|
8
|
+
RenderRequest,
|
|
9
|
+
RenderScheduler,
|
|
10
|
+
} from "../../app/runtime/render-scheduler.ts";
|
|
7
11
|
import type { SessionLifecycle } from "../../app/runtime/session-lifecycle.ts";
|
|
8
12
|
import type { LiveContextOverride } from "../../services/live-context.ts";
|
|
9
13
|
import type { FooterState } from "../../services/session-state.ts";
|
|
@@ -17,9 +21,7 @@ type FooterKind = "starship" | "hidden";
|
|
|
17
21
|
|
|
18
22
|
export type FooterControllerContext = {
|
|
19
23
|
readonly getConfig: () => PolishedTuiConfig;
|
|
20
|
-
readonly
|
|
21
|
-
patch: Partial<FooterComponentConfig>,
|
|
22
|
-
) => PolishedTuiConfig;
|
|
24
|
+
readonly render: Pick<RenderScheduler, "register">;
|
|
23
25
|
readonly state: FooterState;
|
|
24
26
|
readonly sessionLifecycle: SessionLifecycle;
|
|
25
27
|
readonly refresh: () => void;
|
|
@@ -39,7 +41,7 @@ export class FooterLayoutController {
|
|
|
39
41
|
private readonly context: FooterControllerContext;
|
|
40
42
|
private installedKind: FooterKind | undefined;
|
|
41
43
|
private installedToken: symbol | undefined;
|
|
42
|
-
private requestFooterRender:
|
|
44
|
+
private requestFooterRender: RenderRequest | undefined;
|
|
43
45
|
private getActiveExtensionStatuses: () => ReadonlyMap<string, string> = () =>
|
|
44
46
|
new Map();
|
|
45
47
|
private stopSessionTimer: () => void = () => {};
|
|
@@ -68,23 +70,34 @@ export class FooterLayoutController {
|
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
/**
|
|
71
|
-
*
|
|
73
|
+
* 应用 app 已提交的 Footer 配置并更新组件。
|
|
72
74
|
*
|
|
73
|
-
* @param patch Footer
|
|
74
|
-
* @param ctx
|
|
75
|
+
* @param patch 本次修改的 Footer 字段。
|
|
76
|
+
* @param ctx 当前 Pi session 的上下文。
|
|
77
|
+
* @returns 组件应用结果。
|
|
75
78
|
*/
|
|
76
|
-
|
|
79
|
+
applyConfig(
|
|
77
80
|
patch: Partial<FooterComponentConfig>,
|
|
78
81
|
ctx: ExtensionContext,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
82
|
+
previousStyle = this.context.getConfig().components.footer.style,
|
|
83
|
+
): { applied: boolean; reason?: string } {
|
|
84
|
+
const styleChanged =
|
|
85
|
+
this.context.getConfig().components.footer.style !== previousStyle;
|
|
83
86
|
if (patch.style !== undefined) this.reconcile(ctx);
|
|
84
87
|
if (patch.modelLabel !== undefined) this.context.onModelLabelChanged(ctx);
|
|
85
88
|
this.context.onProjectRequirementChanged(ctx, styleChanged);
|
|
86
89
|
this.reconcileSessionTimer();
|
|
87
90
|
this.context.refresh();
|
|
91
|
+
const style = this.effectiveFooterStyle();
|
|
92
|
+
const applied =
|
|
93
|
+
patch.style === undefined ||
|
|
94
|
+
(style === "native"
|
|
95
|
+
? !this.ownsStatusLine(ctx)
|
|
96
|
+
: this.installedKind === style && this.ownsStatusLine(ctx));
|
|
97
|
+
return {
|
|
98
|
+
applied,
|
|
99
|
+
reason: applied ? undefined : `Footer could not apply the ${style} style`,
|
|
100
|
+
};
|
|
88
101
|
}
|
|
89
102
|
|
|
90
103
|
/**
|
|
@@ -135,6 +148,7 @@ export class FooterLayoutController {
|
|
|
135
148
|
this.installedKind = undefined;
|
|
136
149
|
this.installedToken = undefined;
|
|
137
150
|
this.requestFooterRender = undefined;
|
|
151
|
+
this.context.render.register(this, undefined);
|
|
138
152
|
this.getActiveExtensionStatuses = () => new Map();
|
|
139
153
|
}
|
|
140
154
|
|
|
@@ -212,13 +226,6 @@ export class FooterLayoutController {
|
|
|
212
226
|
: new Set<string>();
|
|
213
227
|
}
|
|
214
228
|
|
|
215
|
-
/**
|
|
216
|
-
* Requests a redraw from the mounted Footer component.
|
|
217
|
-
*/
|
|
218
|
-
requestRender(): void {
|
|
219
|
-
this.requestFooterRender?.();
|
|
220
|
-
}
|
|
221
|
-
|
|
222
229
|
/**
|
|
223
230
|
* Reports whether a context is an interactive TUI context.
|
|
224
231
|
*
|
|
@@ -282,6 +289,7 @@ export class FooterLayoutController {
|
|
|
282
289
|
this.installedToken = undefined;
|
|
283
290
|
if (this.footerOwner(ctx) === token) this.setOwnership(ctx, undefined);
|
|
284
291
|
this.requestFooterRender = undefined;
|
|
292
|
+
this.context.render.register(this, undefined);
|
|
285
293
|
this.getActiveExtensionStatuses = () => new Map();
|
|
286
294
|
this.stopSessionTimer();
|
|
287
295
|
if (this.context.sessionLifecycle.isCurrent())
|
|
@@ -301,6 +309,7 @@ export class FooterLayoutController {
|
|
|
301
309
|
installFooter(ctx, this.context.state, this.context.getConfig, {
|
|
302
310
|
setRequestRender: (requestRender) => {
|
|
303
311
|
this.requestFooterRender = requestRender;
|
|
312
|
+
this.context.render.register(this, requestRender);
|
|
304
313
|
},
|
|
305
314
|
scheduleProjectRefresh: (refreshContext) =>
|
|
306
315
|
this.context.scheduleProjectRefresh(refreshContext),
|
|
@@ -334,6 +343,7 @@ export class FooterLayoutController {
|
|
|
334
343
|
this.installedToken = token;
|
|
335
344
|
this.setOwnership(ctx, token);
|
|
336
345
|
this.requestFooterRender = undefined;
|
|
346
|
+
this.context.render.register(this, undefined);
|
|
337
347
|
this.getActiveExtensionStatuses = () => new Map();
|
|
338
348
|
this.stopSessionTimer();
|
|
339
349
|
} catch {
|
|
@@ -373,6 +383,7 @@ export class FooterLayoutController {
|
|
|
373
383
|
this.footerOwner(ctx) === previous.token
|
|
374
384
|
) {
|
|
375
385
|
this.requestFooterRender = previous.requestRender;
|
|
386
|
+
this.context.render.register(this, previous.requestRender);
|
|
376
387
|
this.getActiveExtensionStatuses = previous.getExtensionStatuses;
|
|
377
388
|
return;
|
|
378
389
|
}
|
|
@@ -230,7 +230,7 @@ export function installFooter(
|
|
|
230
230
|
state: FooterState,
|
|
231
231
|
getConfig: () => ZentuiConfig,
|
|
232
232
|
hooks: {
|
|
233
|
-
setRequestRender: (fn: (() => void) | undefined) => void;
|
|
233
|
+
setRequestRender: (fn: ((force: boolean) => void) | undefined) => void;
|
|
234
234
|
scheduleProjectRefresh: (ctx: ExtensionContext) => void;
|
|
235
235
|
setExtensionStatusesGetter?: (
|
|
236
236
|
fn: (() => ReadonlyMap<string, string>) | undefined,
|
|
@@ -240,7 +240,7 @@ export function installFooter(
|
|
|
240
240
|
},
|
|
241
241
|
): void {
|
|
242
242
|
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
243
|
-
hooks.setRequestRender(() => tui.requestRender());
|
|
243
|
+
hooks.setRequestRender((force) => tui.requestRender(force));
|
|
244
244
|
hooks.setExtensionStatusesGetter?.(() => footerData.getExtensionStatuses());
|
|
245
245
|
const unsubscribeBranch = footerData.onBranchChange(() => {
|
|
246
246
|
hooks.scheduleProjectRefresh(ctx);
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ExtensionCommandContext,
|
|
3
|
+
getMarkdownTheme,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import {
|
|
6
|
+
Key,
|
|
7
|
+
Markdown,
|
|
8
|
+
matchesKey,
|
|
9
|
+
type TUI,
|
|
10
|
+
visibleWidth,
|
|
11
|
+
} from "@earendil-works/pi-tui";
|
|
12
|
+
import {
|
|
13
|
+
type OverlayManager,
|
|
14
|
+
overlayManager,
|
|
15
|
+
} from "../../app/overlay/overlay-manager.ts";
|
|
16
|
+
import { padLine } from "../../tools/format.ts";
|
|
17
|
+
import { mouseBaseButton, parseSgrMousePacket } from "../../tools/sgr-mouse.ts";
|
|
18
|
+
|
|
19
|
+
export type ContextPart = {
|
|
20
|
+
label: string;
|
|
21
|
+
tokens: number;
|
|
22
|
+
color:
|
|
23
|
+
| "accent"
|
|
24
|
+
| "success"
|
|
25
|
+
| "warning"
|
|
26
|
+
| "customMessageLabel"
|
|
27
|
+
| "muted"
|
|
28
|
+
| "dim"
|
|
29
|
+
| "error";
|
|
30
|
+
};
|
|
31
|
+
export type ContextPreviewKey =
|
|
32
|
+
| "systemPrompt"
|
|
33
|
+
| "memoryFiles"
|
|
34
|
+
| "skills"
|
|
35
|
+
| "tools"
|
|
36
|
+
| "toolResults"
|
|
37
|
+
| "contextFiles";
|
|
38
|
+
export type ContextInspectorData = {
|
|
39
|
+
parts: ContextPart[];
|
|
40
|
+
used: number;
|
|
41
|
+
contextWindow: number;
|
|
42
|
+
previews: Record<ContextPreviewKey, string>;
|
|
43
|
+
};
|
|
44
|
+
export type DialogBounds = { left: number; top: number; width: number };
|
|
45
|
+
type Hitbox = { row: number; startCol: number; endCol: number };
|
|
46
|
+
|
|
47
|
+
export function escCloseHitbox(bounds: DialogBounds): Hitbox {
|
|
48
|
+
return {
|
|
49
|
+
row: bounds.top + 2,
|
|
50
|
+
startCol: bounds.left + bounds.width - 5,
|
|
51
|
+
endCol: bounds.left + bounds.width - 1,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function hasActiveTextPreview(): boolean {
|
|
55
|
+
return overlayManager.hasActive();
|
|
56
|
+
}
|
|
57
|
+
export function formatTokens(tokens: number): string {
|
|
58
|
+
return tokens < 1_000
|
|
59
|
+
? String(tokens)
|
|
60
|
+
: tokens < 100_000
|
|
61
|
+
? `${(tokens / 1_000).toFixed(1)}k`
|
|
62
|
+
: `${Math.round(tokens / 1_000)}k`;
|
|
63
|
+
}
|
|
64
|
+
function normalizePreviewText(text: string): string {
|
|
65
|
+
return text
|
|
66
|
+
.replace(/\r\n?/g, "\n")
|
|
67
|
+
.replace(/\t/g, " ")
|
|
68
|
+
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "");
|
|
69
|
+
}
|
|
70
|
+
function ensureFullscreenMouseMotion(tui: TUI): void {
|
|
71
|
+
if (tui.mode === "fullscreen") {
|
|
72
|
+
tui.terminal.write("\x1b[?1003h\x1b[?1006h");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function contains(
|
|
76
|
+
hitbox: Hitbox | undefined,
|
|
77
|
+
row: number,
|
|
78
|
+
col: number,
|
|
79
|
+
): boolean {
|
|
80
|
+
return Boolean(
|
|
81
|
+
hitbox &&
|
|
82
|
+
row === hitbox.row &&
|
|
83
|
+
col >= hitbox.startCol &&
|
|
84
|
+
col <= hitbox.endCol,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Markdown 预览的滚动、鼠标命中与视觉状态完全属于视图。 */
|
|
89
|
+
export async function showTextPreview(
|
|
90
|
+
ctx: Pick<ExtensionCommandContext, "ui">,
|
|
91
|
+
title: string,
|
|
92
|
+
rawContent: string,
|
|
93
|
+
overlays: OverlayManager = overlayManager,
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
const content = normalizePreviewText(rawContent);
|
|
96
|
+
await overlays.open(
|
|
97
|
+
ctx,
|
|
98
|
+
(tui, theme, _keybindings, done) => {
|
|
99
|
+
ensureFullscreenMouseMotion(tui);
|
|
100
|
+
let scrollOffset = 0;
|
|
101
|
+
let pageSize = 1;
|
|
102
|
+
let totalLines = 1;
|
|
103
|
+
let escHovered = false;
|
|
104
|
+
let escHitbox: Hitbox | undefined;
|
|
105
|
+
let scrollbarHitbox:
|
|
106
|
+
| {
|
|
107
|
+
col: number;
|
|
108
|
+
startRow: number;
|
|
109
|
+
endRow: number;
|
|
110
|
+
thumbStart: number;
|
|
111
|
+
thumbSize: number;
|
|
112
|
+
maxOffset: number;
|
|
113
|
+
}
|
|
114
|
+
| undefined;
|
|
115
|
+
let scrollbarDragOffset: number | null = null;
|
|
116
|
+
const markdown = new Markdown(content, 0, 0, getMarkdownTheme());
|
|
117
|
+
const scrollTo = (offset: number): void => {
|
|
118
|
+
const next = Math.max(
|
|
119
|
+
0,
|
|
120
|
+
Math.min(offset, Math.max(0, totalLines - pageSize)),
|
|
121
|
+
);
|
|
122
|
+
if (next !== scrollOffset) {
|
|
123
|
+
scrollOffset = next;
|
|
124
|
+
tui.requestRender();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const dragScrollbarTo = (row: number): void => {
|
|
128
|
+
if (!scrollbarHitbox || scrollbarDragOffset === null) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const trackSize = scrollbarHitbox.endRow - scrollbarHitbox.startRow + 1;
|
|
132
|
+
const maxThumbStart = Math.max(
|
|
133
|
+
0,
|
|
134
|
+
trackSize - scrollbarHitbox.thumbSize,
|
|
135
|
+
);
|
|
136
|
+
const start = Math.max(
|
|
137
|
+
0,
|
|
138
|
+
Math.min(
|
|
139
|
+
row - scrollbarHitbox.startRow - scrollbarDragOffset,
|
|
140
|
+
maxThumbStart,
|
|
141
|
+
),
|
|
142
|
+
);
|
|
143
|
+
scrollTo(
|
|
144
|
+
maxThumbStart > 0
|
|
145
|
+
? Math.round((start / maxThumbStart) * scrollbarHitbox.maxOffset)
|
|
146
|
+
: 0,
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
return {
|
|
150
|
+
invalidate() {
|
|
151
|
+
markdown.invalidate();
|
|
152
|
+
},
|
|
153
|
+
handleInput(data: string) {
|
|
154
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
|
|
155
|
+
done(undefined);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (matchesKey(data, Key.up)) {
|
|
159
|
+
scrollTo(scrollOffset - 1);
|
|
160
|
+
} else if (matchesKey(data, Key.down)) {
|
|
161
|
+
scrollTo(scrollOffset + 1);
|
|
162
|
+
} else if (matchesKey(data, "pageUp")) {
|
|
163
|
+
scrollTo(scrollOffset - pageSize);
|
|
164
|
+
} else if (matchesKey(data, "pageDown")) {
|
|
165
|
+
scrollTo(scrollOffset + pageSize);
|
|
166
|
+
} else if (matchesKey(data, Key.home)) {
|
|
167
|
+
scrollTo(0);
|
|
168
|
+
} else if (matchesKey(data, Key.end)) {
|
|
169
|
+
scrollTo(totalLines - pageSize);
|
|
170
|
+
} else {
|
|
171
|
+
const mouse = parseSgrMousePacket(data);
|
|
172
|
+
if (!mouse) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (mouse.final === "m") {
|
|
176
|
+
scrollbarDragOffset = null;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const overEsc = contains(escHitbox, mouse.row, mouse.col);
|
|
180
|
+
if (overEsc !== escHovered) {
|
|
181
|
+
escHovered = overEsc;
|
|
182
|
+
tui.requestRender();
|
|
183
|
+
}
|
|
184
|
+
const button = mouseBaseButton(mouse.code);
|
|
185
|
+
const motion = (mouse.code & 32) !== 0;
|
|
186
|
+
if (button === 0 && !motion) {
|
|
187
|
+
if (overEsc) {
|
|
188
|
+
done(undefined);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (
|
|
192
|
+
scrollbarHitbox &&
|
|
193
|
+
mouse.col === scrollbarHitbox.col &&
|
|
194
|
+
mouse.row >= scrollbarHitbox.startRow &&
|
|
195
|
+
mouse.row <= scrollbarHitbox.endRow
|
|
196
|
+
) {
|
|
197
|
+
const row = mouse.row - scrollbarHitbox.startRow;
|
|
198
|
+
const inThumb =
|
|
199
|
+
row >= scrollbarHitbox.thumbStart &&
|
|
200
|
+
row < scrollbarHitbox.thumbStart + scrollbarHitbox.thumbSize;
|
|
201
|
+
scrollbarDragOffset = inThumb
|
|
202
|
+
? row - scrollbarHitbox.thumbStart
|
|
203
|
+
: Math.floor(scrollbarHitbox.thumbSize / 2);
|
|
204
|
+
dragScrollbarTo(mouse.row);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (motion && scrollbarDragOffset !== null) {
|
|
209
|
+
dragScrollbarTo(mouse.row);
|
|
210
|
+
} else if (button === 64) {
|
|
211
|
+
scrollTo(scrollOffset - 3);
|
|
212
|
+
} else if (button === 65) {
|
|
213
|
+
scrollTo(scrollOffset + 3);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
render(width: number) {
|
|
218
|
+
const inner = Math.max(1, width - 2);
|
|
219
|
+
const escWidth = visibleWidth("[esc]");
|
|
220
|
+
const bodyInner = Math.max(1, inner - 1);
|
|
221
|
+
const terminalHeight = Math.max(1, tui.terminal.rows);
|
|
222
|
+
const availableHeight = Math.max(1, terminalHeight - 4);
|
|
223
|
+
const viewportHeight = Math.min(
|
|
224
|
+
30,
|
|
225
|
+
Math.max(1, Math.floor(terminalHeight * 0.8)),
|
|
226
|
+
availableHeight,
|
|
227
|
+
);
|
|
228
|
+
pageSize = Math.max(1, viewportHeight - 6);
|
|
229
|
+
const wrapped = markdown.render(Math.max(1, bodyInner - 1));
|
|
230
|
+
totalLines = wrapped.length;
|
|
231
|
+
scrollOffset = Math.min(
|
|
232
|
+
scrollOffset,
|
|
233
|
+
Math.max(0, totalLines - pageSize),
|
|
234
|
+
);
|
|
235
|
+
const overlayTop =
|
|
236
|
+
2 + Math.floor((availableHeight - viewportHeight) / 2);
|
|
237
|
+
const overlayLeft = Math.floor(
|
|
238
|
+
(Math.max(1, tui.terminal.columns) - width) / 2,
|
|
239
|
+
);
|
|
240
|
+
escHitbox = escCloseHitbox({
|
|
241
|
+
left: overlayLeft,
|
|
242
|
+
top: overlayTop,
|
|
243
|
+
width,
|
|
244
|
+
});
|
|
245
|
+
const visible = wrapped.slice(scrollOffset, scrollOffset + pageSize);
|
|
246
|
+
const border = (text: string) => theme.fg("border", text);
|
|
247
|
+
const scrollable = totalLines > pageSize;
|
|
248
|
+
const thumbSize = scrollable
|
|
249
|
+
? Math.max(1, Math.floor((pageSize * pageSize) / totalLines))
|
|
250
|
+
: 0;
|
|
251
|
+
const maxOffset = Math.max(0, totalLines - pageSize);
|
|
252
|
+
const thumbStart =
|
|
253
|
+
scrollable && maxOffset > 0
|
|
254
|
+
? Math.round((scrollOffset / maxOffset) * (pageSize - thumbSize))
|
|
255
|
+
: 0;
|
|
256
|
+
scrollbarHitbox = scrollable
|
|
257
|
+
? {
|
|
258
|
+
col: overlayLeft + width - 1,
|
|
259
|
+
startRow: overlayTop + 4,
|
|
260
|
+
endRow: overlayTop + 3 + pageSize,
|
|
261
|
+
thumbStart,
|
|
262
|
+
thumbSize,
|
|
263
|
+
maxOffset,
|
|
264
|
+
}
|
|
265
|
+
: undefined;
|
|
266
|
+
const bodyRows = Array.from({ length: pageSize }, (_, row) => {
|
|
267
|
+
const inThumb = row >= thumbStart && row < thumbStart + thumbSize;
|
|
268
|
+
const scrollbar = scrollable
|
|
269
|
+
? theme.fg(
|
|
270
|
+
inThumb ? "accent" : "borderMuted",
|
|
271
|
+
inThumb ? "█" : "│",
|
|
272
|
+
)
|
|
273
|
+
: " ";
|
|
274
|
+
return `${border("│")}${padLine(` ${visible[row] ?? ""}`, bodyInner)}${scrollbar}${border("│")}`;
|
|
275
|
+
});
|
|
276
|
+
const status = `${totalLines === 0 ? 0 : scrollOffset + 1}-${Math.min(totalLines, scrollOffset + pageSize)} / ${totalLines} lines · ↑↓ PgUp/PgDn Home/End · [esc] close`;
|
|
277
|
+
return [
|
|
278
|
+
border(`╭${"─".repeat(inner)}╮`),
|
|
279
|
+
`${border("│")}${padLine(` ${theme.bold(theme.fg("accent", title))}`, inner - escWidth)}${theme.fg(escHovered ? "text" : "muted", "[esc]")}${border("│")}`,
|
|
280
|
+
`${border("├")}${border("─".repeat(inner))}${border("┤")}`,
|
|
281
|
+
...bodyRows,
|
|
282
|
+
`${border("├")}${border("─".repeat(inner))}${border("┤")}`,
|
|
283
|
+
`${border("│")}${padLine(theme.fg("dim", ` ${status}`), inner)}${border("│")}`,
|
|
284
|
+
border(`╰${"─".repeat(inner)}╯`),
|
|
285
|
+
];
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
overlayOptions: {
|
|
291
|
+
anchor: "center",
|
|
292
|
+
width: "85%",
|
|
293
|
+
minWidth: 50,
|
|
294
|
+
maxHeight: "80%",
|
|
295
|
+
margin: 2,
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const previewLabels = [
|
|
302
|
+
["systemPrompt", "System prompt", "System Prompt"],
|
|
303
|
+
["memoryFiles", "Memory", "Memory Files"],
|
|
304
|
+
["skills", "Skills", "Skills"],
|
|
305
|
+
["tools", "Tools definition", "Tools definition"],
|
|
306
|
+
["toolResults", "Tool results", "Tool Results"],
|
|
307
|
+
["contextFiles", "Context", "Context"],
|
|
308
|
+
] as const;
|
|
309
|
+
|
|
310
|
+
export async function showContextInspector(
|
|
311
|
+
ctx: ExtensionCommandContext,
|
|
312
|
+
data: ContextInspectorData,
|
|
313
|
+
overlays: OverlayManager = overlayManager,
|
|
314
|
+
): Promise<void> {
|
|
315
|
+
const { parts, used, contextWindow } = data;
|
|
316
|
+
const previews = previewLabels
|
|
317
|
+
.filter(([, label]) => parts.some((part) => part.label === label))
|
|
318
|
+
.map(([key, label, title]) => ({
|
|
319
|
+
key,
|
|
320
|
+
label,
|
|
321
|
+
title,
|
|
322
|
+
content: normalizePreviewText(data.previews[key]),
|
|
323
|
+
}));
|
|
324
|
+
let selectedIndex = 0;
|
|
325
|
+
const isCurrent = overlays.sessionGuard();
|
|
326
|
+
while (isCurrent()) {
|
|
327
|
+
const action = await overlays.open<ContextPreviewKey>(
|
|
328
|
+
ctx,
|
|
329
|
+
(tui, theme, _keybindings, done) => {
|
|
330
|
+
ensureFullscreenMouseMotion(tui);
|
|
331
|
+
let previewHitboxes: Array<Hitbox & { key: ContextPreviewKey }> = [];
|
|
332
|
+
let escHitbox: Hitbox | undefined;
|
|
333
|
+
let escHovered = false;
|
|
334
|
+
let hoveredKey: ContextPreviewKey | undefined;
|
|
335
|
+
return {
|
|
336
|
+
invalidate() {},
|
|
337
|
+
handleInput(input: string) {
|
|
338
|
+
if (
|
|
339
|
+
matchesKey(input, Key.escape) ||
|
|
340
|
+
matchesKey(input, Key.ctrl("c"))
|
|
341
|
+
) {
|
|
342
|
+
done(undefined);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (
|
|
346
|
+
previews.length > 0 &&
|
|
347
|
+
(matchesKey(input, Key.up) || matchesKey(input, Key.down))
|
|
348
|
+
) {
|
|
349
|
+
selectedIndex =
|
|
350
|
+
(selectedIndex +
|
|
351
|
+
(matchesKey(input, Key.up) ? -1 : 1) +
|
|
352
|
+
previews.length) %
|
|
353
|
+
previews.length;
|
|
354
|
+
tui.requestRender();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (matchesKey(input, Key.enter)) {
|
|
358
|
+
done(previews[selectedIndex]?.key);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const mouse = parseSgrMousePacket(input);
|
|
362
|
+
if (!mouse || mouse.final !== "M") {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const overEsc = contains(escHitbox, mouse.row, mouse.col);
|
|
366
|
+
const hitbox = previewHitboxes.find((candidate) =>
|
|
367
|
+
contains(candidate, mouse.row, mouse.col),
|
|
368
|
+
);
|
|
369
|
+
if ((mouse.code & 32) !== 0) {
|
|
370
|
+
if (overEsc !== escHovered || hitbox?.key !== hoveredKey) {
|
|
371
|
+
escHovered = overEsc;
|
|
372
|
+
hoveredKey = hitbox?.key;
|
|
373
|
+
tui.requestRender();
|
|
374
|
+
}
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (mouseBaseButton(mouse.code) !== 0) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (overEsc) {
|
|
381
|
+
done(undefined);
|
|
382
|
+
} else if (hitbox) {
|
|
383
|
+
selectedIndex = Math.max(
|
|
384
|
+
0,
|
|
385
|
+
previews.findIndex((preview) => preview.key === hitbox.key),
|
|
386
|
+
);
|
|
387
|
+
done(hitbox.key);
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
render(width: number) {
|
|
391
|
+
const inner = Math.max(1, width - 2);
|
|
392
|
+
const percent =
|
|
393
|
+
contextWindow > 0 ? (used / contextWindow) * 100 : 0;
|
|
394
|
+
const title = theme.bold(theme.fg("accent", "Context Usage"));
|
|
395
|
+
const subtitle = `${formatTokens(used)} / ${formatTokens(contextWindow)} tokens (${percent.toFixed(1)}%)`;
|
|
396
|
+
const barWidth = Math.max(1, Math.min(60, inner - 2));
|
|
397
|
+
let remaining = barWidth;
|
|
398
|
+
const segments = parts
|
|
399
|
+
.map((part, index) => {
|
|
400
|
+
const cells =
|
|
401
|
+
index === parts.length - 1
|
|
402
|
+
? remaining
|
|
403
|
+
: Math.min(
|
|
404
|
+
remaining,
|
|
405
|
+
Math.round(
|
|
406
|
+
(part.tokens / Math.max(1, contextWindow)) * barWidth,
|
|
407
|
+
),
|
|
408
|
+
);
|
|
409
|
+
remaining -= cells;
|
|
410
|
+
return theme.fg(part.color, "█".repeat(Math.max(0, cells)));
|
|
411
|
+
})
|
|
412
|
+
.join("");
|
|
413
|
+
const labelWidth = Math.min(
|
|
414
|
+
24,
|
|
415
|
+
Math.max(...parts.map((part) => part.label.length)),
|
|
416
|
+
);
|
|
417
|
+
const rows = parts.map((part) => {
|
|
418
|
+
const pct =
|
|
419
|
+
contextWindow > 0 ? (part.tokens / contextWindow) * 100 : 0;
|
|
420
|
+
const selected = part.label === previews[selectedIndex]?.label;
|
|
421
|
+
const hovered =
|
|
422
|
+
part.label ===
|
|
423
|
+
previews.find((preview) => preview.key === hoveredKey)?.label;
|
|
424
|
+
const amount = `${formatTokens(part.tokens).padStart(7)} ${pct.toFixed(1).padStart(5)}%`;
|
|
425
|
+
const row = padLine(
|
|
426
|
+
`${selected ? "› " : " "}${theme.fg(part.color, "■")} ${part.label.padEnd(labelWidth)} ${amount}`,
|
|
427
|
+
inner,
|
|
428
|
+
);
|
|
429
|
+
return selected
|
|
430
|
+
? theme.bg("selectedBg", row)
|
|
431
|
+
: hovered
|
|
432
|
+
? theme.bg("customMessageBg", row)
|
|
433
|
+
: row;
|
|
434
|
+
});
|
|
435
|
+
const border = (text: string) => theme.fg("border", text);
|
|
436
|
+
const lines = [
|
|
437
|
+
border(`╭${"─".repeat(inner)}╮`),
|
|
438
|
+
`${border("│")}${padLine(` ${title} ${theme.fg("muted", subtitle)}`, inner - visibleWidth("[esc]"))}${theme.fg(escHovered ? "text" : "muted", "[esc]")}${border("│")}`,
|
|
439
|
+
`${border("├")}${border("─".repeat(inner))}${border("┤")}`,
|
|
440
|
+
`${border("│")}${padLine(` ${segments}`, inner)}${border("│")}`,
|
|
441
|
+
`${border("│")}${" ".repeat(inner)}${border("│")}`,
|
|
442
|
+
...rows.map((row) => `${border("│")}${row}${border("│")}`),
|
|
443
|
+
`${border("├")}${border("─".repeat(inner))}${border("┤")}`,
|
|
444
|
+
`${border("│")}${padLine(theme.fg("dim", " ↑↓ select · Click / Enter to preview · [esc] close"), inner)}${border("│")}`,
|
|
445
|
+
border(`╰${"─".repeat(inner)}╯`),
|
|
446
|
+
];
|
|
447
|
+
const height = Math.max(1, tui.terminal.rows);
|
|
448
|
+
const visibleHeight = Math.min(
|
|
449
|
+
lines.length,
|
|
450
|
+
Math.max(1, Math.floor(height * 0.9)),
|
|
451
|
+
Math.max(1, height - 2),
|
|
452
|
+
);
|
|
453
|
+
const top =
|
|
454
|
+
1 + Math.floor((Math.max(1, height - 2) - visibleHeight) / 2);
|
|
455
|
+
const left = Math.floor(
|
|
456
|
+
(Math.max(1, tui.terminal.columns) - width) / 2,
|
|
457
|
+
);
|
|
458
|
+
escHitbox = escCloseHitbox({ left, top, width });
|
|
459
|
+
previewHitboxes = previews.flatMap((preview) => {
|
|
460
|
+
const index = parts.findIndex(
|
|
461
|
+
(part) => part.label === preview.label,
|
|
462
|
+
);
|
|
463
|
+
const line = 5 + index;
|
|
464
|
+
return index >= 0 && line < visibleHeight
|
|
465
|
+
? [
|
|
466
|
+
{
|
|
467
|
+
key: preview.key,
|
|
468
|
+
row: top + line + 1,
|
|
469
|
+
startCol: left + 1,
|
|
470
|
+
endCol: left + width,
|
|
471
|
+
},
|
|
472
|
+
]
|
|
473
|
+
: [];
|
|
474
|
+
});
|
|
475
|
+
return lines;
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
overlayOptions: {
|
|
481
|
+
anchor: "center",
|
|
482
|
+
width: 64,
|
|
483
|
+
minWidth: 44,
|
|
484
|
+
maxHeight: "90%",
|
|
485
|
+
margin: 1,
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
);
|
|
489
|
+
if (!action) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const preview = previews.find((entry) => entry.key === action);
|
|
493
|
+
if (preview) {
|
|
494
|
+
await showTextPreview(ctx, preview.title, preview.content, overlays);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
@@ -3,16 +3,16 @@ import {
|
|
|
3
3
|
SettingsSelectorComponent,
|
|
4
4
|
type Theme,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import type { ZentuiConfig } from "../../app/config/shell.ts";
|
|
7
|
+
import {
|
|
8
|
+
installPrototypePatch,
|
|
9
|
+
removePrototypePatch,
|
|
10
|
+
} from "../../app/runtime/prototype-patch-registry.ts";
|
|
6
11
|
import {
|
|
7
12
|
EDITOR_BORDER_FALLBACK,
|
|
8
13
|
renderEditorBorder,
|
|
9
14
|
renderThemeStyleOrFallback,
|
|
10
15
|
} from "../../shared/style.ts";
|
|
11
|
-
import type { ZentuiConfig } from "../config/shell.ts";
|
|
12
|
-
import {
|
|
13
|
-
installPrototypePatch,
|
|
14
|
-
removePrototypePatch,
|
|
15
|
-
} from "../ownership/prototype-patch-registry.ts";
|
|
16
16
|
|
|
17
17
|
type PatchableSelectorPrototype = {
|
|
18
18
|
render: (width: number) => string[];
|
|
@@ -2,7 +2,7 @@ import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import {
|
|
3
3
|
hasUnsupportedComponentStyle,
|
|
4
4
|
type PolishedTuiConfig,
|
|
5
|
-
} from "
|
|
5
|
+
} from "../../app/config/shell.ts";
|
|
6
6
|
import { installSelectorBorderStyle } from "./selector-border.ts";
|
|
7
7
|
|
|
8
8
|
/**
|