@xzzpig/pi-starline 0.5.1 → 0.7.2
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/FORK-CHANGELOG.md +26 -0
- package/README.md +68 -181
- package/extensions/starline/config.ts +156 -326
- package/extensions/starline/context-live.ts +33 -0
- package/extensions/starline/editor-cursor.ts +3 -0
- package/extensions/starline/extension-status.ts +9 -5
- package/extensions/starline/footer.ts +331 -528
- package/extensions/starline/fork-editor-cursor.ts +14 -0
- package/extensions/starline/fork-pill-legibility.ts +52 -0
- package/extensions/starline/format.ts +151 -20
- package/extensions/starline/git-live.ts +20 -0
- package/extensions/starline/git-probe.ts +40 -0
- package/extensions/starline/git-segments.ts +175 -0
- package/extensions/starline/git.ts +71 -0
- package/extensions/starline/icons.ts +17 -0
- package/extensions/starline/index.ts +188 -91
- package/extensions/starline/live-session.ts +164 -0
- package/extensions/starline/live-tps.ts +165 -0
- package/extensions/starline/mouse/bubble-dismiss.ts +82 -0
- package/extensions/starline/mouse/cursor-marker.ts +69 -0
- package/extensions/starline/mouse/editor-hit-test.ts +6 -3
- package/extensions/starline/mouse/external-layer.ts +42 -0
- package/extensions/starline/mouse/fork-native-clipboard.ts +57 -0
- package/extensions/starline/mouse/fork-pill-mouse.ts +19 -0
- package/extensions/starline/mouse/fork-pointer-coordination.ts +10 -0
- package/extensions/starline/mouse/fork-transcript-copy.ts +54 -0
- package/extensions/starline/mouse/fork-wheel-compat.ts +22 -0
- package/extensions/starline/mouse/hint.ts +4 -4
- package/extensions/starline/mouse/index.ts +54 -24
- package/extensions/starline/mouse/pointer-events.ts +73 -0
- package/extensions/starline/mouse/pointer-shape.ts +138 -0
- package/extensions/starline/mouse/transcript-copy.ts +76 -33
- package/extensions/starline/paste-collapse.ts +52 -13
- package/extensions/starline/pill-actions.ts +590 -0
- package/extensions/starline/pill-anchors.ts +26 -0
- package/extensions/starline/pill-api.ts +80 -0
- package/extensions/starline/pill-bubble.ts +611 -0
- package/extensions/starline/pill-config.ts +67 -9
- package/extensions/starline/pill.ts +121 -25
- package/extensions/starline/project-probe.ts +79 -0
- package/extensions/starline/prototype-patch-registry.ts +6 -1
- package/extensions/starline/queue-rows.ts +43 -0
- package/extensions/starline/settings-command.ts +124 -177
- package/extensions/starline/subagent-fleet.ts +246 -0
- package/extensions/starline/turn-clock.ts +81 -0
- package/extensions/starline/ui.ts +297 -156
- package/extensions/starline/working-indicator.ts +189 -0
- package/package.json +7 -6
- package/types/pill-api.d.ts +115 -0
- package/extensions/starline/editor-metadata-format.ts +0 -259
- package/extensions/starline/footer-format.ts +0 -279
|
@@ -26,6 +26,8 @@ export type PillConfig = {
|
|
|
26
26
|
separator: PillSeparator;
|
|
27
27
|
bold: boolean;
|
|
28
28
|
caps: PillCaps;
|
|
29
|
+
/** Unfold a pill's bubble upward from the pill instead of showing it at once. */
|
|
30
|
+
bubbleAnimation: boolean;
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
export const EXTENSION_STATUS_SEGMENT = "extensionStatus";
|
|
@@ -35,18 +37,17 @@ export const EXTENSION_STATUS_PREFIX = `${EXTENSION_STATUS_SEGMENT}:`;
|
|
|
35
37
|
export const PILL_SEGMENT_VARIABLES: Record<string, string> = {
|
|
36
38
|
model: "model",
|
|
37
39
|
thinking: "thinking",
|
|
40
|
+
queue: "queue",
|
|
41
|
+
subagents: "subagents",
|
|
38
42
|
cwd: "cwd",
|
|
39
43
|
sessionName: "session_name",
|
|
40
|
-
|
|
41
|
-
gitStatus: "git_status",
|
|
42
|
-
gitState: "git_state",
|
|
44
|
+
git: "git",
|
|
43
45
|
gitCommit: "git_commit",
|
|
44
46
|
gitTag: "git_tag",
|
|
45
47
|
gitMetrics: "git_metrics",
|
|
46
48
|
runtime: "runtime",
|
|
47
49
|
context: "context",
|
|
48
|
-
|
|
49
|
-
cacheHit: "cache_hit",
|
|
50
|
+
tps: "tps",
|
|
50
51
|
cost: "cost",
|
|
51
52
|
sessionDuration: "session_duration",
|
|
52
53
|
username: "username",
|
|
@@ -59,16 +60,19 @@ export const DEFAULT_PILL_CONFIG: PillConfig = {
|
|
|
59
60
|
segments: [
|
|
60
61
|
"model",
|
|
61
62
|
"thinking",
|
|
63
|
+
"queue",
|
|
64
|
+
"subagents",
|
|
62
65
|
"cwd",
|
|
63
|
-
"
|
|
64
|
-
"gitStatus",
|
|
66
|
+
"git",
|
|
65
67
|
"context",
|
|
68
|
+
"tps",
|
|
66
69
|
"cost",
|
|
67
70
|
EXTENSION_STATUS_SEGMENT,
|
|
68
71
|
],
|
|
69
72
|
separator: "powerline",
|
|
70
73
|
bold: true,
|
|
71
74
|
caps: "round",
|
|
75
|
+
bubbleAnimation: true,
|
|
72
76
|
};
|
|
73
77
|
|
|
74
78
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -98,15 +102,65 @@ export function isKnownPillSegment(name: string): boolean {
|
|
|
98
102
|
return name in PILL_SEGMENT_VARIABLES;
|
|
99
103
|
}
|
|
100
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Every segment name `/starline` can put on the bar, in a fixed order so the
|
|
107
|
+
* rows do not move under the cursor when a position changes. Per-status
|
|
108
|
+
* `extensionStatus:<key>` entries are not listed here: they depend on what other
|
|
109
|
+
* extensions published this session, and the *Pills* tab appends a row for each
|
|
110
|
+
* one after these.
|
|
111
|
+
*/
|
|
112
|
+
export const PILL_SEGMENT_NAMES: readonly string[] = [
|
|
113
|
+
...Object.keys(PILL_SEGMENT_VARIABLES),
|
|
114
|
+
EXTENSION_STATUS_SEGMENT,
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
/** 1-based position of a segment in the bar, or 0 when it is not drawn. */
|
|
118
|
+
export function pillSegmentPosition(segments: readonly string[], name: string): number {
|
|
119
|
+
return segments.indexOf(name) + 1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Apply a position edit to `pill.segments`.
|
|
124
|
+
*
|
|
125
|
+
* `value` is `off` or a 1-based position. A position is where the segment ends
|
|
126
|
+
* up: everything already there shifts along, and a position past the end
|
|
127
|
+
* appends. Moving the segment that is already in the bar works the same way,
|
|
128
|
+
* because it is removed before the index is applied.
|
|
129
|
+
*/
|
|
130
|
+
export function movePillSegment(
|
|
131
|
+
segments: readonly string[],
|
|
132
|
+
name: string,
|
|
133
|
+
value: string,
|
|
134
|
+
): string[] {
|
|
135
|
+
const rest = segments.filter((segment) => segment !== name);
|
|
136
|
+
if (value === "off") return rest;
|
|
137
|
+
const position = Number(value);
|
|
138
|
+
if (!Number.isInteger(position) || position < 1) return [...segments];
|
|
139
|
+
const index = Math.min(position - 1, rest.length);
|
|
140
|
+
return [...rest.slice(0, index), name, ...rest.slice(index)];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Segment names that became part of another one. */
|
|
144
|
+
const LEGACY_SEGMENT_NAMES: Record<string, string> = {
|
|
145
|
+
gitBranch: "git",
|
|
146
|
+
gitStatus: "git",
|
|
147
|
+
gitState: "git",
|
|
148
|
+
tokens: "context",
|
|
149
|
+
cacheHit: "context",
|
|
150
|
+
};
|
|
151
|
+
|
|
101
152
|
function parseSegments(value: unknown): string[] {
|
|
102
153
|
if (!Array.isArray(value)) return DEFAULT_PILL_CONFIG.segments;
|
|
103
154
|
const segments: string[] = [];
|
|
104
155
|
for (const entry of value) {
|
|
105
156
|
if (typeof entry !== "string") continue;
|
|
106
|
-
|
|
157
|
+
// A bar that listed the branch and the dirty block separately now draws
|
|
158
|
+
// both in one pill, so the old names collapse into it instead of vanishing.
|
|
159
|
+
const renamed = LEGACY_SEGMENT_NAMES[entry.trim()] ?? entry.trim();
|
|
107
160
|
// Unknown names are dropped rather than rendered as empty pills, so a
|
|
108
161
|
// typo shortens the bar instead of punching a hole in it.
|
|
109
|
-
if (
|
|
162
|
+
if (renamed && isKnownPillSegment(renamed) && !segments.includes(renamed))
|
|
163
|
+
segments.push(renamed);
|
|
110
164
|
}
|
|
111
165
|
return segments;
|
|
112
166
|
}
|
|
@@ -118,5 +172,9 @@ export function normalizePillConfig(value: unknown): PillConfig {
|
|
|
118
172
|
separator: parseSeparator(value.separator),
|
|
119
173
|
bold: typeof value.bold === "boolean" ? value.bold : DEFAULT_PILL_CONFIG.bold,
|
|
120
174
|
caps: parseCaps(value.caps),
|
|
175
|
+
bubbleAnimation:
|
|
176
|
+
typeof value.bubbleAnimation === "boolean"
|
|
177
|
+
? value.bubbleAnimation
|
|
178
|
+
: DEFAULT_PILL_CONFIG.bubbleAnimation,
|
|
121
179
|
};
|
|
122
180
|
}
|
|
@@ -13,6 +13,7 @@ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
|
13
13
|
|
|
14
14
|
import type { ColorSource, ColorSpec } from "./config";
|
|
15
15
|
import type { ExtensionStatusSegment } from "./extension-status";
|
|
16
|
+
import { hasExplicitForeground, reverseVideoSgr } from "./fork-pill-legibility";
|
|
16
17
|
import {
|
|
17
18
|
EXTENSION_STATUS_PREFIX,
|
|
18
19
|
EXTENSION_STATUS_SEGMENT,
|
|
@@ -25,9 +26,13 @@ const RESET = "\x1b[0m";
|
|
|
25
26
|
const BOLD = "\x1b[1m";
|
|
26
27
|
const DEFAULT_BG = "\x1b[49m";
|
|
27
28
|
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Text colours are the 256-colour cube's black and white (16/231), whose values
|
|
31
|
+
* the standard fixes. The basic named codes are not fixed: a terminal theme may
|
|
32
|
+
* map ANSI black to a mid grey, which leaves mid-tone pills hard to read.
|
|
33
|
+
*/
|
|
34
|
+
const TEXT_DARK = "\x1b[38;5;16m";
|
|
35
|
+
const TEXT_LIGHT = "\x1b[38;5;231m";
|
|
31
36
|
|
|
32
37
|
/** Catppuccin surface1 — the neutral for segments whose spec names no colour. */
|
|
33
38
|
const NEUTRAL_BG = "\x1b[48;2;69;71;90m";
|
|
@@ -43,6 +48,42 @@ const CAP_GLYPHS = { left: "", right: "" };
|
|
|
43
48
|
/** Divider used where a solid arrow would be invisible. */
|
|
44
49
|
const THIN_SEPARATOR = SEPARATOR_GLYPHS["powerline-thin"];
|
|
45
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Where one pill ended up in a rendered bar, in visible columns.
|
|
53
|
+
*
|
|
54
|
+
* `startColumn` is inclusive, `endColumn` exclusive. Columns count what the
|
|
55
|
+
* terminal draws, so a CJK pill spans two cells per character and a pill that
|
|
56
|
+
* `width` cut in half still reports the cells it kept.
|
|
57
|
+
*
|
|
58
|
+
* A span covers everything the pill visually owns: its own padding, the
|
|
59
|
+
* separator that points out of it, and — on the two ends of the bar — the cap.
|
|
60
|
+
* That leaves no dead cells between two pills for a click to fall into.
|
|
61
|
+
*/
|
|
62
|
+
export type PillSpan = {
|
|
63
|
+
/** `PillInput.key` of the segment this span belongs to. */
|
|
64
|
+
key: string;
|
|
65
|
+
startColumn: number;
|
|
66
|
+
endColumn: number;
|
|
67
|
+
/**
|
|
68
|
+
* First column painted solid in the pill's colour. The hit area starts earlier
|
|
69
|
+
* on the first pill, whose round cap is the colour drawn on the terminal's own
|
|
70
|
+
* background; anything that has to line up with the pill's edge uses this.
|
|
71
|
+
*/
|
|
72
|
+
bodyStartColumn: number;
|
|
73
|
+
/** First column after the pill's own colour: where its arrow or cap starts. */
|
|
74
|
+
bodyEndColumn: number;
|
|
75
|
+
/** Background SGR the pill was painted with, for anything that has to match it. */
|
|
76
|
+
bg: string;
|
|
77
|
+
/** Foreground SGR, or "" when the text carried its own styling. */
|
|
78
|
+
fg: string;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export type PillRenderOptions = {
|
|
82
|
+
ascii?: boolean;
|
|
83
|
+
/** Called once per pill that survived truncation, in left-to-right order. */
|
|
84
|
+
onSpan?: (span: PillSpan) => void;
|
|
85
|
+
};
|
|
86
|
+
|
|
46
87
|
export type PillInput = {
|
|
47
88
|
/** Segment name, for diagnostics. */
|
|
48
89
|
key: string;
|
|
@@ -123,10 +164,13 @@ export function collectPillInputs(
|
|
|
123
164
|
}
|
|
124
165
|
|
|
125
166
|
type PreparedPill = {
|
|
167
|
+
key: string;
|
|
126
168
|
text: string;
|
|
127
169
|
bg: string;
|
|
128
170
|
fg: string;
|
|
129
171
|
bold: boolean;
|
|
172
|
+
/** Body SGR for a terminal-owned background, which paints text in reverse. */
|
|
173
|
+
reverse: string;
|
|
130
174
|
};
|
|
131
175
|
|
|
132
176
|
const XTERM_CUBE_LEVELS = [0, 95, 135, 175, 215, 255];
|
|
@@ -213,18 +257,25 @@ function prepare(
|
|
|
213
257
|
if (!text || visibleWidth(text) === 0) continue;
|
|
214
258
|
|
|
215
259
|
if (input.keepStyling) {
|
|
216
|
-
pills.push({ text, bg: neutral, fg: "", bold: false });
|
|
260
|
+
pills.push({ key: input.key, text, bg: neutral, fg: "", bold: false, reverse: "" });
|
|
217
261
|
continue;
|
|
218
262
|
}
|
|
219
263
|
|
|
220
264
|
const resolved = resolveColorSpec(theme, source, input.spec);
|
|
221
265
|
const bg = resolved.bg ?? resolveBackgroundSgr(theme, source, input.spec) ?? neutral;
|
|
222
|
-
// An explicit fg: in the spec wins
|
|
266
|
+
// An explicit fg: in the spec wins. A background the terminal owns has no
|
|
267
|
+
// knowable RGB, so reverse video hands both sides to the palette instead of
|
|
268
|
+
// guessing from the standard table.
|
|
269
|
+
const explicitFg = hasExplicitForeground(input.spec);
|
|
270
|
+
const reverse = explicitFg ? "" : (reverseVideoSgr(bg) ?? "");
|
|
271
|
+
const specFg = explicitFg || (resolved.bg && resolved.fg) ? resolved.fg : undefined;
|
|
223
272
|
pills.push({
|
|
273
|
+
key: input.key,
|
|
224
274
|
text,
|
|
225
275
|
bg,
|
|
226
|
-
fg:
|
|
276
|
+
fg: reverse ? "" : (specFg ?? contrastTextSgr(bg)),
|
|
227
277
|
bold: config.bold || resolved.bold,
|
|
278
|
+
reverse,
|
|
228
279
|
});
|
|
229
280
|
}
|
|
230
281
|
|
|
@@ -234,6 +285,11 @@ function prepare(
|
|
|
234
285
|
/**
|
|
235
286
|
* Render the pill bar. Returns "" when nothing has content, so the caller can
|
|
236
287
|
* fall back rather than emit a lone pair of caps.
|
|
288
|
+
*
|
|
289
|
+
* `options.onSpan` receives the hit area of every pill the terminal will
|
|
290
|
+
* actually show, measured in the bar's own visible columns. Recording the spans
|
|
291
|
+
* here rather than recomputing them later keeps them in step with the render:
|
|
292
|
+
* colour, caps, separators and truncation are all already accounted for.
|
|
237
293
|
*/
|
|
238
294
|
export function renderPillBar(
|
|
239
295
|
inputs: PillInput[],
|
|
@@ -241,7 +297,7 @@ export function renderPillBar(
|
|
|
241
297
|
source: ColorSource,
|
|
242
298
|
config: PillConfig,
|
|
243
299
|
width: number,
|
|
244
|
-
options:
|
|
300
|
+
options: PillRenderOptions = {},
|
|
245
301
|
): string {
|
|
246
302
|
const pills = prepare(inputs, theme, source, config);
|
|
247
303
|
if (pills.length === 0) return "";
|
|
@@ -255,41 +311,81 @@ export function renderPillBar(
|
|
|
255
311
|
const rightCap = wantsCaps ? CAP_GLYPHS.right : "";
|
|
256
312
|
|
|
257
313
|
let out = "";
|
|
314
|
+
// Columns the terminal will draw so far. Getting this from `visibleWidth`
|
|
315
|
+
// rather than string length is what makes CJK segment text hit the right cells.
|
|
316
|
+
let column = 0;
|
|
317
|
+
const emit = (piece: string) => {
|
|
318
|
+
out += piece;
|
|
319
|
+
column += visibleWidth(piece);
|
|
320
|
+
};
|
|
258
321
|
|
|
322
|
+
const capStart = column;
|
|
259
323
|
const firstBg = pills[0]?.bg;
|
|
260
324
|
if (leftCap && firstBg) {
|
|
261
325
|
const capFg = toForegroundSgr(firstBg);
|
|
262
|
-
if (capFg)
|
|
326
|
+
if (capFg) emit(`${DEFAULT_BG}${capFg}${leftCap}${RESET}`);
|
|
263
327
|
}
|
|
264
328
|
|
|
329
|
+
const spans: PillSpan[] = [];
|
|
330
|
+
|
|
265
331
|
pills.forEach((pill, index) => {
|
|
266
|
-
|
|
332
|
+
// The left cap opens the bar in the first pill's colour, so it belongs to it.
|
|
333
|
+
const startColumn = index === 0 ? capStart : column;
|
|
334
|
+
const bodyStartColumn = column;
|
|
335
|
+
emit(`${pill.reverse || pill.bg}${pill.fg}${pill.bold ? BOLD : ""} ${pill.text} ${RESET}`);
|
|
336
|
+
const bodyEndColumn = column;
|
|
267
337
|
|
|
268
338
|
const next = pills[index + 1];
|
|
269
339
|
if (next) {
|
|
270
|
-
if (!separator) return;
|
|
271
|
-
|
|
272
340
|
// A solid arrow works by being the left colour on the right colour, so
|
|
273
341
|
// it vanishes when the two pills share a background — several segments
|
|
274
342
|
// default to the same theme colour, which merged them into one blob.
|
|
275
343
|
// Fall back to the thin divider in the text colour, which stays visible.
|
|
276
|
-
if (pill.bg === next.bg) {
|
|
277
|
-
|
|
278
|
-
|
|
344
|
+
if (separator && pill.bg === next.bg) {
|
|
345
|
+
const dividerFg = pill.reverse ? "" : pill.fg || contrastTextSgr(pill.bg);
|
|
346
|
+
emit(`${pill.reverse || pill.bg}${dividerFg}${THIN_SEPARATOR}${RESET}`);
|
|
347
|
+
} else if (separator) {
|
|
348
|
+
const arrowFg = toForegroundSgr(pill.bg);
|
|
349
|
+
// Without a usable foreground the arrow would be invisible; letting the
|
|
350
|
+
// backgrounds abut is better than drawing a glyph in the wrong colour.
|
|
351
|
+
if (arrowFg) emit(`${next.bg}${arrowFg}${separator}${RESET}`);
|
|
279
352
|
}
|
|
280
|
-
|
|
281
|
-
const arrowFg = toForegroundSgr(pill.bg);
|
|
282
|
-
// Without a usable foreground the arrow would be invisible; letting the
|
|
283
|
-
// backgrounds abut is better than drawing a glyph in the wrong colour.
|
|
284
|
-
out += arrowFg ? `${next.bg}${arrowFg}${separator}${RESET}` : "";
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (rightCap) {
|
|
353
|
+
} else if (rightCap) {
|
|
289
354
|
const capFg = toForegroundSgr(pill.bg);
|
|
290
|
-
if (capFg)
|
|
355
|
+
if (capFg) emit(`${DEFAULT_BG}${capFg}${rightCap}${RESET}`);
|
|
291
356
|
}
|
|
357
|
+
|
|
358
|
+
// The arrow (or cap) is emitted before the span closes, so it belongs to
|
|
359
|
+
// the pill that points out of itself.
|
|
360
|
+
spans.push({
|
|
361
|
+
key: pill.key,
|
|
362
|
+
startColumn,
|
|
363
|
+
endColumn: column,
|
|
364
|
+
bodyStartColumn,
|
|
365
|
+
bodyEndColumn,
|
|
366
|
+
bg: pill.bg,
|
|
367
|
+
fg: pill.fg,
|
|
368
|
+
});
|
|
292
369
|
});
|
|
293
370
|
|
|
294
|
-
|
|
371
|
+
const rendered = width > 0 ? truncateToWidth(out, width, "") : out;
|
|
372
|
+
if (options.onSpan) {
|
|
373
|
+
// Truncation keeps a prefix, so surviving cells keep their columns; only
|
|
374
|
+
// the far end needs clamping, and a pill clipped away entirely vanishes.
|
|
375
|
+
const visible = visibleWidth(rendered);
|
|
376
|
+
for (const span of spans) {
|
|
377
|
+
const startColumn = Math.min(span.startColumn, visible);
|
|
378
|
+
const endColumn = Math.min(span.endColumn, visible);
|
|
379
|
+
if (endColumn > startColumn) {
|
|
380
|
+
const bodyStartColumn = Math.min(span.bodyStartColumn, endColumn - 1);
|
|
381
|
+
const bodyEndColumn = Math.max(
|
|
382
|
+
bodyStartColumn + 1,
|
|
383
|
+
Math.min(span.bodyEndColumn, endColumn),
|
|
384
|
+
);
|
|
385
|
+
options.onSpan({ ...span, startColumn, endColumn, bodyStartColumn, bodyEndColumn });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return rendered;
|
|
295
391
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One project refresh's reads: git, the runtime in this directory, and the
|
|
3
|
+
* project's own version.
|
|
4
|
+
*
|
|
5
|
+
* Kept out of the extension entry so a test can hand it fake readers and see
|
|
6
|
+
* which of them a given bar actually asks for — the question "does a bar with
|
|
7
|
+
* no git pill still run `git status`?" is not answerable from a closure.
|
|
8
|
+
*
|
|
9
|
+
* The readers are injected rather than imported at the call site for exactly
|
|
10
|
+
* that reason; production passes the real ones.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { PolishedTuiConfig } from "./config";
|
|
14
|
+
import {
|
|
15
|
+
emptyGitStatus,
|
|
16
|
+
type GitHost,
|
|
17
|
+
type GitReadResult,
|
|
18
|
+
readGitHost,
|
|
19
|
+
readGitStatus,
|
|
20
|
+
} from "./git";
|
|
21
|
+
import { gitProbeOptions } from "./git-probe";
|
|
22
|
+
import { type PackageVersionReadResult, readPackageVersionResult } from "./package-version";
|
|
23
|
+
import { type RuntimeReadResult, readRuntimeInfo } from "./runtime";
|
|
24
|
+
|
|
25
|
+
export type ProjectProbe = {
|
|
26
|
+
git: GitReadResult;
|
|
27
|
+
/** Undefined when the bar does not ask for a forge icon: keep the last one. */
|
|
28
|
+
gitHost: GitHost | undefined;
|
|
29
|
+
runtime: RuntimeReadResult;
|
|
30
|
+
/** Undefined when the bar does not show the project version. */
|
|
31
|
+
packageVersion: PackageVersionReadResult | undefined;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type ProjectProbeReaders = {
|
|
35
|
+
readGitStatus: typeof readGitStatus;
|
|
36
|
+
readGitHost: typeof readGitHost;
|
|
37
|
+
readRuntimeInfo: typeof readRuntimeInfo;
|
|
38
|
+
readPackageVersion: typeof readPackageVersionResult;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const REAL_READERS: ProjectProbeReaders = {
|
|
42
|
+
readGitStatus,
|
|
43
|
+
readGitHost,
|
|
44
|
+
readRuntimeInfo,
|
|
45
|
+
readPackageVersion: readPackageVersionResult,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Read what the bar is about to draw, in parallel.
|
|
50
|
+
*
|
|
51
|
+
* A bar with no git pill gets an empty status instead of a read, which also
|
|
52
|
+
* clears the branch and dirty markers the previous bar left in state — the
|
|
53
|
+
* object is reused across refreshes, so skipping the read must not mean keeping
|
|
54
|
+
* yesterday's branch.
|
|
55
|
+
*/
|
|
56
|
+
export async function probeProject(
|
|
57
|
+
cwd: string,
|
|
58
|
+
config: PolishedTuiConfig,
|
|
59
|
+
readers: ProjectProbeReaders = REAL_READERS,
|
|
60
|
+
): Promise<ProjectProbe> {
|
|
61
|
+
const probe = gitProbeOptions(config.pill.segments, {
|
|
62
|
+
showTag: config.git.commit.showTag,
|
|
63
|
+
hostIcon: config.git.hostIcon,
|
|
64
|
+
});
|
|
65
|
+
const wantPackage = config.pill.segments.includes("packageVersion");
|
|
66
|
+
const [git, runtime, packageVersion, gitHost] = await Promise.all([
|
|
67
|
+
probe.enabled
|
|
68
|
+
? readers.readGitStatus(cwd, {
|
|
69
|
+
readExactTag: probe.readExactTag,
|
|
70
|
+
readMetrics: probe.readMetrics,
|
|
71
|
+
ignoreSubmodules: config.git.metrics.ignoreSubmodules,
|
|
72
|
+
})
|
|
73
|
+
: Promise.resolve<GitReadResult>({ kind: "ok", status: emptyGitStatus() }),
|
|
74
|
+
readers.readRuntimeInfo(cwd),
|
|
75
|
+
wantPackage ? readers.readPackageVersion(cwd) : Promise.resolve(undefined),
|
|
76
|
+
probe.readHost ? readers.readGitHost(cwd) : Promise.resolve(undefined),
|
|
77
|
+
]);
|
|
78
|
+
return { git, gitHost, runtime, packageVersion };
|
|
79
|
+
}
|
|
@@ -21,7 +21,12 @@ const LEGACY_PROTOTYPE_PATCH_REGISTRY = Symbol.for("pi-zentui.prototype-patch-re
|
|
|
21
21
|
type PrototypePatchAdapter =
|
|
22
22
|
| "user-message-render"
|
|
23
23
|
| "user-message-invalidate"
|
|
24
|
-
| "selector-border-render"
|
|
24
|
+
| "selector-border-render"
|
|
25
|
+
| "selection-marker-dedupe"
|
|
26
|
+
| "agent-session-capture-prompt"
|
|
27
|
+
| "agent-session-capture-usage"
|
|
28
|
+
| "pending-messages-hide"
|
|
29
|
+
| "mouse-pointer-shape";
|
|
25
30
|
|
|
26
31
|
type PrototypeMethodName = string;
|
|
27
32
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hides Pi's own queued-message rows above the editor while the queue pill is
|
|
3
|
+
* showing the same thing.
|
|
4
|
+
*
|
|
5
|
+
* Pi draws `Steering: …` / `Follow-up: …` plus a dequeue hint in
|
|
6
|
+
* `pendingMessagesContainer`, rebuilt by `InteractiveMode.updatePendingMessagesDisplay`.
|
|
7
|
+
* There is no switch for it, so the method is wrapped: when the queue pill is on,
|
|
8
|
+
* the container is cleared instead of refilled, which leaves the layout row empty
|
|
9
|
+
* rather than collapsing the layout from outside.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately tied to the pill rather than a config key: the rows only disappear
|
|
12
|
+
* when something else is already carrying that information. Remove `queue` from
|
|
13
|
+
* `pill.segments` (or turn the status line off) and the rows come back on the next
|
|
14
|
+
* queue update, with no restart.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { InteractiveMode } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { installPrototypePatch } from "./prototype-patch-registry";
|
|
19
|
+
|
|
20
|
+
type PendingMessagesHost = {
|
|
21
|
+
pendingMessagesContainer?: { clear?: () => void };
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Wrap the rebuild so it can be skipped. `isEnabled` is read on every queue
|
|
26
|
+
* update, so a `/starline` change applies immediately.
|
|
27
|
+
*/
|
|
28
|
+
export function installNativeQueueRowHiding(isEnabled: () => boolean): () => void {
|
|
29
|
+
try {
|
|
30
|
+
return installPrototypePatch(
|
|
31
|
+
InteractiveMode.prototype,
|
|
32
|
+
"updatePendingMessagesDisplay",
|
|
33
|
+
"pending-messages-hide",
|
|
34
|
+
({ predecessor, receiver, args }) => {
|
|
35
|
+
if (!isEnabled()) return Function.prototype.apply.call(predecessor, receiver, args);
|
|
36
|
+
(receiver as PendingMessagesHost).pendingMessagesContainer?.clear?.();
|
|
37
|
+
return undefined;
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
} catch {
|
|
41
|
+
return () => {};
|
|
42
|
+
}
|
|
43
|
+
}
|