pi-one-ui 0.1.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.
Files changed (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/extensions/app/config/renderer.ts +452 -0
  4. package/extensions/app/config/shell.ts +2484 -0
  5. package/extensions/app/ownership.ts +18 -0
  6. package/extensions/app/panel.ts +533 -0
  7. package/extensions/app/presets.ts +36 -0
  8. package/extensions/features/agent-summary/core.ts +194 -0
  9. package/extensions/features/agent-summary/index.ts +37 -0
  10. package/extensions/features/compact-thinking.ts +1262 -0
  11. package/extensions/features/context.ts +857 -0
  12. package/extensions/features/reference/index.ts +643 -0
  13. package/extensions/features/reference/session.ts +278 -0
  14. package/extensions/features/reference/subagent.ts +218 -0
  15. package/extensions/features/shell/aliases.ts +28 -0
  16. package/extensions/features/shell/flush-docked-bash.ts +39 -0
  17. package/extensions/features/shell/startup-header.ts +357 -0
  18. package/extensions/features/shell/working-message.ts +226 -0
  19. package/extensions/index.ts +52 -0
  20. package/extensions/shell/accent-rail-editor.ts +112 -0
  21. package/extensions/shell/accent-rail-layout-patch.ts +530 -0
  22. package/extensions/shell/completion-menu.ts +186 -0
  23. package/extensions/shell/editor-metadata-format.ts +287 -0
  24. package/extensions/shell/editor-transfer.ts +73 -0
  25. package/extensions/shell/extension-status.ts +102 -0
  26. package/extensions/shell/footer-format.ts +388 -0
  27. package/extensions/shell/footer-layout.ts +144 -0
  28. package/extensions/shell/footer.ts +1035 -0
  29. package/extensions/shell/format.ts +577 -0
  30. package/extensions/shell/git.ts +434 -0
  31. package/extensions/shell/icons.ts +261 -0
  32. package/extensions/shell/index.ts +1659 -0
  33. package/extensions/shell/interaction-summary.ts +1194 -0
  34. package/extensions/shell/live-context.ts +82 -0
  35. package/extensions/shell/minimalist-editor.ts +548 -0
  36. package/extensions/shell/package-version.ts +675 -0
  37. package/extensions/shell/project-refresh.ts +130 -0
  38. package/extensions/shell/project-state.ts +60 -0
  39. package/extensions/shell/prototype-patch-registry.ts +183 -0
  40. package/extensions/shell/runtime.ts +915 -0
  41. package/extensions/shell/selector-border.ts +109 -0
  42. package/extensions/shell/session-lifecycle.ts +60 -0
  43. package/extensions/shell/settings-command.ts +2144 -0
  44. package/extensions/shell/settings-previews.ts +137 -0
  45. package/extensions/shell/state.ts +83 -0
  46. package/extensions/shell/style.ts +480 -0
  47. package/extensions/shell/telemetry.ts +90 -0
  48. package/extensions/shell/ui.ts +1326 -0
  49. package/extensions/shell/user-message-osc.ts +279 -0
  50. package/extensions/shell/user-message-styles.ts +266 -0
  51. package/extensions/shell/user-message.ts +226 -0
  52. package/extensions/shell/working-line-messages.ts +24 -0
  53. package/extensions/shell/working-line-spinners.ts +54 -0
  54. package/extensions/shell/working-line.ts +1603 -0
  55. package/extensions/tools/ansi-color.ts +44 -0
  56. package/extensions/tools/ansi-text.ts +52 -0
  57. package/extensions/tools/component-tree.ts +37 -0
  58. package/extensions/tools/format.ts +28 -0
  59. package/extensions/tools/fullscreen-detect.ts +11 -0
  60. package/extensions/tools/patch-keys.ts +158 -0
  61. package/extensions/tools/sgr-mouse.ts +59 -0
  62. package/extensions/tools/tool-loading-icon.ts +20 -0
  63. package/extensions/tools/tool-result-sanitize.ts +27 -0
  64. package/extensions/transcript/index.ts +65 -0
  65. package/extensions/transcript/renderer/compact-mode.ts +1443 -0
  66. package/extensions/transcript/renderer/default-mode.ts +735 -0
  67. package/extensions/transcript/renderer/index.ts +286 -0
  68. package/extensions/transcript/renderer/markdown-enhance.ts +325 -0
  69. package/extensions/transcript/renderer/mouse/hover.ts +175 -0
  70. package/extensions/transcript/renderer/mouse/interaction.ts +1016 -0
  71. package/extensions/transcript/renderer/mouse/layout.ts +207 -0
  72. package/extensions/transcript/renderer/mouse/packets.ts +96 -0
  73. package/extensions/transcript/renderer/mouse/scroll.ts +211 -0
  74. package/extensions/transcript/renderer/tool/diff/ATTRIBUTION.md +28 -0
  75. package/extensions/transcript/renderer/tool/diff/ansi-utils.ts +128 -0
  76. package/extensions/transcript/renderer/tool/diff/diff-component.ts +112 -0
  77. package/extensions/transcript/renderer/tool/diff/diff-edit-render.ts +255 -0
  78. package/extensions/transcript/renderer/tool/diff/diff-header.ts +184 -0
  79. package/extensions/transcript/renderer/tool/diff/diff-highlight.ts +131 -0
  80. package/extensions/transcript/renderer/tool/diff/diff-inline.ts +350 -0
  81. package/extensions/transcript/renderer/tool/diff/diff-layout.ts +870 -0
  82. package/extensions/transcript/renderer/tool/diff/diff-limits.ts +189 -0
  83. package/extensions/transcript/renderer/tool/diff/diff-palette.ts +436 -0
  84. package/extensions/transcript/renderer/tool/diff/diff-parse.ts +536 -0
  85. package/extensions/transcript/renderer/tool/diff/diff-presentation.ts +79 -0
  86. package/extensions/transcript/renderer/tool/diff/diff-renderer.ts +29 -0
  87. package/extensions/transcript/renderer/tool/diff/diff-text.ts +57 -0
  88. package/extensions/transcript/renderer/tool/diff/diff-write-render.ts +629 -0
  89. package/extensions/transcript/renderer/tool/diff/index.ts +147 -0
  90. package/extensions/transcript/renderer/tool/diff/line-width-safety.ts +100 -0
  91. package/extensions/transcript/renderer/tool/diff/shiki-highlight.ts +104 -0
  92. package/extensions/transcript/renderer/tool/diff/write-execution.ts +124 -0
  93. package/extensions/transcript/renderer/tool/grouping.ts +619 -0
  94. package/extensions/transcript/renderer/tool/message-display.ts +183 -0
  95. package/extensions/transcript/renderer/tool/names.ts +195 -0
  96. package/extensions/transcript/renderer/tool/result.ts +842 -0
  97. package/extensions/transcript/renderer/tool/show-more-hint.ts +42 -0
  98. package/extensions/transcript/renderer/transcript-refresh.ts +79 -0
  99. package/package.json +58 -0
  100. package/themes/cc-dark.json +91 -0
  101. package/themes/cc-light.json +91 -0
@@ -0,0 +1,1035 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
+ import type { SeparatorStyle, ZentuiConfig } from "../app/config/shell.ts";
4
+ import { FOOTER_FORMAT_ALIASES } from "../app/config/shell.ts";
5
+ import { sanitizeEditorMetadataText } from "./editor-metadata-format.ts";
6
+ import {
7
+ collectExtensionStatusSegments,
8
+ type ExtensionStatusSegment,
9
+ sanitizeExtensionStatusText,
10
+ } from "./extension-status.ts";
11
+ import {
12
+ collectFooterFormatReferences,
13
+ compileCompactFormat,
14
+ parseFooterFormat,
15
+ renderFormatSplit,
16
+ renderFormatTokens,
17
+ stripOrphanSeparators,
18
+ } from "./footer-format.ts";
19
+ import {
20
+ compactChunkBudget,
21
+ fullFooterFitsAligned,
22
+ packCompactChunks,
23
+ reflowFullFooter,
24
+ } from "./footer-layout.ts";
25
+ import {
26
+ buildContextDisplayLabel,
27
+ buildSessionDurationLabel,
28
+ contextColorTier,
29
+ formatCwdLabel,
30
+ formatGitBranchText,
31
+ formatGitCommitSegment,
32
+ formatGitMetricsSegment,
33
+ formatOsLabel,
34
+ formatPackageVersionSegment,
35
+ formatRuntimeSegment,
36
+ formatTimeLabel,
37
+ formatUsernameHostLabel,
38
+ } from "./format.ts";
39
+ import { resolveRuntimeSymbol } from "./icons.ts";
40
+ import type { LiveContextOverride } from "./live-context.ts";
41
+ import { type FooterState, modelLabelFor } from "./state.ts";
42
+ import { renderStyleForSource } from "./style.ts";
43
+
44
+ const separatorText: Record<SeparatorStyle, string> = {
45
+ pipe: " | ",
46
+ dot: " · ",
47
+ chevron: " › ",
48
+ none: " ",
49
+ };
50
+
51
+ function joinStatusTexts(statusTexts: string[], separator: string): string {
52
+ return statusTexts.filter(Boolean).join(separator);
53
+ }
54
+
55
+ function normalizeModelInfoPart(value: string): string {
56
+ return value.toLowerCase().replace(/[^a-z0-9]/g, "");
57
+ }
58
+
59
+ function composeModelInfoLabel(model: string, provider: string): string {
60
+ const normalizedModel = normalizeModelInfoPart(model);
61
+ const normalizedProvider = normalizeModelInfoPart(provider);
62
+ const providerIsDuplicated =
63
+ normalizedProvider.length > 0 &&
64
+ normalizedModel.includes(normalizedProvider);
65
+ return [model, providerIsDuplicated ? "" : provider]
66
+ .filter(Boolean)
67
+ .join(" ");
68
+ }
69
+
70
+ function fitStatusTexts(
71
+ statusTexts: string[],
72
+ maxWidth: number,
73
+ separator: string,
74
+ ): string {
75
+ if (maxWidth <= 0) return "";
76
+
77
+ const fitted: string[] = [];
78
+ for (const text of statusTexts) {
79
+ const candidate = joinStatusTexts([...fitted, text], separator);
80
+ if (visibleWidth(candidate) <= maxWidth) {
81
+ fitted.push(text);
82
+ continue;
83
+ }
84
+
85
+ if (fitted.length === 0) {
86
+ return maxWidth > 1 ? truncateToWidth(text, maxWidth, "…") : "";
87
+ }
88
+ break;
89
+ }
90
+
91
+ return joinStatusTexts(fitted, separator);
92
+ }
93
+
94
+ function appendStatusArea(
95
+ base: string,
96
+ statusText: string,
97
+ separator: string,
98
+ ): string {
99
+ if (!base) return statusText;
100
+ if (!statusText) return base;
101
+ return `${base}${separator}${statusText}`;
102
+ }
103
+
104
+ function prependStatusArea(
105
+ base: string,
106
+ statusText: string,
107
+ separator: string,
108
+ ): string {
109
+ if (!base) return statusText;
110
+ if (!statusText) return base;
111
+ return `${statusText}${separator}${base}`;
112
+ }
113
+
114
+ function composeBuiltInFooterContent(
115
+ left: string,
116
+ right: string,
117
+ innerWidth: number,
118
+ ): string {
119
+ const leftWidth = visibleWidth(left);
120
+ const rightWidth = visibleWidth(right);
121
+ return leftWidth >= innerWidth
122
+ ? truncateToWidth(left, innerWidth, "")
123
+ : leftWidth + 1 + rightWidth <= innerWidth
124
+ ? `${left}${" ".repeat(innerWidth - leftWidth - rightWidth)}${right}`
125
+ : truncateToWidth(left, innerWidth, "");
126
+ }
127
+
128
+ function composeFooterContent(
129
+ builtInLeft: string,
130
+ builtInRight: string,
131
+ extensionLeft: string[],
132
+ extensionMiddle: string[],
133
+ extensionRight: string[],
134
+ separator: string,
135
+ innerWidth: number,
136
+ ): string {
137
+ const builtInLeftWidth = visibleWidth(builtInLeft);
138
+ const builtInRightWidth = visibleWidth(builtInRight);
139
+ const minimumGap = builtInLeft && builtInRight ? 1 : 0;
140
+
141
+ if (builtInLeftWidth + minimumGap + builtInRightWidth > innerWidth) {
142
+ return composeBuiltInFooterContent(builtInLeft, builtInRight, innerWidth);
143
+ }
144
+
145
+ const available = Math.max(
146
+ 0,
147
+ innerWidth - builtInLeftWidth - builtInRightWidth - minimumGap,
148
+ );
149
+ let remaining = available;
150
+ const leftConnectorWidth =
151
+ builtInLeft && extensionLeft.length > 0 ? visibleWidth(separator) : 0;
152
+ const rightConnectorWidth =
153
+ builtInRight && extensionRight.length > 0 ? visibleWidth(separator) : 0;
154
+ let leftStatus = "";
155
+ let rightStatus = "";
156
+
157
+ if (extensionLeft.length > 0 && extensionRight.length > 0) {
158
+ const leftBudget = Math.max(
159
+ 0,
160
+ Math.floor(available / 2) - leftConnectorWidth,
161
+ );
162
+ leftStatus = fitStatusTexts(extensionLeft, leftBudget, separator);
163
+ remaining -= leftStatus ? leftConnectorWidth + visibleWidth(leftStatus) : 0;
164
+
165
+ const rightBudget = Math.max(0, remaining - rightConnectorWidth);
166
+ rightStatus = fitStatusTexts(extensionRight, rightBudget, separator);
167
+ remaining -= rightStatus
168
+ ? rightConnectorWidth + visibleWidth(rightStatus)
169
+ : 0;
170
+
171
+ const expandedLeftBudget = Math.max(
172
+ 0,
173
+ remaining + visibleWidth(leftStatus),
174
+ );
175
+ const expandedLeftStatus = fitStatusTexts(
176
+ extensionLeft,
177
+ expandedLeftBudget,
178
+ separator,
179
+ );
180
+ if (visibleWidth(expandedLeftStatus) > visibleWidth(leftStatus)) {
181
+ remaining += leftStatus
182
+ ? leftConnectorWidth + visibleWidth(leftStatus)
183
+ : 0;
184
+ leftStatus = expandedLeftStatus;
185
+ remaining -= leftStatus
186
+ ? leftConnectorWidth + visibleWidth(leftStatus)
187
+ : 0;
188
+ }
189
+ } else if (extensionLeft.length > 0) {
190
+ leftStatus = fitStatusTexts(
191
+ extensionLeft,
192
+ Math.max(0, available - leftConnectorWidth),
193
+ separator,
194
+ );
195
+ remaining -= leftStatus ? leftConnectorWidth + visibleWidth(leftStatus) : 0;
196
+ } else if (extensionRight.length > 0) {
197
+ rightStatus = fitStatusTexts(
198
+ extensionRight,
199
+ Math.max(0, available - rightConnectorWidth),
200
+ separator,
201
+ );
202
+ remaining -= rightStatus
203
+ ? rightConnectorWidth + visibleWidth(rightStatus)
204
+ : 0;
205
+ }
206
+
207
+ const left = appendStatusArea(builtInLeft, leftStatus, separator);
208
+ const right = prependStatusArea(builtInRight, rightStatus, separator);
209
+ const gapWidth = Math.max(
210
+ 0,
211
+ innerWidth - visibleWidth(left) - visibleWidth(right),
212
+ );
213
+ const middle = fitStatusTexts(extensionMiddle, gapWidth, separator);
214
+ const middleWidth = visibleWidth(middle);
215
+
216
+ if (!middle || middleWidth <= 0) {
217
+ return `${left}${" ".repeat(gapWidth)}${right}`;
218
+ }
219
+
220
+ const leftPadding = Math.floor((gapWidth - middleWidth) / 2);
221
+ const rightPadding = gapWidth - middleWidth - leftPadding;
222
+ return `${left}${" ".repeat(leftPadding)}${middle}${" ".repeat(rightPadding)}${right}`;
223
+ }
224
+
225
+ export function installFooter(
226
+ ctx: ExtensionContext,
227
+ state: FooterState,
228
+ getConfig: () => ZentuiConfig,
229
+ hooks: {
230
+ setRequestRender: (fn: (() => void) | undefined) => void;
231
+ scheduleProjectRefresh: (ctx: ExtensionContext) => void;
232
+ setExtensionStatusesGetter?: (
233
+ fn: (() => ReadonlyMap<string, string>) | undefined,
234
+ ) => void;
235
+ getLiveContext?: () => LiveContextOverride | undefined;
236
+ onDispose?: () => void;
237
+ },
238
+ ): void {
239
+ ctx.ui.setFooter((tui, theme, footerData) => {
240
+ hooks.setRequestRender(() => tui.requestRender());
241
+ hooks.setExtensionStatusesGetter?.(() => footerData.getExtensionStatuses());
242
+ const unsubscribeBranch = footerData.onBranchChange(() => {
243
+ hooks.scheduleProjectRefresh(ctx);
244
+ tui.requestRender();
245
+ });
246
+
247
+ return {
248
+ dispose: () => {
249
+ unsubscribeBranch();
250
+ hooks.setRequestRender(undefined);
251
+ hooks.setExtensionStatusesGetter?.(undefined);
252
+ hooks.onDispose?.();
253
+ },
254
+ invalidate() {},
255
+ render(width: number): string[] {
256
+ if (width <= 0) return [""];
257
+ const config = getConfig();
258
+ const footer = config.components.footer;
259
+ const footerModelLabel = modelLabelFor(state, footer.modelLabel);
260
+ const wideFormatTokens = config.components.footer.styles.starship.format
261
+ ? parseFooterFormat(config.components.footer.styles.starship.format)
262
+ : [];
263
+ const compactFormatTokens = config.components.footer.styles.starship
264
+ .responsive
265
+ ? parseFooterFormat(
266
+ config.components.footer.styles.starship.compactFormat,
267
+ )
268
+ : [];
269
+ const wideReferences = collectFooterFormatReferences(
270
+ wideFormatTokens,
271
+ FOOTER_FORMAT_ALIASES,
272
+ );
273
+ const compactReferences = collectFooterFormatReferences(
274
+ compactFormatTokens,
275
+ FOOTER_FORMAT_ALIASES,
276
+ );
277
+ const colorSource = config.components.footer.colorSource;
278
+ const iconMode = config.icons.mode;
279
+ const formattedCwd = sanitizeEditorMetadataText(
280
+ formatCwdLabel(ctx.cwd, config.icons.cwd, {
281
+ mode: config.components.footer.styles.starship.pathDisplay.mode,
282
+ depth: config.components.footer.styles.starship.pathDisplay.depth,
283
+ }),
284
+ );
285
+ const branch =
286
+ sanitizeEditorMetadataText(state.branch ?? "") || undefined;
287
+ const gitStateLabel = sanitizeEditorMetadataText(
288
+ state.gitStateLabel ?? "",
289
+ );
290
+ const commit = state.commit
291
+ ? {
292
+ ...state.commit,
293
+ oid: state.commit.oid
294
+ ? sanitizeEditorMetadataText(state.commit.oid) || null
295
+ : null,
296
+ tag: state.commit.tag
297
+ ? sanitizeEditorMetadataText(state.commit.tag) || null
298
+ : null,
299
+ }
300
+ : undefined;
301
+ const runtime = state.runtime
302
+ ? {
303
+ ...state.runtime,
304
+ name: sanitizeEditorMetadataText(state.runtime.name),
305
+ symbol: sanitizeEditorMetadataText(state.runtime.symbol),
306
+ version: sanitizeEditorMetadataText(state.runtime.version ?? ""),
307
+ }
308
+ : undefined;
309
+ const packageVersion = state.packageVersion
310
+ ? {
311
+ ...state.packageVersion,
312
+ version: sanitizeEditorMetadataText(state.packageVersion.version),
313
+ }
314
+ : undefined;
315
+ const separator = renderStyleForSource(
316
+ theme,
317
+ colorSource,
318
+ config.colors.separator,
319
+ separatorText[config.components.footer.styles.starship.separator],
320
+ );
321
+ const innerWidth = Math.max(1, width - 2);
322
+ const cwdLabel = renderStyleForSource(
323
+ theme,
324
+ colorSource,
325
+ config.colors.cwd,
326
+ formattedCwd,
327
+ );
328
+ const needsSessionName =
329
+ (config.components.footer.styles.starship.format
330
+ ? wideReferences.has("session_name")
331
+ : config.components.footer.styles.starship.segments.sessionName) ||
332
+ compactReferences.has("session_name");
333
+ const sessionName = needsSessionName
334
+ ? sanitizeExtensionStatusText(
335
+ ctx.sessionManager.getSessionName() ?? "",
336
+ )
337
+ : "";
338
+ const sessionNameLabel = sessionName
339
+ ? renderStyleForSource(
340
+ theme,
341
+ colorSource,
342
+ config.colors.sessionName,
343
+ sessionName,
344
+ )
345
+ : "";
346
+ const builtInSessionNameLabel = sessionNameLabel
347
+ ? `in ${sessionNameLabel}`
348
+ : "";
349
+ const branchText = branch
350
+ ? formatGitBranchText(
351
+ branch,
352
+ config.components.footer.styles.starship.gitBranch.maxLength,
353
+ )
354
+ : undefined;
355
+ const contextUsage = ctx.getContextUsage();
356
+ const liveContext = hooks.getLiveContext?.();
357
+ const contextWindow =
358
+ ctx.model?.contextWindow ?? contextUsage?.contextWindow;
359
+ const useLiveContext =
360
+ liveContext !== undefined &&
361
+ contextWindow !== undefined &&
362
+ contextWindow > 0;
363
+ const contextPercent = useLiveContext
364
+ ? (liveContext.tokens / contextWindow) * 100
365
+ : contextUsage?.percent;
366
+ const contextLabel = buildContextDisplayLabel({
367
+ percent: contextPercent,
368
+ contextWindow,
369
+ style: config.components.footer.styles.starship.contextStyle,
370
+ asciiGauge: iconMode === "ascii",
371
+ });
372
+ const tier = contextColorTier(
373
+ contextPercent,
374
+ config.components.footer.styles.starship.contextThresholds,
375
+ );
376
+ const contextColor =
377
+ tier === "error"
378
+ ? config.colors.contextError
379
+ : tier === "warning"
380
+ ? config.colors.contextWarning
381
+ : config.colors.contextNormal;
382
+ const cacheReadLabel = state.cacheReadLabel
383
+ ? renderStyleForSource(
384
+ theme,
385
+ colorSource,
386
+ config.colors.tokens,
387
+ state.cacheReadLabel,
388
+ )
389
+ : "";
390
+ const cacheWriteLabel = state.cacheWriteLabel
391
+ ? renderStyleForSource(
392
+ theme,
393
+ colorSource,
394
+ config.colors.tokens,
395
+ state.cacheWriteLabel,
396
+ )
397
+ : "";
398
+ const subscriptionLabel = state.subscription
399
+ ? renderStyleForSource(
400
+ theme,
401
+ colorSource,
402
+ config.colors.cost,
403
+ "(sub)",
404
+ )
405
+ : "";
406
+ const autoCompactionLabel = state.autoCompaction
407
+ ? renderStyleForSource(theme, colorSource, contextColor, "(auto)")
408
+ : "";
409
+ const gitColor = (text: string) =>
410
+ renderStyleForSource(
411
+ theme,
412
+ colorSource,
413
+ config.colors.gitBranch,
414
+ text,
415
+ );
416
+ const gitStatusColor = (text: string) =>
417
+ renderStyleForSource(
418
+ theme,
419
+ colorSource,
420
+ config.colors.gitStatus,
421
+ text,
422
+ );
423
+ const gitIcon = config.icons.git ? gitColor(config.icons.git) : "";
424
+ const gitCounts =
425
+ config.components.footer.styles.starship.segments.gitCounts;
426
+ const stashLabel =
427
+ state.stashed > 0
428
+ ? gitCounts
429
+ ? `${config.icons.stashed}${state.stashed}`
430
+ : config.icons.stashed
431
+ : "";
432
+ const allStatus = [
433
+ state.conflicted > 0 ? config.icons.conflicted : "",
434
+ stashLabel,
435
+ state.deleted > 0 ? config.icons.deleted : "",
436
+ state.renamed > 0 ? config.icons.renamed : "",
437
+ state.modified > 0 ? config.icons.modified : "",
438
+ state.typechanged > 0 ? config.icons.typechanged : "",
439
+ state.staged > 0 ? config.icons.staged : "",
440
+ state.untracked > 0 ? config.icons.untracked : "",
441
+ ].join("");
442
+ const aheadBehind = (() => {
443
+ if (state.ahead > 0 && state.behind > 0) {
444
+ return gitCounts
445
+ ? `${config.icons.ahead}${state.ahead}${config.icons.behind}${state.behind}`
446
+ : config.icons.diverged;
447
+ }
448
+ if (state.ahead > 0)
449
+ return gitCounts
450
+ ? `${config.icons.ahead}${state.ahead}`
451
+ : config.icons.ahead;
452
+ if (state.behind > 0)
453
+ return gitCounts
454
+ ? `${config.icons.behind}${state.behind}`
455
+ : config.icons.behind;
456
+ return "";
457
+ })();
458
+ const statusBlock =
459
+ allStatus || aheadBehind
460
+ ? gitStatusColor(`[${allStatus}${aheadBehind}]`)
461
+ : "";
462
+ const gitStateBlock = gitStateLabel
463
+ ? gitStatusColor(gitStateLabel)
464
+ : "";
465
+ const renderVariable = (name: string): string => {
466
+ const canonical = FOOTER_FORMAT_ALIASES[name] ?? name;
467
+ switch (canonical) {
468
+ case "cwd":
469
+ return cwdLabel;
470
+ case "session_name":
471
+ return sessionNameLabel;
472
+ case "git_branch":
473
+ return branchText
474
+ ? gitIcon
475
+ ? `${gitIcon} ${gitColor(branchText)}`
476
+ : gitColor(branchText)
477
+ : "";
478
+ case "git_status":
479
+ return statusBlock;
480
+ case "git_state":
481
+ return gitStateBlock;
482
+ case "runtime": {
483
+ if (!runtime) return "";
484
+ const symbol = resolveRuntimeSymbol(
485
+ runtime.name,
486
+ runtime.symbol,
487
+ iconMode,
488
+ );
489
+ const label = runtime.version
490
+ ? `${symbol} ${runtime.version}`
491
+ : symbol;
492
+ return renderStyleForSource(
493
+ theme,
494
+ colorSource,
495
+ runtime.style,
496
+ label,
497
+ );
498
+ }
499
+ case "model":
500
+ return sanitizeExtensionStatusText(footerModelLabel);
501
+ case "provider":
502
+ return sanitizeExtensionStatusText(state.providerLabel);
503
+ case "session_duration":
504
+ return state.sessionStartEpoch
505
+ ? renderStyleForSource(
506
+ theme,
507
+ colorSource,
508
+ config.colors.sessionDuration,
509
+ buildSessionDurationLabel(state.sessionStartEpoch),
510
+ )
511
+ : "";
512
+ case "username":
513
+ return renderStyleForSource(
514
+ theme,
515
+ colorSource,
516
+ config.colors.username,
517
+ formatUsernameHostLabel(config.icons.username),
518
+ );
519
+ case "os":
520
+ return renderStyleForSource(
521
+ theme,
522
+ colorSource,
523
+ config.colors.os,
524
+ formatOsLabel(config.icons.os, iconMode),
525
+ );
526
+ case "time":
527
+ return renderStyleForSource(
528
+ theme,
529
+ colorSource,
530
+ config.colors.time,
531
+ formatTimeLabel(config.icons.time),
532
+ );
533
+ case "context":
534
+ return renderStyleForSource(
535
+ theme,
536
+ colorSource,
537
+ contextColor,
538
+ contextLabel,
539
+ );
540
+ case "tokens":
541
+ return renderStyleForSource(
542
+ theme,
543
+ colorSource,
544
+ config.colors.tokens,
545
+ state.tokenLabel,
546
+ );
547
+ case "cache_read":
548
+ return cacheReadLabel;
549
+ case "cache_write":
550
+ return cacheWriteLabel;
551
+ case "cost":
552
+ return renderStyleForSource(
553
+ theme,
554
+ colorSource,
555
+ config.colors.cost,
556
+ state.costLabel,
557
+ );
558
+ case "subscription":
559
+ return subscriptionLabel;
560
+ case "auto_compaction":
561
+ return autoCompactionLabel;
562
+ case "package":
563
+ return formatPackageVersionSegment(
564
+ theme,
565
+ packageVersion,
566
+ colorSource,
567
+ iconMode,
568
+ config.icons.package,
569
+ config.colors.packageVersion,
570
+ );
571
+ case "package_version":
572
+ return packageVersion?.version
573
+ ? renderStyleForSource(
574
+ theme,
575
+ colorSource,
576
+ config.colors.packageVersion,
577
+ packageVersion.version,
578
+ )
579
+ : "";
580
+ case "sep":
581
+ return renderStyleForSource(
582
+ theme,
583
+ colorSource,
584
+ config.colors.separator,
585
+ " | ",
586
+ );
587
+ case "git_commit":
588
+ return formatGitCommitSegment(
589
+ theme,
590
+ commit,
591
+ config.components.footer.styles.starship.gitCommit,
592
+ colorSource,
593
+ config.colors.gitCommit,
594
+ );
595
+ case "git_tag":
596
+ return config.components.footer.styles.starship.gitCommit
597
+ .showTag && commit?.tag
598
+ ? renderStyleForSource(
599
+ theme,
600
+ colorSource,
601
+ config.colors.gitCommit,
602
+ commit.tag,
603
+ )
604
+ : "";
605
+ case "git_metrics":
606
+ return formatGitMetricsSegment(
607
+ theme,
608
+ state.metrics,
609
+ config.components.footer.styles.starship.gitMetrics,
610
+ colorSource,
611
+ config.colors.gitMetricsAdded,
612
+ config.colors.gitMetricsDeleted,
613
+ );
614
+ case "git_added":
615
+ return state.metrics
616
+ ? renderStyleForSource(
617
+ theme,
618
+ colorSource,
619
+ config.colors.gitMetricsAdded,
620
+ `+${state.metrics.added}`,
621
+ )
622
+ : "";
623
+ case "git_deleted":
624
+ return state.metrics
625
+ ? renderStyleForSource(
626
+ theme,
627
+ colorSource,
628
+ config.colors.gitMetricsDeleted,
629
+ `−${state.metrics.deleted}`,
630
+ )
631
+ : "";
632
+ default:
633
+ return "";
634
+ }
635
+ };
636
+ const branchParts: string[] = [];
637
+ if (config.components.footer.styles.starship.segments.gitBranch) {
638
+ if (branchText) {
639
+ branchParts.push("on", gitIcon, gitColor(branchText));
640
+ } else if (commit?.detached) {
641
+ // `HEAD` uses git-branch style; `(hash)` uses git-commit style
642
+ // (bold green) per Starship `git_commit` format.
643
+ branchParts.push("on", gitIcon, gitColor("HEAD"));
644
+ if (
645
+ config.components.footer.styles.starship.segments.gitCommit &&
646
+ commit.oid
647
+ ) {
648
+ const shortHash = commit.oid.slice(
649
+ 0,
650
+ config.components.footer.styles.starship.gitCommit.hashLength,
651
+ );
652
+ const tag =
653
+ config.components.footer.styles.starship.gitCommit.showTag &&
654
+ commit.tag
655
+ ? commit.tag
656
+ : "";
657
+ const inner = [shortHash, tag].filter(Boolean).join(" ");
658
+ branchParts.push(
659
+ renderStyleForSource(
660
+ theme,
661
+ colorSource,
662
+ config.colors.gitCommit,
663
+ `(${inner})`,
664
+ ),
665
+ );
666
+ }
667
+ }
668
+ }
669
+ const gitStatusParts =
670
+ config.components.footer.styles.starship.segments.gitStatus &&
671
+ statusBlock
672
+ ? [statusBlock]
673
+ : [];
674
+ const showGitState =
675
+ config.components.footer.styles.starship.segments.gitBranch ||
676
+ config.components.footer.styles.starship.segments.gitStatus;
677
+ const gitStateParts =
678
+ showGitState && gitStateBlock ? [gitStateBlock] : [];
679
+ const branchLabel = [
680
+ ...branchParts,
681
+ ...gitStatusParts,
682
+ ...gitStateParts,
683
+ ]
684
+ .filter(Boolean)
685
+ .join(" ");
686
+ const runtimeLabel = config.components.footer.styles.starship.segments
687
+ .runtime
688
+ ? formatRuntimeSegment(
689
+ theme,
690
+ runtime,
691
+ config.colors.runtimePrefix,
692
+ colorSource,
693
+ iconMode,
694
+ )
695
+ : "";
696
+ const packageVersionLabel = config.components.footer.styles.starship
697
+ .segments.packageVersion
698
+ ? formatPackageVersionSegment(
699
+ theme,
700
+ packageVersion,
701
+ colorSource,
702
+ iconMode,
703
+ config.icons.package,
704
+ config.colors.packageVersion,
705
+ )
706
+ : "";
707
+ // Skip standalone gitCommit when hash is already folded into the
708
+ // branch display on detached HEAD.
709
+ const hashFoldedIntoBranch =
710
+ commit?.detached &&
711
+ config.components.footer.styles.starship.segments.gitBranch;
712
+ const gitCommitLabel =
713
+ config.components.footer.styles.starship.segments.gitCommit &&
714
+ !hashFoldedIntoBranch
715
+ ? formatGitCommitSegment(
716
+ theme,
717
+ commit,
718
+ config.components.footer.styles.starship.gitCommit,
719
+ colorSource,
720
+ config.colors.gitCommit,
721
+ )
722
+ : "";
723
+ const gitMetricsLabel = config.components.footer.styles.starship
724
+ .segments.gitMetrics
725
+ ? formatGitMetricsSegment(
726
+ theme,
727
+ state.metrics,
728
+ config.components.footer.styles.starship.gitMetrics,
729
+ colorSource,
730
+ config.colors.gitMetricsAdded,
731
+ config.colors.gitMetricsDeleted,
732
+ )
733
+ : "";
734
+
735
+ const sessionDurationSegment = (() => {
736
+ if (
737
+ !config.components.footer.styles.starship.segments
738
+ .sessionDuration ||
739
+ !state.sessionStartEpoch
740
+ )
741
+ return "";
742
+ const timeLabel = buildSessionDurationLabel(state.sessionStartEpoch);
743
+ const prefix = renderStyleForSource(theme, colorSource, "", "up for");
744
+ const time = renderStyleForSource(
745
+ theme,
746
+ colorSource,
747
+ config.colors.sessionDuration,
748
+ timeLabel,
749
+ );
750
+ return `${prefix} ${time}`;
751
+ })();
752
+ const usernameSegment = config.components.footer.styles.starship
753
+ .segments.username
754
+ ? renderStyleForSource(
755
+ theme,
756
+ colorSource,
757
+ config.colors.username,
758
+ formatUsernameHostLabel(config.icons.username),
759
+ )
760
+ : "";
761
+ const osSegment = config.components.footer.styles.starship.segments.os
762
+ ? renderStyleForSource(
763
+ theme,
764
+ colorSource,
765
+ config.colors.os,
766
+ formatOsLabel(config.icons.os, iconMode),
767
+ )
768
+ : "";
769
+ const left = [
770
+ osSegment,
771
+ usernameSegment,
772
+ config.components.footer.styles.starship.segments.cwd ? cwdLabel : "",
773
+ config.components.footer.styles.starship.segments.sessionName
774
+ ? builtInSessionNameLabel
775
+ : "",
776
+ branchLabel,
777
+ gitCommitLabel,
778
+ gitMetricsLabel,
779
+ packageVersionLabel,
780
+ runtimeLabel,
781
+ sessionDurationSegment,
782
+ ]
783
+ .filter(Boolean)
784
+ .join(" ");
785
+
786
+ const modelInfoSegment = config.components.footer.styles.starship
787
+ .segments.modelInfo
788
+ ? composeModelInfoLabel(
789
+ sanitizeExtensionStatusText(footerModelLabel),
790
+ sanitizeExtensionStatusText(state.providerLabel),
791
+ )
792
+ : "";
793
+ const timeSegment = config.components.footer.styles.starship.segments
794
+ .time
795
+ ? renderStyleForSource(
796
+ theme,
797
+ colorSource,
798
+ config.colors.time,
799
+ formatTimeLabel(config.icons.time),
800
+ )
801
+ : "";
802
+ const builtInContextLabel = [
803
+ renderStyleForSource(theme, colorSource, contextColor, contextLabel),
804
+ autoCompactionLabel,
805
+ ]
806
+ .filter(Boolean)
807
+ .join(" ");
808
+ const builtInTokenLabel = [
809
+ renderStyleForSource(
810
+ theme,
811
+ colorSource,
812
+ config.colors.tokens,
813
+ state.tokenLabel,
814
+ ),
815
+ cacheReadLabel,
816
+ cacheWriteLabel,
817
+ ]
818
+ .filter(Boolean)
819
+ .join(" ");
820
+ const builtInCostLabel = [
821
+ renderStyleForSource(
822
+ theme,
823
+ colorSource,
824
+ config.colors.cost,
825
+ state.costLabel,
826
+ ),
827
+ subscriptionLabel,
828
+ ]
829
+ .filter(Boolean)
830
+ .join(" ");
831
+ const right = [
832
+ modelInfoSegment,
833
+ config.components.footer.styles.starship.segments.context
834
+ ? builtInContextLabel
835
+ : "",
836
+ config.components.footer.styles.starship.segments.tokens
837
+ ? builtInTokenLabel
838
+ : "",
839
+ config.components.footer.styles.starship.segments.cost
840
+ ? builtInCostLabel
841
+ : "",
842
+ timeSegment,
843
+ ]
844
+ .filter(Boolean)
845
+ .join(separator);
846
+
847
+ let contentLeft = left;
848
+ let contentMiddle = "";
849
+ let contentRight = right;
850
+ if (config.components.footer.styles.starship.format) {
851
+ const {
852
+ left: fmtLeft,
853
+ middle: fmtMiddle,
854
+ right: fmtRight,
855
+ } = renderFormatSplit(wideFormatTokens, renderVariable);
856
+ contentLeft = stripOrphanSeparators(fmtLeft);
857
+ contentMiddle = stripOrphanSeparators(fmtMiddle);
858
+ contentRight = stripOrphanSeparators(fmtRight);
859
+ }
860
+
861
+ const extensionStatuses = collectExtensionStatusSegments(
862
+ footerData.getExtensionStatuses(),
863
+ config,
864
+ );
865
+ const renderExtensionStatus = (segment: ExtensionStatusSegment) =>
866
+ segment.colorMode === "original"
867
+ ? segment.text
868
+ : renderStyleForSource(
869
+ theme,
870
+ colorSource,
871
+ config.colors.extensionStatus,
872
+ segment.text,
873
+ );
874
+ const extensionLeftSegments = extensionStatuses.left.map(
875
+ renderExtensionStatus,
876
+ );
877
+ const extensionMiddleSegments = extensionStatuses.middle.map(
878
+ renderExtensionStatus,
879
+ );
880
+ const extensionRightSegments = extensionStatuses.right.map(
881
+ renderExtensionStatus,
882
+ );
883
+ const middleSegments = contentMiddle
884
+ ? [contentMiddle, ...extensionMiddleSegments]
885
+ : extensionMiddleSegments;
886
+ const renderLegacyContent = () =>
887
+ composeFooterContent(
888
+ contentLeft,
889
+ contentRight,
890
+ extensionLeftSegments,
891
+ middleSegments,
892
+ extensionRightSegments,
893
+ separator,
894
+ innerWidth,
895
+ );
896
+ const frameRows = (rows: string[]) =>
897
+ rows.map((row) => {
898
+ const framed =
899
+ width > 2 ? ` ${truncateToWidth(row, width - 2, "")} ` : row;
900
+ return truncateToWidth(framed, width, "");
901
+ });
902
+
903
+ if (!config.components.footer.styles.starship.responsive)
904
+ return frameRows([renderLegacyContent()]);
905
+
906
+ const fullZones = {
907
+ left: appendStatusArea(
908
+ contentLeft,
909
+ joinStatusTexts(extensionLeftSegments, separator),
910
+ separator,
911
+ ),
912
+ middle: appendStatusArea(
913
+ contentMiddle,
914
+ joinStatusTexts(extensionMiddleSegments, separator),
915
+ separator,
916
+ ),
917
+ right: prependStatusArea(
918
+ contentRight,
919
+ joinStatusTexts(extensionRightSegments, separator),
920
+ separator,
921
+ ),
922
+ };
923
+ if (fullFooterFitsAligned(fullZones, innerWidth)) {
924
+ return frameRows([renderLegacyContent()]);
925
+ }
926
+
927
+ const reflowed = reflowFullFooter(fullZones, innerWidth);
928
+ if (reflowed) return frameRows(reflowed);
929
+
930
+ const chunkBudget = compactChunkBudget(innerWidth);
931
+ const compactCwdLabel = truncateToWidth(
932
+ renderStyleForSource(
933
+ theme,
934
+ colorSource,
935
+ config.colors.cwd,
936
+ sanitizeEditorMetadataText(
937
+ formatCwdLabel(ctx.cwd, config.icons.cwd, {
938
+ mode: "basename",
939
+ depth: 0,
940
+ }),
941
+ ),
942
+ ),
943
+ chunkBudget,
944
+ "…",
945
+ );
946
+ const compactSessionNameLabel = truncateToWidth(
947
+ sessionNameLabel,
948
+ Math.max(1, chunkBudget - visibleWidth("in ")),
949
+ "…",
950
+ );
951
+ const compactBranchBudget = Math.max(
952
+ 1,
953
+ chunkBudget -
954
+ visibleWidth("on ") -
955
+ (statusBlock ? visibleWidth(statusBlock) + 1 : 0),
956
+ );
957
+ const compactBranchLabel = truncateToWidth(
958
+ renderVariable("git_branch"),
959
+ compactBranchBudget,
960
+ "…",
961
+ );
962
+ const renderCompactVariable = (name: string): string => {
963
+ const canonical = FOOTER_FORMAT_ALIASES[name] ?? name;
964
+ switch (canonical) {
965
+ case "cwd":
966
+ return compactCwdLabel;
967
+ case "session_name":
968
+ return compactSessionNameLabel;
969
+ case "git_branch":
970
+ return compactBranchLabel;
971
+ default:
972
+ return renderVariable(name);
973
+ }
974
+ };
975
+ const compactChunks: Array<{
976
+ text: string;
977
+ boundary: "space" | "separator";
978
+ }> = [];
979
+ for (const chunk of compileCompactFormat(compactFormatTokens)) {
980
+ if (chunk.kind === "extensions") {
981
+ const statuses = [
982
+ ...extensionLeftSegments,
983
+ ...extensionMiddleSegments,
984
+ ...extensionRightSegments,
985
+ ];
986
+ for (const [index, text] of statuses.entries()) {
987
+ compactChunks.push({
988
+ text,
989
+ boundary: index === 0 ? chunk.boundary : "space",
990
+ });
991
+ }
992
+ continue;
993
+ }
994
+ let rendered = stripOrphanSeparators(
995
+ renderFormatTokens(chunk.tokens, renderCompactVariable),
996
+ );
997
+ const references = collectFooterFormatReferences(
998
+ chunk.tokens,
999
+ FOOTER_FORMAT_ALIASES,
1000
+ );
1001
+ if (
1002
+ ["cwd", "session_name", "git_branch"].some((name) =>
1003
+ references.has(name),
1004
+ )
1005
+ ) {
1006
+ rendered = truncateToWidth(rendered, chunkBudget, "…");
1007
+ }
1008
+ if (rendered)
1009
+ compactChunks.push({ text: rendered, boundary: chunk.boundary });
1010
+ }
1011
+ return frameRows(
1012
+ packCompactChunks(
1013
+ compactChunks,
1014
+ innerWidth,
1015
+ config.components.footer.styles.starship.compactMaxLines,
1016
+ renderVariable("sep"),
1017
+ ),
1018
+ );
1019
+ },
1020
+ };
1021
+ });
1022
+ }
1023
+
1024
+ export function installHiddenFooter(
1025
+ ctx: ExtensionContext,
1026
+ onDispose?: () => void,
1027
+ ): void {
1028
+ ctx.ui.setFooter(() => ({
1029
+ dispose: onDispose,
1030
+ invalidate() {},
1031
+ render(): string[] {
1032
+ return [];
1033
+ },
1034
+ }));
1035
+ }