@xzzpig/pi-starline 0.5.1 → 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/FORK-CHANGELOG.md +19 -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/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 +607 -0
- package/extensions/starline/pill-config.ts +67 -9
- package/extensions/starline/pill.ts +102 -20
- 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 +6 -5
- 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
|
}
|
|
@@ -43,6 +43,42 @@ const CAP_GLYPHS = { left: "", right: "" };
|
|
|
43
43
|
/** Divider used where a solid arrow would be invisible. */
|
|
44
44
|
const THIN_SEPARATOR = SEPARATOR_GLYPHS["powerline-thin"];
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Where one pill ended up in a rendered bar, in visible columns.
|
|
48
|
+
*
|
|
49
|
+
* `startColumn` is inclusive, `endColumn` exclusive. Columns count what the
|
|
50
|
+
* terminal draws, so a CJK pill spans two cells per character and a pill that
|
|
51
|
+
* `width` cut in half still reports the cells it kept.
|
|
52
|
+
*
|
|
53
|
+
* A span covers everything the pill visually owns: its own padding, the
|
|
54
|
+
* separator that points out of it, and — on the two ends of the bar — the cap.
|
|
55
|
+
* That leaves no dead cells between two pills for a click to fall into.
|
|
56
|
+
*/
|
|
57
|
+
export type PillSpan = {
|
|
58
|
+
/** `PillInput.key` of the segment this span belongs to. */
|
|
59
|
+
key: string;
|
|
60
|
+
startColumn: number;
|
|
61
|
+
endColumn: number;
|
|
62
|
+
/**
|
|
63
|
+
* First column painted solid in the pill's colour. The hit area starts earlier
|
|
64
|
+
* on the first pill, whose round cap is the colour drawn on the terminal's own
|
|
65
|
+
* background; anything that has to line up with the pill's edge uses this.
|
|
66
|
+
*/
|
|
67
|
+
bodyStartColumn: number;
|
|
68
|
+
/** First column after the pill's own colour: where its arrow or cap starts. */
|
|
69
|
+
bodyEndColumn: number;
|
|
70
|
+
/** Background SGR the pill was painted with, for anything that has to match it. */
|
|
71
|
+
bg: string;
|
|
72
|
+
/** Foreground SGR, or "" when the text carried its own styling. */
|
|
73
|
+
fg: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type PillRenderOptions = {
|
|
77
|
+
ascii?: boolean;
|
|
78
|
+
/** Called once per pill that survived truncation, in left-to-right order. */
|
|
79
|
+
onSpan?: (span: PillSpan) => void;
|
|
80
|
+
};
|
|
81
|
+
|
|
46
82
|
export type PillInput = {
|
|
47
83
|
/** Segment name, for diagnostics. */
|
|
48
84
|
key: string;
|
|
@@ -123,6 +159,7 @@ export function collectPillInputs(
|
|
|
123
159
|
}
|
|
124
160
|
|
|
125
161
|
type PreparedPill = {
|
|
162
|
+
key: string;
|
|
126
163
|
text: string;
|
|
127
164
|
bg: string;
|
|
128
165
|
fg: string;
|
|
@@ -213,7 +250,7 @@ function prepare(
|
|
|
213
250
|
if (!text || visibleWidth(text) === 0) continue;
|
|
214
251
|
|
|
215
252
|
if (input.keepStyling) {
|
|
216
|
-
pills.push({ text, bg: neutral, fg: "", bold: false });
|
|
253
|
+
pills.push({ key: input.key, text, bg: neutral, fg: "", bold: false });
|
|
217
254
|
continue;
|
|
218
255
|
}
|
|
219
256
|
|
|
@@ -221,6 +258,7 @@ function prepare(
|
|
|
221
258
|
const bg = resolved.bg ?? resolveBackgroundSgr(theme, source, input.spec) ?? neutral;
|
|
222
259
|
// An explicit fg: in the spec wins; otherwise pick for legibility.
|
|
223
260
|
pills.push({
|
|
261
|
+
key: input.key,
|
|
224
262
|
text,
|
|
225
263
|
bg,
|
|
226
264
|
fg: resolved.bg && resolved.fg ? resolved.fg : contrastTextSgr(bg),
|
|
@@ -234,6 +272,11 @@ function prepare(
|
|
|
234
272
|
/**
|
|
235
273
|
* Render the pill bar. Returns "" when nothing has content, so the caller can
|
|
236
274
|
* fall back rather than emit a lone pair of caps.
|
|
275
|
+
*
|
|
276
|
+
* `options.onSpan` receives the hit area of every pill the terminal will
|
|
277
|
+
* actually show, measured in the bar's own visible columns. Recording the spans
|
|
278
|
+
* here rather than recomputing them later keeps them in step with the render:
|
|
279
|
+
* colour, caps, separators and truncation are all already accounted for.
|
|
237
280
|
*/
|
|
238
281
|
export function renderPillBar(
|
|
239
282
|
inputs: PillInput[],
|
|
@@ -241,7 +284,7 @@ export function renderPillBar(
|
|
|
241
284
|
source: ColorSource,
|
|
242
285
|
config: PillConfig,
|
|
243
286
|
width: number,
|
|
244
|
-
options:
|
|
287
|
+
options: PillRenderOptions = {},
|
|
245
288
|
): string {
|
|
246
289
|
const pills = prepare(inputs, theme, source, config);
|
|
247
290
|
if (pills.length === 0) return "";
|
|
@@ -255,41 +298,80 @@ export function renderPillBar(
|
|
|
255
298
|
const rightCap = wantsCaps ? CAP_GLYPHS.right : "";
|
|
256
299
|
|
|
257
300
|
let out = "";
|
|
301
|
+
// Columns the terminal will draw so far. Getting this from `visibleWidth`
|
|
302
|
+
// rather than string length is what makes CJK segment text hit the right cells.
|
|
303
|
+
let column = 0;
|
|
304
|
+
const emit = (piece: string) => {
|
|
305
|
+
out += piece;
|
|
306
|
+
column += visibleWidth(piece);
|
|
307
|
+
};
|
|
258
308
|
|
|
309
|
+
const capStart = column;
|
|
259
310
|
const firstBg = pills[0]?.bg;
|
|
260
311
|
if (leftCap && firstBg) {
|
|
261
312
|
const capFg = toForegroundSgr(firstBg);
|
|
262
|
-
if (capFg)
|
|
313
|
+
if (capFg) emit(`${DEFAULT_BG}${capFg}${leftCap}${RESET}`);
|
|
263
314
|
}
|
|
264
315
|
|
|
316
|
+
const spans: PillSpan[] = [];
|
|
317
|
+
|
|
265
318
|
pills.forEach((pill, index) => {
|
|
266
|
-
|
|
319
|
+
// The left cap opens the bar in the first pill's colour, so it belongs to it.
|
|
320
|
+
const startColumn = index === 0 ? capStart : column;
|
|
321
|
+
const bodyStartColumn = column;
|
|
322
|
+
emit(`${pill.bg}${pill.fg}${pill.bold ? BOLD : ""} ${pill.text} ${RESET}`);
|
|
323
|
+
const bodyEndColumn = column;
|
|
267
324
|
|
|
268
325
|
const next = pills[index + 1];
|
|
269
326
|
if (next) {
|
|
270
|
-
if (!separator) return;
|
|
271
|
-
|
|
272
327
|
// A solid arrow works by being the left colour on the right colour, so
|
|
273
328
|
// it vanishes when the two pills share a background — several segments
|
|
274
329
|
// default to the same theme colour, which merged them into one blob.
|
|
275
330
|
// Fall back to the thin divider in the text colour, which stays visible.
|
|
276
|
-
if (pill.bg === next.bg) {
|
|
277
|
-
|
|
278
|
-
|
|
331
|
+
if (separator && pill.bg === next.bg) {
|
|
332
|
+
emit(`${pill.bg}${pill.fg || contrastTextSgr(pill.bg)}${THIN_SEPARATOR}${RESET}`);
|
|
333
|
+
} else if (separator) {
|
|
334
|
+
const arrowFg = toForegroundSgr(pill.bg);
|
|
335
|
+
// Without a usable foreground the arrow would be invisible; letting the
|
|
336
|
+
// backgrounds abut is better than drawing a glyph in the wrong colour.
|
|
337
|
+
if (arrowFg) emit(`${next.bg}${arrowFg}${separator}${RESET}`);
|
|
279
338
|
}
|
|
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) {
|
|
339
|
+
} else if (rightCap) {
|
|
289
340
|
const capFg = toForegroundSgr(pill.bg);
|
|
290
|
-
if (capFg)
|
|
341
|
+
if (capFg) emit(`${DEFAULT_BG}${capFg}${rightCap}${RESET}`);
|
|
291
342
|
}
|
|
343
|
+
|
|
344
|
+
// The arrow (or cap) is emitted before the span closes, so it belongs to
|
|
345
|
+
// the pill that points out of itself.
|
|
346
|
+
spans.push({
|
|
347
|
+
key: pill.key,
|
|
348
|
+
startColumn,
|
|
349
|
+
endColumn: column,
|
|
350
|
+
bodyStartColumn,
|
|
351
|
+
bodyEndColumn,
|
|
352
|
+
bg: pill.bg,
|
|
353
|
+
fg: pill.fg,
|
|
354
|
+
});
|
|
292
355
|
});
|
|
293
356
|
|
|
294
|
-
|
|
357
|
+
const rendered = width > 0 ? truncateToWidth(out, width, "") : out;
|
|
358
|
+
if (options.onSpan) {
|
|
359
|
+
// Truncation keeps a prefix, so surviving cells keep their columns; only
|
|
360
|
+
// the far end needs clamping, and a pill clipped away entirely vanishes.
|
|
361
|
+
const visible = visibleWidth(rendered);
|
|
362
|
+
for (const span of spans) {
|
|
363
|
+
const startColumn = Math.min(span.startColumn, visible);
|
|
364
|
+
const endColumn = Math.min(span.endColumn, visible);
|
|
365
|
+
if (endColumn > startColumn) {
|
|
366
|
+
const bodyStartColumn = Math.min(span.bodyStartColumn, endColumn - 1);
|
|
367
|
+
const bodyEndColumn = Math.max(
|
|
368
|
+
bodyStartColumn + 1,
|
|
369
|
+
Math.min(span.bodyEndColumn, endColumn),
|
|
370
|
+
);
|
|
371
|
+
options.onSpan({ ...span, startColumn, endColumn, bodyStartColumn, bodyEndColumn });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return rendered;
|
|
295
377
|
}
|
|
@@ -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
|
+
}
|