@xzzpig/pi-starline 0.5.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/FORK-CHANGELOG.md +28 -0
- package/README.md +91 -181
- package/dist/public.d.ts +5 -0
- 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/explicit-expand-target.ts +89 -0
- 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/tool-box.ts +4 -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 +18 -12
- 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
|
@@ -1,178 +1,68 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type TuiMouseEvent,
|
|
4
|
+
type TuiMouseEventResult,
|
|
5
|
+
truncateToWidth,
|
|
6
|
+
visibleWidth,
|
|
7
|
+
} from "@earendil-works/pi-tui";
|
|
3
8
|
import {
|
|
4
9
|
type ColorSpec,
|
|
5
|
-
FOOTER_FORMAT_ALIASES,
|
|
6
10
|
getExtensionStatusColor,
|
|
7
11
|
getExtensionStatusIcon,
|
|
8
12
|
type PolishedTuiConfig,
|
|
9
|
-
type SeparatorStyle,
|
|
10
13
|
} from "./config";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
type ExtensionStatusSegment,
|
|
14
|
-
sanitizeExtensionStatusText,
|
|
15
|
-
} from "./extension-status";
|
|
16
|
-
import { parseFooterFormat, renderFormatSplit, stripOrphanSeparators } from "./footer-format";
|
|
14
|
+
import { setActiveContextSnapshot } from "./context-live";
|
|
15
|
+
import { collectExtensionStatusSegments, sanitizeExtensionStatusText } from "./extension-status";
|
|
17
16
|
import {
|
|
18
17
|
buildContextDisplayLabel,
|
|
19
18
|
buildSessionDurationLabel,
|
|
19
|
+
buildTpsLabel,
|
|
20
20
|
contextColorTier,
|
|
21
21
|
formatCwdLabel,
|
|
22
|
-
formatGitBranchText,
|
|
23
|
-
formatGitCommitSegment,
|
|
24
|
-
formatGitMetricsSegment,
|
|
25
22
|
formatOsLabel,
|
|
26
|
-
formatPackageVersionSegment,
|
|
27
|
-
formatRuntimeSegment,
|
|
28
23
|
formatTimeLabel,
|
|
29
24
|
formatUsernameHostLabel,
|
|
30
25
|
} from "./format";
|
|
31
|
-
import
|
|
32
|
-
import {
|
|
26
|
+
import { renderGitSegments } from "./git-segments";
|
|
27
|
+
import { resolveRuntimeSymbol } from "./icons";
|
|
33
28
|
import type { LiveContextOverride } from "./live-context";
|
|
34
|
-
import {
|
|
29
|
+
import { requestPointerShape } from "./mouse/pointer-shape";
|
|
30
|
+
import { collectPillInputs, type PillSpan, renderPillBar } from "./pill";
|
|
31
|
+
import { pillActionFor, queuedSegmentText, runPillAction } from "./pill-actions";
|
|
32
|
+
import { clearPillAnchors, setPillAnchors } from "./pill-anchors";
|
|
33
|
+
import { closePillBubble, isPillBubbleOpenFor, type PillAnchor } from "./pill-bubble";
|
|
35
34
|
import type { FooterState } from "./state";
|
|
36
35
|
import { renderStyleForSource } from "./style";
|
|
36
|
+
import { subagentsSegmentText } from "./subagent-fleet";
|
|
37
|
+
import type { TurnTiming } from "./turn-clock";
|
|
38
|
+
import { modelPillLabel } from "./working-indicator";
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
function joinStatusTexts(statusTexts: string[], separator: string): string {
|
|
46
|
-
return statusTexts.filter(Boolean).join(separator);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function fitStatusTexts(statusTexts: string[], maxWidth: number, separator: string): string {
|
|
50
|
-
if (maxWidth <= 0) return "";
|
|
51
|
-
|
|
52
|
-
const fitted: string[] = [];
|
|
53
|
-
for (const text of statusTexts) {
|
|
54
|
-
const candidate = joinStatusTexts([...fitted, text], separator);
|
|
55
|
-
if (visibleWidth(candidate) <= maxWidth) {
|
|
56
|
-
fitted.push(text);
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (fitted.length === 0) {
|
|
61
|
-
return maxWidth > 1 ? truncateToWidth(text, maxWidth, "…") : "";
|
|
62
|
-
}
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return joinStatusTexts(fitted, separator);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function appendStatusArea(base: string, statusText: string, separator: string): string {
|
|
70
|
-
if (!base) return statusText;
|
|
71
|
-
if (!statusText) return base;
|
|
72
|
-
return `${base}${separator}${statusText}`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function prependStatusArea(base: string, statusText: string, separator: string): string {
|
|
76
|
-
if (!base) return statusText;
|
|
77
|
-
if (!statusText) return base;
|
|
78
|
-
return `${statusText}${separator}${base}`;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function composeBuiltInFooterContent(left: string, right: string, innerWidth: number): string {
|
|
82
|
-
const leftWidth = visibleWidth(left);
|
|
83
|
-
const rightWidth = visibleWidth(right);
|
|
84
|
-
return leftWidth >= innerWidth
|
|
85
|
-
? truncateToWidth(left, innerWidth, "")
|
|
86
|
-
: leftWidth + 1 + rightWidth <= innerWidth
|
|
87
|
-
? `${left}${" ".repeat(innerWidth - leftWidth - rightWidth)}${right}`
|
|
88
|
-
: truncateToWidth(left, innerWidth, "");
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function composeFooterContent(
|
|
92
|
-
builtInLeft: string,
|
|
93
|
-
builtInRight: string,
|
|
94
|
-
extensionLeft: string[],
|
|
95
|
-
extensionMiddle: string[],
|
|
96
|
-
extensionRight: string[],
|
|
97
|
-
separator: string,
|
|
98
|
-
innerWidth: number,
|
|
99
|
-
): string {
|
|
100
|
-
const builtInLeftWidth = visibleWidth(builtInLeft);
|
|
101
|
-
const builtInRightWidth = visibleWidth(builtInRight);
|
|
102
|
-
const minimumGap = builtInLeft && builtInRight ? 1 : 0;
|
|
103
|
-
|
|
104
|
-
if (builtInLeftWidth + minimumGap + builtInRightWidth > innerWidth) {
|
|
105
|
-
return composeBuiltInFooterContent(builtInLeft, builtInRight, innerWidth);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const available = Math.max(0, innerWidth - builtInLeftWidth - builtInRightWidth - minimumGap);
|
|
109
|
-
let remaining = available;
|
|
110
|
-
const leftConnectorWidth = builtInLeft && extensionLeft.length > 0 ? visibleWidth(separator) : 0;
|
|
111
|
-
const rightConnectorWidth =
|
|
112
|
-
builtInRight && extensionRight.length > 0 ? visibleWidth(separator) : 0;
|
|
113
|
-
let leftStatus = "";
|
|
114
|
-
let rightStatus = "";
|
|
115
|
-
|
|
116
|
-
if (extensionLeft.length > 0 && extensionRight.length > 0) {
|
|
117
|
-
const leftBudget = Math.max(0, Math.floor(available / 2) - leftConnectorWidth);
|
|
118
|
-
leftStatus = fitStatusTexts(extensionLeft, leftBudget, separator);
|
|
119
|
-
remaining -= leftStatus ? leftConnectorWidth + visibleWidth(leftStatus) : 0;
|
|
120
|
-
|
|
121
|
-
const rightBudget = Math.max(0, remaining - rightConnectorWidth);
|
|
122
|
-
rightStatus = fitStatusTexts(extensionRight, rightBudget, separator);
|
|
123
|
-
remaining -= rightStatus ? rightConnectorWidth + visibleWidth(rightStatus) : 0;
|
|
124
|
-
|
|
125
|
-
const expandedLeftBudget = Math.max(0, remaining + visibleWidth(leftStatus));
|
|
126
|
-
const expandedLeftStatus = fitStatusTexts(extensionLeft, expandedLeftBudget, separator);
|
|
127
|
-
if (visibleWidth(expandedLeftStatus) > visibleWidth(leftStatus)) {
|
|
128
|
-
remaining += leftStatus ? leftConnectorWidth + visibleWidth(leftStatus) : 0;
|
|
129
|
-
leftStatus = expandedLeftStatus;
|
|
130
|
-
remaining -= leftStatus ? leftConnectorWidth + visibleWidth(leftStatus) : 0;
|
|
131
|
-
}
|
|
132
|
-
} else if (extensionLeft.length > 0) {
|
|
133
|
-
leftStatus = fitStatusTexts(
|
|
134
|
-
extensionLeft,
|
|
135
|
-
Math.max(0, available - leftConnectorWidth),
|
|
136
|
-
separator,
|
|
137
|
-
);
|
|
138
|
-
remaining -= leftStatus ? leftConnectorWidth + visibleWidth(leftStatus) : 0;
|
|
139
|
-
} else if (extensionRight.length > 0) {
|
|
140
|
-
rightStatus = fitStatusTexts(
|
|
141
|
-
extensionRight,
|
|
142
|
-
Math.max(0, available - rightConnectorWidth),
|
|
143
|
-
separator,
|
|
144
|
-
);
|
|
145
|
-
remaining -= rightStatus ? rightConnectorWidth + visibleWidth(rightStatus) : 0;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const left = appendStatusArea(builtInLeft, leftStatus, separator);
|
|
149
|
-
const right = prependStatusArea(builtInRight, rightStatus, separator);
|
|
150
|
-
const gapWidth = Math.max(0, innerWidth - visibleWidth(left) - visibleWidth(right));
|
|
151
|
-
const middle = fitStatusTexts(extensionMiddle, gapWidth, separator);
|
|
152
|
-
const middleWidth = visibleWidth(middle);
|
|
40
|
+
/**
|
|
41
|
+
* Pi's component mouse event and result. Pi dispatches to `handleMouse` from
|
|
42
|
+
* pi-tui 0.85.0; on 0.84.x the method is simply never called.
|
|
43
|
+
*/
|
|
44
|
+
export type FooterMouseEvent = TuiMouseEvent;
|
|
153
45
|
|
|
154
|
-
|
|
155
|
-
return `${left}${" ".repeat(gapWidth)}${right}`;
|
|
156
|
-
}
|
|
46
|
+
export type FooterMouseResult = TuiMouseEventResult;
|
|
157
47
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
48
|
+
/**
|
|
49
|
+
* The footer Pi mounts. `handleMouse` is optional and undeclared by the old
|
|
50
|
+
* component type, which is why the object is built under this local shape.
|
|
51
|
+
*/
|
|
52
|
+
type FooterComponent = {
|
|
53
|
+
render(width: number): string[];
|
|
54
|
+
invalidate(): void;
|
|
55
|
+
dispose(): void;
|
|
56
|
+
handleMouse?(event: FooterMouseEvent): FooterMouseResult | undefined;
|
|
57
|
+
};
|
|
162
58
|
|
|
163
|
-
/**
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
case "bitbucket":
|
|
171
|
-
return icons.gitHostBitbucket;
|
|
172
|
-
default:
|
|
173
|
-
return icons.gitHostGeneric;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
59
|
+
/**
|
|
60
|
+
* How many renders' worth of pill hit maps to keep. A render is also how the
|
|
61
|
+
* layout measures, so one frame can render the footer at more than one width;
|
|
62
|
+
* keying by width keeps a measurement from replacing the map the click on screen
|
|
63
|
+
* will be matched against.
|
|
64
|
+
*/
|
|
65
|
+
const PILL_HIT_MAP_LIMIT = 4;
|
|
176
66
|
|
|
177
67
|
/** Prefix a label with an icon, collapsing to the label when there is no icon. */
|
|
178
68
|
function withIcon(icon: string, label: string): string {
|
|
@@ -210,6 +100,10 @@ export function installFooter(
|
|
|
210
100
|
scheduleProjectRefresh: (ctx: ExtensionContext) => void;
|
|
211
101
|
setExtensionStatusesGetter?: (fn: (() => ReadonlyMap<string, string>) | undefined) => void;
|
|
212
102
|
getLiveContext?: () => LiveContextOverride | undefined;
|
|
103
|
+
/** Decode throughput of the latest assistant message, or undefined before one landed. */
|
|
104
|
+
getLiveTps?: () => number | undefined;
|
|
105
|
+
/** The current run's elapsed time, and the last run's total. */
|
|
106
|
+
getTurnTiming?: () => TurnTiming;
|
|
213
107
|
getThinkingLevel?: () => string | undefined;
|
|
214
108
|
},
|
|
215
109
|
): void {
|
|
@@ -221,25 +115,179 @@ export function installFooter(
|
|
|
221
115
|
tui.requestRender();
|
|
222
116
|
});
|
|
223
117
|
|
|
224
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Pill hit areas from the render that is on screen, per width.
|
|
120
|
+
*
|
|
121
|
+
* Replaced on every render, read only by `handleMouse`: no timers, no
|
|
122
|
+
* invalidation, nothing to keep in sync with what the footer drew.
|
|
123
|
+
*/
|
|
124
|
+
const pillSpansByWidth = new Map<number, PillSpan[]>();
|
|
125
|
+
|
|
126
|
+
const rememberPillSpans = (
|
|
127
|
+
width: number,
|
|
128
|
+
spans: readonly PillSpan[],
|
|
129
|
+
columnOffset: number,
|
|
130
|
+
lineWidth: number,
|
|
131
|
+
) => {
|
|
132
|
+
const visibleSpans: PillSpan[] = [];
|
|
133
|
+
const anchors = new Map<string, PillAnchor>();
|
|
134
|
+
for (const span of spans) {
|
|
135
|
+
// The bar starts `columnOffset` columns in, and the line may have been cut
|
|
136
|
+
// again after the bar was; clamp to what the terminal actually shows.
|
|
137
|
+
const startColumn = Math.min(lineWidth, span.startColumn + columnOffset);
|
|
138
|
+
const endColumn = Math.min(lineWidth, span.endColumn + columnOffset);
|
|
139
|
+
if (endColumn <= startColumn) continue;
|
|
140
|
+
const bodyStartColumn = Math.min(endColumn - 1, span.bodyStartColumn + columnOffset);
|
|
141
|
+
const bodyEndColumn = Math.max(
|
|
142
|
+
bodyStartColumn + 1,
|
|
143
|
+
Math.min(endColumn, span.bodyEndColumn + columnOffset),
|
|
144
|
+
);
|
|
145
|
+
visibleSpans.push({ ...span, startColumn, endColumn, bodyStartColumn, bodyEndColumn });
|
|
146
|
+
// Same columns, plus the colours, for anything that wants to sit next to
|
|
147
|
+
// this pill later — a bubble today, whatever the next action needs.
|
|
148
|
+
anchors.set(span.key, {
|
|
149
|
+
key: span.key,
|
|
150
|
+
startColumn: bodyStartColumn,
|
|
151
|
+
endColumn: bodyEndColumn,
|
|
152
|
+
centerColumn: Math.floor((startColumn + endColumn - 1) / 2),
|
|
153
|
+
terminalWidth: width,
|
|
154
|
+
background: span.bg,
|
|
155
|
+
foreground: span.fg,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
pillSpansByWidth.set(width, visibleSpans);
|
|
159
|
+
setPillAnchors(anchors);
|
|
160
|
+
if (pillSpansByWidth.size > PILL_HIT_MAP_LIMIT) {
|
|
161
|
+
const oldest = pillSpansByWidth.keys().next();
|
|
162
|
+
if (!oldest.done) pillSpansByWidth.delete(oldest.value);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The pill this mouse-down already dealt with: it opened that pill's bubble, or
|
|
168
|
+
* closed it because it was the one open.
|
|
169
|
+
*
|
|
170
|
+
* `click` is not a terminal event: Pi-tui synthesizes one on release for every
|
|
171
|
+
* press that did not move, and dispatches it to the same component. Running the
|
|
172
|
+
* action on the press is what takes the button-hold out of the bubble's latency,
|
|
173
|
+
* so that synthesized click has to be swallowed rather than toggling it back.
|
|
174
|
+
*/
|
|
175
|
+
let pressActed: { key: string; x: number } | null = null;
|
|
176
|
+
|
|
177
|
+
const component: FooterComponent = {
|
|
225
178
|
dispose: () => {
|
|
226
179
|
unsubscribeBranch();
|
|
227
180
|
hooks.setRequestRender(undefined);
|
|
228
181
|
hooks.setExtensionStatusesGetter?.(undefined);
|
|
229
182
|
},
|
|
230
183
|
invalidate() {},
|
|
184
|
+
/**
|
|
185
|
+
* A click on a pill runs that segment's action.
|
|
186
|
+
*
|
|
187
|
+
* Pi's footer slot is a `Container`, which forwards mouse events to the
|
|
188
|
+
* component inside it with `y` rebased to this component and `x` unchanged, so
|
|
189
|
+
* these are exactly the columns `render` measured. Nothing is declared or
|
|
190
|
+
* patched for this.
|
|
191
|
+
*
|
|
192
|
+
* Returning undefined for anything that is not a target (another row, empty
|
|
193
|
+
* space, an inert segment) is what lets the click fall through instead of
|
|
194
|
+
* being swallowed by the footer.
|
|
195
|
+
*/
|
|
196
|
+
handleMouse(event: FooterMouseEvent): FooterMouseResult | undefined {
|
|
197
|
+
if (!Number.isFinite(event.x) || !Number.isFinite(event.y)) return undefined;
|
|
198
|
+
// The pill bar is the one and only row this component draws.
|
|
199
|
+
if (event.y !== 0) return undefined;
|
|
200
|
+
const width = typeof event.width === "number" ? Math.floor(event.width) : undefined;
|
|
201
|
+
if (width === undefined) return undefined;
|
|
202
|
+
const spans = pillSpansByWidth.get(width);
|
|
203
|
+
if (!spans) return undefined;
|
|
204
|
+
|
|
205
|
+
const hit = spans.find((span) => event.x >= span.startColumn && event.x < span.endColumn);
|
|
206
|
+
const action = hit === undefined ? undefined : pillActionFor(hit.key);
|
|
207
|
+
if (!hit || !action) {
|
|
208
|
+
// Nothing here to click: padding at either end, a gap, or a pill that is
|
|
209
|
+
// only a read-out. For an open bubble this row is then just "outside" like
|
|
210
|
+
// the rest of the screen, and the outside-click dismissal cannot decide
|
|
211
|
+
// that alone — it stays off this row so a press on a pill keeps its
|
|
212
|
+
// toggle. Press, not click: a press that claims nothing is never followed
|
|
213
|
+
// by a synthesized click, and this must not turn into a swallow.
|
|
214
|
+
if (event.type === "press") closePillBubble();
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Moving over a pill that does something turns the pointer into a hand.
|
|
219
|
+
// Pi has no leave event; mouse/pointer-shape.ts puts it back once a move
|
|
220
|
+
// lands anywhere that does not ask for it.
|
|
221
|
+
if (event.type === "move") {
|
|
222
|
+
requestPointerShape("pointer");
|
|
223
|
+
return { handled: true };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const anchor = () => ({
|
|
227
|
+
key: hit.key,
|
|
228
|
+
startColumn: hit.bodyStartColumn,
|
|
229
|
+
endColumn: hit.bodyEndColumn,
|
|
230
|
+
centerColumn: Math.floor((hit.startColumn + hit.endColumn - 1) / 2),
|
|
231
|
+
terminalWidth: width,
|
|
232
|
+
background: hit.bg,
|
|
233
|
+
foreground: hit.fg,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The action runs on the press, not on Pi's synthesized click.
|
|
238
|
+
*
|
|
239
|
+
* Waiting for the click means the bubble opens after the button is released,
|
|
240
|
+
* which is where the delay came from: a click is press + release + synth, and
|
|
241
|
+
* that hop is pure added latency for a target one row tall. The press is also
|
|
242
|
+
* what a drag starts from, so a drag cancels what the press opened.
|
|
243
|
+
*/
|
|
244
|
+
if (event.type === "press") {
|
|
245
|
+
// Pressing the pill again puts its bubble away, like a menu-bar item.
|
|
246
|
+
if (isPillBubbleOpenFor(hit.key)) {
|
|
247
|
+
closePillBubble();
|
|
248
|
+
// The release still synthesizes a click for this press. Claim it, so that
|
|
249
|
+
// click is swallowed instead of running the action and reopening what
|
|
250
|
+
// this press just closed.
|
|
251
|
+
pressActed = { key: hit.key, x: event.x };
|
|
252
|
+
return { handled: true };
|
|
253
|
+
}
|
|
254
|
+
runPillAction(action, ctx, anchor());
|
|
255
|
+
pressActed = { key: hit.key, x: event.x };
|
|
256
|
+
return { handled: true };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// A press that moves was a selection or a stray gesture: undo it rather than
|
|
260
|
+
// leaving a bubble the user never asked for. Two columns is past any jitter.
|
|
261
|
+
if (event.type === "drag") {
|
|
262
|
+
if (pressActed && Math.abs(event.x - pressActed.x) > 2) {
|
|
263
|
+
closePillBubble();
|
|
264
|
+
pressActed = null;
|
|
265
|
+
}
|
|
266
|
+
return { handled: true };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (event.type === "click") {
|
|
270
|
+
const acted = pressActed;
|
|
271
|
+
pressActed = null;
|
|
272
|
+
// The press already ran this; running it again would toggle the bubble shut.
|
|
273
|
+
if (acted?.key === hit.key) return { handled: true };
|
|
274
|
+
// No press preceded this click, which terminals do not send today, but a
|
|
275
|
+
// synthetic click from elsewhere would still mean what it always meant.
|
|
276
|
+
if (isPillBubbleOpenFor(hit.key)) {
|
|
277
|
+
closePillBubble();
|
|
278
|
+
return { handled: true };
|
|
279
|
+
}
|
|
280
|
+
runPillAction(action, ctx, anchor());
|
|
281
|
+
return { handled: true };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return { handled: true };
|
|
285
|
+
},
|
|
231
286
|
render(width: number): string[] {
|
|
232
287
|
if (width <= 0) return [""];
|
|
233
288
|
const config = getConfig();
|
|
234
289
|
const colorSource = config.colorSources.starship;
|
|
235
290
|
const iconMode = config.icons.mode;
|
|
236
|
-
const separator = renderStyleForSource(
|
|
237
|
-
theme,
|
|
238
|
-
colorSource,
|
|
239
|
-
config.colors.separator,
|
|
240
|
-
separatorText[config.separator],
|
|
241
|
-
);
|
|
242
|
-
const innerWidth = Math.max(1, width - 2);
|
|
243
291
|
const cwdLabel = renderStyleForSource(
|
|
244
292
|
theme,
|
|
245
293
|
colorSource,
|
|
@@ -254,7 +302,13 @@ export function installFooter(
|
|
|
254
302
|
theme,
|
|
255
303
|
colorSource,
|
|
256
304
|
config.colors.model,
|
|
257
|
-
|
|
305
|
+
// While the agent works the spinner takes the icon's slot at the same
|
|
306
|
+
// display width, so the pill never resizes, and the run's elapsed time
|
|
307
|
+
// joins the name behind it.
|
|
308
|
+
modelPillLabel(config.icons.model, state.modelLabel, {
|
|
309
|
+
ascii: iconMode === "ascii",
|
|
310
|
+
timing: hooks.getTurnTiming?.(),
|
|
311
|
+
}),
|
|
258
312
|
)
|
|
259
313
|
: "";
|
|
260
314
|
// "off" reads as no thinking at all, matching the editor metadata line.
|
|
@@ -268,20 +322,10 @@ export function installFooter(
|
|
|
268
322
|
withIcon(config.icons.thinking, thinkingText),
|
|
269
323
|
)
|
|
270
324
|
: "";
|
|
271
|
-
const
|
|
272
|
-
? /(?:\$session_name\b|\$\{session_name\})/.test(config.footerFormat)
|
|
273
|
-
: config.footerSegments.sessionName;
|
|
274
|
-
const sessionName = needsSessionName
|
|
275
|
-
? sanitizeExtensionStatusText(ctx.sessionManager.getSessionName() ?? "")
|
|
276
|
-
: "";
|
|
325
|
+
const sessionName = sanitizeExtensionStatusText(ctx.sessionManager.getSessionName() ?? "");
|
|
277
326
|
const sessionNameLabel = sessionName
|
|
278
327
|
? renderStyleForSource(theme, colorSource, config.colors.sessionName, sessionName)
|
|
279
328
|
: "";
|
|
280
|
-
const builtInSessionNameLabel = sessionNameLabel ? `in ${sessionNameLabel}` : "";
|
|
281
|
-
const branch = state.branch;
|
|
282
|
-
const branchText = branch
|
|
283
|
-
? formatGitBranchText(branch, config.gitBranch.maxLength)
|
|
284
|
-
: undefined;
|
|
285
329
|
const contextUsage = ctx.getContextUsage();
|
|
286
330
|
const liveContext = hooks.getLiveContext?.();
|
|
287
331
|
const contextWindow = ctx.model?.contextWindow ?? contextUsage?.contextWindow;
|
|
@@ -305,76 +349,49 @@ export function installFooter(
|
|
|
305
349
|
? config.colors.contextWarning
|
|
306
350
|
: config.colors.contextNormal;
|
|
307
351
|
const contextDisplay = withIcon(config.icons.context, contextLabel);
|
|
308
|
-
const tokensDisplay = withIcon(config.icons.tokens, state.tokenLabel);
|
|
309
352
|
const costDisplay = withIcon(config.icons.cost, state.costLabel);
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const
|
|
323
|
-
const gitIcon = gitGlyph ? gitColor(gitGlyph) : "";
|
|
324
|
-
const gitCounts = config.footerSegments.gitCounts;
|
|
325
|
-
const stashLabel =
|
|
326
|
-
state.stashed > 0
|
|
327
|
-
? gitCounts
|
|
328
|
-
? `${config.icons.stashed}${state.stashed}`
|
|
329
|
-
: config.icons.stashed
|
|
330
|
-
: "";
|
|
331
|
-
const allStatus = [
|
|
332
|
-
state.conflicted > 0 ? config.icons.conflicted : "",
|
|
333
|
-
stashLabel,
|
|
334
|
-
state.deleted > 0 ? config.icons.deleted : "",
|
|
335
|
-
state.renamed > 0 ? config.icons.renamed : "",
|
|
336
|
-
state.modified > 0 ? config.icons.modified : "",
|
|
337
|
-
state.typechanged > 0 ? config.icons.typechanged : "",
|
|
338
|
-
state.staged > 0 ? config.icons.staged : "",
|
|
339
|
-
state.untracked > 0 ? config.icons.untracked : "",
|
|
340
|
-
].join("");
|
|
341
|
-
const aheadBehind = (() => {
|
|
342
|
-
if (state.ahead > 0 && state.behind > 0) {
|
|
343
|
-
return gitCounts
|
|
344
|
-
? `${config.icons.ahead}${state.ahead}${config.icons.behind}${state.behind}`
|
|
345
|
-
: config.icons.diverged;
|
|
346
|
-
}
|
|
347
|
-
if (state.ahead > 0)
|
|
348
|
-
return gitCounts ? `${config.icons.ahead}${state.ahead}` : config.icons.ahead;
|
|
349
|
-
if (state.behind > 0)
|
|
350
|
-
return gitCounts ? `${config.icons.behind}${state.behind}` : config.icons.behind;
|
|
351
|
-
return "";
|
|
352
|
-
})();
|
|
353
|
-
const statusBlock =
|
|
354
|
-
allStatus || aheadBehind ? gitStatusColor(`[${allStatus}${aheadBehind}]`) : "";
|
|
355
|
-
const gitStateLabel = state.gitStateLabel ?? "";
|
|
356
|
-
const gitStateBlock = gitStateLabel ? gitStatusColor(gitStateLabel) : "";
|
|
353
|
+
const tpsLabel = buildTpsLabel(hooks.getLiveTps?.());
|
|
354
|
+
const tpsDisplay = tpsLabel ? withIcon(config.icons.tps, tpsLabel) : "";
|
|
355
|
+
// The one pill shows a percentage; the rest of these numbers are only
|
|
356
|
+
// on screen while its bubble is open.
|
|
357
|
+
const contextTabTokens = useLiveContext ? liveContext.tokens : undefined;
|
|
358
|
+
setActiveContextSnapshot({
|
|
359
|
+
percent: contextPercent ?? undefined,
|
|
360
|
+
contextWindow,
|
|
361
|
+
tokens: contextTabTokens,
|
|
362
|
+
tokenLabel: state.tokenLabel,
|
|
363
|
+
cacheLabel: state.cacheHitLabel,
|
|
364
|
+
});
|
|
365
|
+
const git = renderGitSegments(state, config, theme, colorSource);
|
|
357
366
|
const renderVariable = (name: string): string => {
|
|
358
|
-
|
|
359
|
-
switch (canonical) {
|
|
367
|
+
switch (name) {
|
|
360
368
|
case "model":
|
|
361
369
|
return modelLabel;
|
|
362
370
|
case "thinking":
|
|
363
371
|
return thinkingLabel;
|
|
372
|
+
case "queue":
|
|
373
|
+
return renderStyleForSource(
|
|
374
|
+
theme,
|
|
375
|
+
colorSource,
|
|
376
|
+
config.colors.queue,
|
|
377
|
+
queuedSegmentText(ctx, config.icons.queue),
|
|
378
|
+
);
|
|
379
|
+
case "subagents":
|
|
380
|
+
return renderStyleForSource(
|
|
381
|
+
theme,
|
|
382
|
+
colorSource,
|
|
383
|
+
config.colors.subagents,
|
|
384
|
+
withIcon(
|
|
385
|
+
config.icons.subagents,
|
|
386
|
+
subagentsSegmentText(config.segmentOptions.subagents.format),
|
|
387
|
+
),
|
|
388
|
+
);
|
|
364
389
|
case "cwd":
|
|
365
390
|
return cwdLabel;
|
|
366
391
|
case "session_name":
|
|
367
392
|
return sessionNameLabel;
|
|
368
|
-
case "
|
|
369
|
-
return
|
|
370
|
-
? gitIcon
|
|
371
|
-
? `${gitIcon} ${gitColor(branchText)}`
|
|
372
|
-
: gitColor(branchText)
|
|
373
|
-
: "";
|
|
374
|
-
case "git_status":
|
|
375
|
-
return statusBlock;
|
|
376
|
-
case "git_state":
|
|
377
|
-
return gitStateBlock;
|
|
393
|
+
case "git":
|
|
394
|
+
return git.label;
|
|
378
395
|
case "runtime": {
|
|
379
396
|
if (!state.runtime) return "";
|
|
380
397
|
const symbol = resolveRuntimeSymbol(
|
|
@@ -417,23 +434,10 @@ export function installFooter(
|
|
|
417
434
|
);
|
|
418
435
|
case "context":
|
|
419
436
|
return renderStyleForSource(theme, colorSource, contextColor, contextDisplay);
|
|
420
|
-
case "tokens":
|
|
421
|
-
return renderStyleForSource(theme, colorSource, config.colors.tokens, tokensDisplay);
|
|
422
|
-
case "cache_hit":
|
|
423
|
-
return cacheHitDisplay
|
|
424
|
-
? renderStyleForSource(theme, colorSource, config.colors.cacheHit, cacheHitDisplay)
|
|
425
|
-
: "";
|
|
426
437
|
case "cost":
|
|
427
438
|
return renderStyleForSource(theme, colorSource, config.colors.cost, costDisplay);
|
|
428
|
-
case "
|
|
429
|
-
return
|
|
430
|
-
theme,
|
|
431
|
-
state.packageVersion,
|
|
432
|
-
colorSource,
|
|
433
|
-
iconMode,
|
|
434
|
-
config.icons.package,
|
|
435
|
-
config.colors.packageVersion,
|
|
436
|
-
);
|
|
439
|
+
case "tps":
|
|
440
|
+
return renderStyleForSource(theme, colorSource, config.colors.tps, tpsDisplay);
|
|
437
441
|
case "package_version":
|
|
438
442
|
return state.packageVersion?.version
|
|
439
443
|
? renderStyleForSource(
|
|
@@ -443,302 +447,101 @@ export function installFooter(
|
|
|
443
447
|
state.packageVersion.version,
|
|
444
448
|
)
|
|
445
449
|
: "";
|
|
446
|
-
case "sep":
|
|
447
|
-
return renderStyleForSource(theme, colorSource, config.colors.separator, " | ");
|
|
448
450
|
case "git_commit":
|
|
449
|
-
return
|
|
450
|
-
theme,
|
|
451
|
-
state.commit,
|
|
452
|
-
config.gitCommit,
|
|
453
|
-
colorSource,
|
|
454
|
-
config.colors.gitCommit,
|
|
455
|
-
);
|
|
451
|
+
return git.commit;
|
|
456
452
|
case "git_tag":
|
|
457
|
-
return
|
|
458
|
-
? renderStyleForSource(
|
|
459
|
-
theme,
|
|
460
|
-
colorSource,
|
|
461
|
-
config.colors.gitCommit,
|
|
462
|
-
state.commit.tag,
|
|
463
|
-
)
|
|
464
|
-
: "";
|
|
453
|
+
return git.tag;
|
|
465
454
|
case "git_metrics":
|
|
466
|
-
return
|
|
467
|
-
theme,
|
|
468
|
-
state.metrics,
|
|
469
|
-
config.gitMetrics,
|
|
470
|
-
colorSource,
|
|
471
|
-
config.colors.gitMetricsAdded,
|
|
472
|
-
config.colors.gitMetricsDeleted,
|
|
473
|
-
);
|
|
474
|
-
case "git_added":
|
|
475
|
-
return state.metrics
|
|
476
|
-
? renderStyleForSource(
|
|
477
|
-
theme,
|
|
478
|
-
colorSource,
|
|
479
|
-
config.colors.gitMetricsAdded,
|
|
480
|
-
`+${state.metrics.added}`,
|
|
481
|
-
)
|
|
482
|
-
: "";
|
|
483
|
-
case "git_deleted":
|
|
484
|
-
return state.metrics
|
|
485
|
-
? renderStyleForSource(
|
|
486
|
-
theme,
|
|
487
|
-
colorSource,
|
|
488
|
-
config.colors.gitMetricsDeleted,
|
|
489
|
-
`−${state.metrics.deleted}`,
|
|
490
|
-
)
|
|
491
|
-
: "";
|
|
455
|
+
return git.metrics;
|
|
492
456
|
default:
|
|
493
457
|
return "";
|
|
494
458
|
}
|
|
495
459
|
};
|
|
496
|
-
const branchParts: string[] = [];
|
|
497
|
-
if (config.footerSegments.gitBranch) {
|
|
498
|
-
if (branchText) {
|
|
499
|
-
branchParts.push("on", gitIcon, gitColor(branchText));
|
|
500
|
-
} else if (state.commit?.detached) {
|
|
501
|
-
// `HEAD` uses git-branch style; `(hash)` uses git-commit style
|
|
502
|
-
// (bold green) per Starship `git_commit` format.
|
|
503
|
-
branchParts.push("on", gitIcon, gitColor("HEAD"));
|
|
504
|
-
if (config.footerSegments.gitCommit && state.commit.oid) {
|
|
505
|
-
const shortHash = state.commit.oid.slice(0, config.gitCommit.hashLength);
|
|
506
|
-
const tag = config.gitCommit.showTag && state.commit.tag ? state.commit.tag : "";
|
|
507
|
-
const inner = [shortHash, tag].filter(Boolean).join(" ");
|
|
508
|
-
branchParts.push(
|
|
509
|
-
renderStyleForSource(theme, colorSource, config.colors.gitCommit, `(${inner})`),
|
|
510
|
-
);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
const gitStatusParts = config.footerSegments.gitStatus && statusBlock ? [statusBlock] : [];
|
|
515
|
-
const showGitState = config.footerSegments.gitBranch || config.footerSegments.gitStatus;
|
|
516
|
-
const gitStateParts = showGitState && gitStateBlock ? [gitStateBlock] : [];
|
|
517
|
-
const branchLabel = [...branchParts, ...gitStatusParts, ...gitStateParts]
|
|
518
|
-
.filter(Boolean)
|
|
519
|
-
.join(" ");
|
|
520
|
-
const runtimeLabel = config.footerSegments.runtime
|
|
521
|
-
? formatRuntimeSegment(
|
|
522
|
-
theme,
|
|
523
|
-
state.runtime,
|
|
524
|
-
config.colors.runtimePrefix,
|
|
525
|
-
colorSource,
|
|
526
|
-
iconMode,
|
|
527
|
-
)
|
|
528
|
-
: "";
|
|
529
|
-
const packageVersionLabel = config.footerSegments.packageVersion
|
|
530
|
-
? formatPackageVersionSegment(
|
|
531
|
-
theme,
|
|
532
|
-
state.packageVersion,
|
|
533
|
-
colorSource,
|
|
534
|
-
iconMode,
|
|
535
|
-
config.icons.package,
|
|
536
|
-
config.colors.packageVersion,
|
|
537
|
-
)
|
|
538
|
-
: "";
|
|
539
|
-
// Skip standalone gitCommit when hash is already folded into the
|
|
540
|
-
// branch display on detached HEAD.
|
|
541
|
-
const hashFoldedIntoBranch = state.commit?.detached && config.footerSegments.gitBranch;
|
|
542
|
-
const gitCommitLabel =
|
|
543
|
-
config.footerSegments.gitCommit && !hashFoldedIntoBranch
|
|
544
|
-
? formatGitCommitSegment(
|
|
545
|
-
theme,
|
|
546
|
-
state.commit,
|
|
547
|
-
config.gitCommit,
|
|
548
|
-
colorSource,
|
|
549
|
-
config.colors.gitCommit,
|
|
550
|
-
)
|
|
551
|
-
: "";
|
|
552
|
-
const gitMetricsLabel = config.footerSegments.gitMetrics
|
|
553
|
-
? formatGitMetricsSegment(
|
|
554
|
-
theme,
|
|
555
|
-
state.metrics,
|
|
556
|
-
config.gitMetrics,
|
|
557
|
-
colorSource,
|
|
558
|
-
config.colors.gitMetricsAdded,
|
|
559
|
-
config.colors.gitMetricsDeleted,
|
|
560
|
-
)
|
|
561
|
-
: "";
|
|
562
|
-
|
|
563
|
-
const sessionDurationSegment = (() => {
|
|
564
|
-
if (!config.footerSegments.sessionDuration || !state.sessionStartEpoch) return "";
|
|
565
|
-
const timeLabel = buildSessionDurationLabel(state.sessionStartEpoch);
|
|
566
|
-
const prefix = renderStyleForSource(theme, colorSource, "", "up for");
|
|
567
|
-
const time = renderStyleForSource(
|
|
568
|
-
theme,
|
|
569
|
-
colorSource,
|
|
570
|
-
config.colors.sessionDuration,
|
|
571
|
-
timeLabel,
|
|
572
|
-
);
|
|
573
|
-
return `${prefix} ${time}`;
|
|
574
|
-
})();
|
|
575
|
-
const usernameSegment = config.footerSegments.username
|
|
576
|
-
? renderStyleForSource(
|
|
577
|
-
theme,
|
|
578
|
-
colorSource,
|
|
579
|
-
config.colors.username,
|
|
580
|
-
formatUsernameHostLabel(config.icons.username),
|
|
581
|
-
)
|
|
582
|
-
: "";
|
|
583
|
-
const osSegment = config.footerSegments.os
|
|
584
|
-
? renderStyleForSource(
|
|
585
|
-
theme,
|
|
586
|
-
colorSource,
|
|
587
|
-
config.colors.os,
|
|
588
|
-
formatOsLabel(config.icons.os, iconMode),
|
|
589
|
-
)
|
|
590
|
-
: "";
|
|
591
|
-
const left = [
|
|
592
|
-
config.footerSegments.model ? modelLabel : "",
|
|
593
|
-
config.footerSegments.thinking ? thinkingLabel : "",
|
|
594
|
-
osSegment,
|
|
595
|
-
usernameSegment,
|
|
596
|
-
config.footerSegments.cwd ? cwdLabel : "",
|
|
597
|
-
config.footerSegments.sessionName ? builtInSessionNameLabel : "",
|
|
598
|
-
branchLabel,
|
|
599
|
-
gitCommitLabel,
|
|
600
|
-
gitMetricsLabel,
|
|
601
|
-
packageVersionLabel,
|
|
602
|
-
runtimeLabel,
|
|
603
|
-
sessionDurationSegment,
|
|
604
|
-
]
|
|
605
|
-
.filter(Boolean)
|
|
606
|
-
.join(" ");
|
|
607
|
-
|
|
608
|
-
const timeSegment = config.footerSegments.time
|
|
609
|
-
? renderStyleForSource(
|
|
610
|
-
theme,
|
|
611
|
-
colorSource,
|
|
612
|
-
config.colors.time,
|
|
613
|
-
formatTimeLabel(config.icons.time),
|
|
614
|
-
)
|
|
615
|
-
: "";
|
|
616
|
-
const right = [
|
|
617
|
-
config.footerSegments.context
|
|
618
|
-
? renderStyleForSource(theme, colorSource, contextColor, contextDisplay)
|
|
619
|
-
: "",
|
|
620
|
-
config.footerSegments.tokens
|
|
621
|
-
? renderStyleForSource(theme, colorSource, config.colors.tokens, tokensDisplay)
|
|
622
|
-
: "",
|
|
623
|
-
config.footerSegments.cacheHit && cacheHitDisplay
|
|
624
|
-
? renderStyleForSource(theme, colorSource, config.colors.cacheHit, cacheHitDisplay)
|
|
625
|
-
: "",
|
|
626
|
-
config.footerSegments.cost
|
|
627
|
-
? renderStyleForSource(theme, colorSource, config.colors.cost, costDisplay)
|
|
628
|
-
: "",
|
|
629
|
-
timeSegment,
|
|
630
|
-
]
|
|
631
|
-
.filter(Boolean)
|
|
632
|
-
.join(separator);
|
|
633
|
-
|
|
634
|
-
let contentLeft = left;
|
|
635
|
-
let contentMiddle = "";
|
|
636
|
-
let contentRight = right;
|
|
637
|
-
if (config.footerFormat) {
|
|
638
|
-
const {
|
|
639
|
-
left: fmtLeft,
|
|
640
|
-
middle: fmtMiddle,
|
|
641
|
-
right: fmtRight,
|
|
642
|
-
} = renderFormatSplit(parseFooterFormat(config.footerFormat), renderVariable);
|
|
643
|
-
contentLeft = stripOrphanSeparators(fmtLeft);
|
|
644
|
-
contentMiddle = stripOrphanSeparators(fmtMiddle);
|
|
645
|
-
contentRight = stripOrphanSeparators(fmtRight);
|
|
646
|
-
}
|
|
647
|
-
|
|
648
460
|
const extensionStatuses = collectExtensionStatusSegments(
|
|
649
461
|
footerData.getExtensionStatuses(),
|
|
650
462
|
config,
|
|
651
463
|
);
|
|
652
464
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
}
|
|
700
|
-
};
|
|
701
|
-
|
|
702
|
-
const bar = renderPillBar(
|
|
703
|
-
collectPillInputs(
|
|
704
|
-
config.pill.segments,
|
|
705
|
-
[...extensionStatuses.left, ...extensionStatuses.middle, ...extensionStatuses.right],
|
|
706
|
-
renderVariable,
|
|
707
|
-
pillSpecFor,
|
|
708
|
-
(key) => getExtensionStatusColor(config, key) ?? config.colors.extensionStatus,
|
|
709
|
-
(key) => getExtensionStatusIcon(config, key),
|
|
710
|
-
),
|
|
711
|
-
theme,
|
|
712
|
-
colorSource,
|
|
713
|
-
config.pill,
|
|
714
|
-
Math.max(0, width - 2),
|
|
715
|
-
{ ascii: iconMode === "ascii" },
|
|
716
|
-
);
|
|
717
|
-
// An empty bar means nothing had content; fall through to the text
|
|
718
|
-
// footer rather than emit a bare pair of caps.
|
|
719
|
-
if (bar) return [truncateToWidth(width > 2 ? ` ${bar} ` : bar, width, "")];
|
|
720
|
-
}
|
|
465
|
+
// Several segment colours are dynamic — the context tier, the detected
|
|
466
|
+
// runtime, the thinking level — so the spec lookup lives here where
|
|
467
|
+
// that state is already resolved, and pill.ts stays presentation-only.
|
|
468
|
+
const pillSpecFor = (segment: string): ColorSpec => {
|
|
469
|
+
switch (segment) {
|
|
470
|
+
case "model":
|
|
471
|
+
return config.colors.model;
|
|
472
|
+
case "thinking":
|
|
473
|
+
return config.colors.thinking ?? thinkingThemeKey(thinkingText);
|
|
474
|
+
case "queue":
|
|
475
|
+
return config.colors.queue;
|
|
476
|
+
case "subagents":
|
|
477
|
+
return config.colors.subagents;
|
|
478
|
+
case "cwd":
|
|
479
|
+
return config.colors.cwd;
|
|
480
|
+
case "sessionName":
|
|
481
|
+
return config.colors.sessionName;
|
|
482
|
+
case "git":
|
|
483
|
+
return config.colors.gitBranch;
|
|
484
|
+
case "gitCommit":
|
|
485
|
+
case "gitTag":
|
|
486
|
+
return config.colors.gitCommit;
|
|
487
|
+
case "gitMetrics":
|
|
488
|
+
return config.colors.gitMetricsAdded;
|
|
489
|
+
case "runtime":
|
|
490
|
+
return state.runtime?.style ?? config.colors.runtimePrefix;
|
|
491
|
+
case "context":
|
|
492
|
+
return contextColor;
|
|
493
|
+
case "cost":
|
|
494
|
+
return config.colors.cost;
|
|
495
|
+
case "tps":
|
|
496
|
+
return config.colors.tps;
|
|
497
|
+
case "sessionDuration":
|
|
498
|
+
return config.colors.sessionDuration;
|
|
499
|
+
case "username":
|
|
500
|
+
return config.colors.username;
|
|
501
|
+
case "time":
|
|
502
|
+
return config.colors.time;
|
|
503
|
+
case "os":
|
|
504
|
+
return config.colors.os;
|
|
505
|
+
case "packageVersion":
|
|
506
|
+
return config.colors.packageVersion;
|
|
507
|
+
default:
|
|
508
|
+
return config.colors.extensionStatus;
|
|
509
|
+
}
|
|
510
|
+
};
|
|
721
511
|
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
512
|
+
const spans: PillSpan[] = [];
|
|
513
|
+
const bar = renderPillBar(
|
|
514
|
+
collectPillInputs(
|
|
515
|
+
config.pill.segments,
|
|
516
|
+
[...extensionStatuses.left, ...extensionStatuses.middle, ...extensionStatuses.right],
|
|
517
|
+
renderVariable,
|
|
518
|
+
pillSpecFor,
|
|
519
|
+
(key) => getExtensionStatusColor(config, key) ?? config.colors.extensionStatus,
|
|
520
|
+
(key) => getExtensionStatusIcon(config, key),
|
|
521
|
+
),
|
|
522
|
+
theme,
|
|
523
|
+
colorSource,
|
|
524
|
+
config.pill,
|
|
525
|
+
width,
|
|
526
|
+
{
|
|
527
|
+
ascii: iconMode === "ascii",
|
|
528
|
+
onSpan: (span) => spans.push(span),
|
|
529
|
+
},
|
|
738
530
|
);
|
|
739
|
-
|
|
740
|
-
|
|
531
|
+
// Nothing has content: draw nothing rather than a bare pair of caps.
|
|
532
|
+
if (!bar) {
|
|
533
|
+
pillSpansByWidth.delete(width);
|
|
534
|
+
clearPillAnchors();
|
|
535
|
+
return [""];
|
|
536
|
+
}
|
|
537
|
+
// Flush left, in the editor's first column: the bar's round cap sits
|
|
538
|
+
// under the editor's rail rather than a column to its right.
|
|
539
|
+
const line = truncateToWidth(bar, width, "");
|
|
540
|
+
rememberPillSpans(width, spans, 0, visibleWidth(line));
|
|
541
|
+
return [line];
|
|
741
542
|
},
|
|
742
543
|
};
|
|
544
|
+
|
|
545
|
+
return component;
|
|
743
546
|
});
|
|
744
547
|
}
|