@groeponline/pi-wishcraft 0.17.3

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 (106) hide show
  1. package/AGENTS.md +68 -0
  2. package/CHANGELOG.md +724 -0
  3. package/CONTRIBUTING.md +37 -0
  4. package/README.md +648 -0
  5. package/RELEASE.md +117 -0
  6. package/ROADMAP.md +52 -0
  7. package/bash-mode/completion-providers.ts +269 -0
  8. package/bash-mode/completion.ts +416 -0
  9. package/bash-mode/editor-ghost.ts +40 -0
  10. package/bash-mode/editor-input.ts +80 -0
  11. package/bash-mode/editor.ts +437 -0
  12. package/bash-mode/history.ts +263 -0
  13. package/bash-mode/shell-session.ts +286 -0
  14. package/bash-mode/transcript.ts +108 -0
  15. package/bash-mode/types.ts +80 -0
  16. package/index.ts +6 -0
  17. package/package.json +55 -0
  18. package/queue/store.ts +443 -0
  19. package/queue/types.ts +54 -0
  20. package/src/config/custom-items.ts +182 -0
  21. package/src/config/extension-statuses.ts +51 -0
  22. package/src/config/layout.ts +60 -0
  23. package/src/config/parse.ts +127 -0
  24. package/src/config/powerline-config.ts +18 -0
  25. package/src/config/presets.ts +245 -0
  26. package/src/config/primitives.ts +117 -0
  27. package/src/config/segment-ids.ts +114 -0
  28. package/src/config/segment-options.ts +128 -0
  29. package/src/config/settings-patch.ts +26 -0
  30. package/src/config/types.ts +277 -0
  31. package/src/core/frontmatter.ts +40 -0
  32. package/src/editor/autocomplete-chain.ts +41 -0
  33. package/src/extension/activate.ts +28 -0
  34. package/src/extension/bash-mode-actions.ts +104 -0
  35. package/src/extension/commands.ts +268 -0
  36. package/src/extension/constants.ts +46 -0
  37. package/src/extension/custom-editor.ts +406 -0
  38. package/src/extension/git-invalidation.ts +40 -0
  39. package/src/extension/layout.ts +160 -0
  40. package/src/extension/menu-views.ts +393 -0
  41. package/src/extension/powerline-widgets.ts +95 -0
  42. package/src/extension/prompt-history.ts +219 -0
  43. package/src/extension/queue-commands.ts +245 -0
  44. package/src/extension/queue-context.ts +12 -0
  45. package/src/extension/queue-integration.ts +434 -0
  46. package/src/extension/segment-context.ts +212 -0
  47. package/src/extension/session-lifecycle.ts +373 -0
  48. package/src/extension/settings-io.ts +202 -0
  49. package/src/extension/shortcuts-config.ts +357 -0
  50. package/src/extension/shortcuts-router.ts +383 -0
  51. package/src/extension/skills/inline-invocation.ts +174 -0
  52. package/src/extension/skills/ook.md +6 -0
  53. package/src/extension/skills/test.md +6 -0
  54. package/src/extension/stale-context.ts +10 -0
  55. package/src/extension/stash-history.ts +103 -0
  56. package/src/extension/state.ts +159 -0
  57. package/src/extension/status-line-renderers.ts +222 -0
  58. package/src/extension/types.ts +97 -0
  59. package/src/extension/vibe-command.ts +160 -0
  60. package/src/extension/welcome-control.ts +27 -0
  61. package/src/extension/welcome-integration.ts +153 -0
  62. package/src/git/status.ts +332 -0
  63. package/src/paths/agent-dirs.ts +67 -0
  64. package/src/render/timer.ts +46 -0
  65. package/src/segments/core.ts +256 -0
  66. package/src/segments/custom.ts +114 -0
  67. package/src/segments/index.ts +3 -0
  68. package/src/segments/registry.ts +87 -0
  69. package/src/segments/shared.ts +36 -0
  70. package/src/segments/system.ts +235 -0
  71. package/src/segments/usage.ts +178 -0
  72. package/src/shell/cd-command.ts +190 -0
  73. package/src/shortcuts/matching.ts +61 -0
  74. package/src/theme/colors.ts +60 -0
  75. package/src/theme/icons.ts +175 -0
  76. package/src/theme/separators.ts +41 -0
  77. package/src/theme/theme.ts +211 -0
  78. package/src/tools/graph.ts +75 -0
  79. package/src/tools/patch.ts +179 -0
  80. package/src/tools/ripgrep.ts +104 -0
  81. package/src/usage/context.ts +97 -0
  82. package/src/usage/ledger.ts +293 -0
  83. package/src/usage/rates.ts +155 -0
  84. package/src/welcome/auto-dismiss.ts +43 -0
  85. package/src/welcome/banner.ts +68 -0
  86. package/src/welcome/discover.ts +234 -0
  87. package/src/welcome/format.ts +18 -0
  88. package/src/welcome/index.ts +5 -0
  89. package/src/welcome/layout.ts +36 -0
  90. package/src/welcome/overlay.ts +80 -0
  91. package/src/welcome/renderer.ts +157 -0
  92. package/src/welcome/sessions.ts +107 -0
  93. package/src/welcome/types.ts +41 -0
  94. package/src/welcome/widgets/graph-widget.ts +25 -0
  95. package/src/welcome/widgets/index.ts +20 -0
  96. package/src/welcome/widgets/queue-widget.ts +26 -0
  97. package/src/welcome/widgets/sessions-widget.ts +23 -0
  98. package/src/welcome/widgets/shortcuts-widget.ts +17 -0
  99. package/src/welcome/widgets/system-widget.ts +29 -0
  100. package/src/working-vibes/generate.ts +144 -0
  101. package/src/working-vibes/index.ts +24 -0
  102. package/src/working-vibes/manager.ts +198 -0
  103. package/src/working-vibes/provider.ts +163 -0
  104. package/src/working-vibes/storage.ts +357 -0
  105. package/theme.example.json +24 -0
  106. package/tsconfig.json +13 -0
@@ -0,0 +1,406 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ReadonlyFooterDataProvider,
4
+ Theme,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import type { AutocompleteProvider } from "@earendil-works/pi-tui";
7
+
8
+ import { BashModeEditor } from "../../bash-mode/editor.ts";
9
+ import {
10
+ BashAutocompleteProvider,
11
+ ModeAwareAutocompleteProvider,
12
+ OneOffBashAutocompleteProvider,
13
+ } from "../../bash-mode/completion-providers.ts";
14
+ import { getOneOffBashCommandContext } from "../../bash-mode/completion.ts";
15
+ import {
16
+ parseCompactQueuedPrompt,
17
+ parseSigilIdeaCapture,
18
+ } from "../../queue/store.ts";
19
+ import {
20
+ getEditorAutocompleteProvider,
21
+ passAutocompleteProviderThroughPreviousEditor,
22
+ } from "../editor/autocomplete-chain.ts";
23
+ import { subscribeGitUpdates } from "../git/status.ts";
24
+ import { ansi, getFgAnsiCode } from "../theme/colors.ts";
25
+ import {
26
+ restorePromptHistory,
27
+ snapshotPromptHistory,
28
+ trackPromptHistory,
29
+ } from "./prompt-history.ts";
30
+ import {
31
+ captureIdeaFromParsedInput,
32
+ capturePostCompactPrompt,
33
+ isSigilIdeaDraft,
34
+ captureSigilGlyph,
35
+ requestQueueRender,
36
+ finishFailedCompaction,
37
+ } from "./queue-integration.ts";
38
+ import {
39
+ installFooterStatusRepaintHook,
40
+ requestStatusRender,
41
+ } from "./segment-context.ts";
42
+ import { installPowerlineWidgets } from "./powerline-widgets.ts";
43
+ import {
44
+ getPowerlineShortcutAction,
45
+ isStashShortcutInput,
46
+ runPowerlineShortcut,
47
+ stashOrRestoreEditorText,
48
+ } from "./shortcuts-router.ts";
49
+ import {
50
+ dismissWelcome,
51
+ scheduleDismissWelcome,
52
+ } from "./welcome-control.ts";
53
+ import { config } from "./state.ts";
54
+ import type { RuntimeState } from "./types.ts";
55
+ import {
56
+ ensureShellSession,
57
+ getShellCwd,
58
+ getShellHistoryEntries,
59
+ getShellPath,
60
+ runShellCommand,
61
+ setBashModeActive,
62
+ } from "./bash-mode-actions.ts";
63
+
64
+
65
+ export function setupCustomEditor(
66
+ pi: ExtensionAPI,
67
+ rt: RuntimeState,
68
+ ctx: any,
69
+ ) {
70
+ snapshotPromptHistory(rt.currentEditor);
71
+ if (!rt.enabled) {
72
+ return;
73
+ }
74
+
75
+ rt.stashShortcutInputUnsubscribe?.();
76
+ rt.stashShortcutInputUnsubscribe =
77
+ typeof ctx.ui.onTerminalInput === "function"
78
+ ? ctx.ui.onTerminalInput((data: string) => {
79
+ if (!rt.enabled || !ctx.hasUI || rt.tuiRef?.hasOverlay?.()) {
80
+ return undefined;
81
+ }
82
+ if (isStashShortcutInput(data)) {
83
+ stashOrRestoreEditorText(rt, ctx);
84
+ scheduleDismissWelcome(rt, ctx);
85
+ rt.tuiRef?.requestRender();
86
+ return { consume: true };
87
+ }
88
+
89
+ const powerlineShortcutAction = getPowerlineShortcutAction(rt, data);
90
+ if (!powerlineShortcutAction) {
91
+ return undefined;
92
+ }
93
+
94
+ runPowerlineShortcut(pi, rt, ctx, powerlineShortcutAction);
95
+ scheduleDismissWelcome(rt, ctx);
96
+ rt.tuiRef?.requestRender();
97
+ return { consume: true };
98
+ })
99
+ : null;
100
+
101
+ ctx.ui.setWidget("powerline-top", undefined);
102
+ ctx.ui.setWidget("powerline-secondary", undefined);
103
+ ctx.ui.setWidget("powerline-bash-transcript", undefined);
104
+ ctx.ui.setWidget("powerline-status", undefined);
105
+ ctx.ui.setWidget("powerline-queue-preview", undefined);
106
+ ctx.ui.setWidget("powerline-last-prompt", undefined);
107
+
108
+ let autocompleteFixed = false;
109
+ const previousEditorFactory =
110
+ typeof ctx.ui.getEditorComponent === "function"
111
+ ? ctx.ui.getEditorComponent()
112
+ : undefined;
113
+
114
+ const editorFactory = (tui: any, editorTheme: any, keybindings: any) => {
115
+ const previousEditor = previousEditorFactory?.(
116
+ tui,
117
+ editorTheme,
118
+ keybindings,
119
+ );
120
+ const editor = new BashModeEditor(tui, editorTheme, keybindings, {
121
+ keybindings,
122
+ isBashModeActive: () => rt.bashModeActive,
123
+ isShellRunning: () => rt.shellSession?.state.running ?? false,
124
+ onExitBashMode: () => {
125
+ void setBashModeActive(rt, false, ctx);
126
+ },
127
+ onSubmitCommand: (command) => void runShellCommand(rt, command, ctx),
128
+ editorBoundaryShortcuts: {
129
+ start: rt.resolvedShortcuts.editorStart,
130
+ end: rt.resolvedShortcuts.editorEnd,
131
+ },
132
+ onInterrupt: () => {
133
+ rt.shellSession?.interrupt();
134
+ ctx.ui.notify("Sent interrupt to shell", "info");
135
+ },
136
+ onNotify: (message, level = "info") => ctx.ui.notify(message, level),
137
+ getHistoryEntries: (prefix) => getShellHistoryEntries(rt, prefix),
138
+ resolveGhostSuggestion: async (text, signal) => {
139
+ const oneOffBash = getOneOffBashCommandContext(text);
140
+ if (oneOffBash) {
141
+ const ghost = await rt.bashCompletionEngine.getGhostSuggestion(
142
+ oneOffBash.command,
143
+ getShellCwd(rt),
144
+ getShellPath(),
145
+ signal,
146
+ );
147
+ return ghost
148
+ ? { ...ghost, value: `${oneOffBash.prefix}${ghost.value}` }
149
+ : null;
150
+ }
151
+
152
+ return rt.bashCompletionEngine.getGhostSuggestion(
153
+ text,
154
+ getShellCwd(rt),
155
+ getShellPath(),
156
+ signal,
157
+ );
158
+ },
159
+ });
160
+
161
+ let installingPowerlineAutocompleteProvider = false;
162
+ const originalSetAutocompleteProvider =
163
+ editor.setAutocompleteProvider.bind(editor);
164
+ editor.setAutocompleteProvider = (provider: AutocompleteProvider) => {
165
+ if (installingPowerlineAutocompleteProvider) {
166
+ originalSetAutocompleteProvider(provider);
167
+ return;
168
+ }
169
+
170
+ originalSetAutocompleteProvider(
171
+ passAutocompleteProviderThroughPreviousEditor(provider, previousEditor),
172
+ );
173
+ attachAutocompleteProvider();
174
+ };
175
+
176
+ const getInstalledAutocompleteProvider = ():
177
+ AutocompleteProvider | undefined => {
178
+ return (
179
+ getEditorAutocompleteProvider(editor) ??
180
+ getEditorAutocompleteProvider(previousEditor)
181
+ );
182
+ };
183
+
184
+ const attachAutocompleteProvider = (): boolean => {
185
+ if (editor.hasWrappedProvider()) return true;
186
+ const defaultProvider = getInstalledAutocompleteProvider();
187
+ if (!defaultProvider) return false;
188
+
189
+ const bashProvider = new BashAutocompleteProvider();
190
+ const oneOffBashProvider = new OneOffBashAutocompleteProvider();
191
+ installingPowerlineAutocompleteProvider = true;
192
+ try {
193
+ editor.installAutocompleteProvider(
194
+ new ModeAwareAutocompleteProvider(
195
+ defaultProvider,
196
+ bashProvider,
197
+ oneOffBashProvider,
198
+ () => rt.bashModeActive,
199
+ ),
200
+ );
201
+ } finally {
202
+ installingPowerlineAutocompleteProvider = false;
203
+ }
204
+ return true;
205
+ };
206
+
207
+ rt.currentEditor = editor;
208
+ trackPromptHistory(editor);
209
+ restorePromptHistory(editor);
210
+ attachAutocompleteProvider();
211
+
212
+ const originalHandleInput = editor.handleInput.bind(editor);
213
+ editor.handleInput = (data: string) => {
214
+ rt.lastEditorInputAt = Date.now();
215
+
216
+ const isSubmit =
217
+ keybindings.matches(data, "tui.input.submit") &&
218
+ !keybindings.matches(data, "tui.input.newLine");
219
+ const isFollowUpSubmit = keybindings.matches(
220
+ data,
221
+ "app.message.followUp",
222
+ );
223
+ if (!rt.bashModeActive && (isSubmit || isFollowUpSubmit)) {
224
+ const sigilCapture = parseSigilIdeaCapture(
225
+ editor.getExpandedText(),
226
+ config.queue.captureSigil,
227
+ );
228
+ if (sigilCapture) {
229
+ try {
230
+ const item = captureIdeaFromParsedInput(rt, ctx, sigilCapture);
231
+ if (item) {
232
+ editor.addToHistory?.(editor.getExpandedText().trim());
233
+ editor.setText("");
234
+ }
235
+ } catch (error) {
236
+ ctx.ui.notify(
237
+ error instanceof Error ? error.message : String(error),
238
+ "error",
239
+ );
240
+ }
241
+ scheduleDismissWelcome(rt, ctx);
242
+ return;
243
+ }
244
+ }
245
+
246
+ if (
247
+ !rt.powerlineCompacting &&
248
+ !rt.bashModeActive &&
249
+ isSubmit &&
250
+ typeof ctx.compact === "function"
251
+ ) {
252
+ const editorText = editor.getExpandedText().trim();
253
+ const compactQueuedPrompt = parseCompactQueuedPrompt(editorText);
254
+ if (editorText === "/compact" || compactQueuedPrompt) {
255
+ editor.addToHistory?.(editorText);
256
+ editor.setText("");
257
+ if (compactQueuedPrompt) {
258
+ capturePostCompactPrompt(rt, ctx, compactQueuedPrompt);
259
+ }
260
+ rt.powerlineCompacting = true;
261
+ rt.deliverAfterRetrySettles = false;
262
+ requestQueueRender(rt);
263
+ ctx.compact({
264
+ onError: (error: Error) => {
265
+ finishFailedCompaction(rt, ctx, error.message);
266
+ ctx.ui.notify(error.message, "error");
267
+ },
268
+ });
269
+ scheduleDismissWelcome(rt, ctx);
270
+ return;
271
+ }
272
+ }
273
+
274
+ if (
275
+ rt.powerlineCompacting &&
276
+ !rt.bashModeActive &&
277
+ (isSubmit || isFollowUpSubmit)
278
+ ) {
279
+ const text = editor.getExpandedText().trim();
280
+ if (!text) return;
281
+ if (text.startsWith("/")) {
282
+ originalHandleInput(data);
283
+ return;
284
+ }
285
+ editor.addToHistory?.(text);
286
+ editor.setText("");
287
+ capturePostCompactPrompt(rt, ctx, text);
288
+ scheduleDismissWelcome(rt, ctx);
289
+ return;
290
+ }
291
+
292
+ if (isStashShortcutInput(data)) {
293
+ stashOrRestoreEditorText(rt, ctx);
294
+ scheduleDismissWelcome(rt, ctx);
295
+ return;
296
+ }
297
+
298
+ const powerlineShortcutAction = getPowerlineShortcutAction(rt, data);
299
+ if (powerlineShortcutAction) {
300
+ runPowerlineShortcut(pi, rt, ctx, powerlineShortcutAction);
301
+ scheduleDismissWelcome(rt, ctx);
302
+ return;
303
+ }
304
+
305
+ if (!autocompleteFixed && !getInstalledAutocompleteProvider()) {
306
+ autocompleteFixed = true;
307
+ snapshotPromptHistory(editor);
308
+ ctx.ui.setEditorComponent(editorFactory);
309
+ rt.currentEditor?.handleInput(data);
310
+ return;
311
+ }
312
+
313
+ attachAutocompleteProvider();
314
+ scheduleDismissWelcome(rt, ctx);
315
+ originalHandleInput(data);
316
+ };
317
+
318
+ const originalRender = editor.render.bind(editor);
319
+ editor.render = (width: number): string[] => {
320
+ if (width < 10) {
321
+ return originalRender(width);
322
+ }
323
+
324
+ const bc = (s: string) => `${getFgAnsiCode("sep")}${s}${ansi.reset}`;
325
+ const captureDraft =
326
+ !rt.bashModeActive && isSigilIdeaDraft(editor.getExpandedText());
327
+ const promptGlyph = rt.bashModeActive
328
+ ? "$"
329
+ : captureDraft
330
+ ? captureSigilGlyph()
331
+ : ">";
332
+ const promptColor = captureDraft
333
+ ? getFgAnsiCode("queue")
334
+ : ansi.getFgAnsi(200, 200, 200);
335
+ const prompt = `${promptColor}${promptGlyph}${ansi.reset}`;
336
+ const promptPrefix = ` ${prompt} `;
337
+ const contPrefix = " ";
338
+ const contentWidth = Math.max(1, width - 3);
339
+ const lines = originalRender(contentWidth);
340
+
341
+ if (lines.length === 0) return lines;
342
+
343
+ let bottomBorderIndex = lines.length - 1;
344
+ for (let i = lines.length - 1; i >= 1; i--) {
345
+ const stripped = lines[i]?.replace(/\x1b\[[0-9;]*m/g, "") || "";
346
+ if (stripped.length > 0 && /^─{3,}/.test(stripped)) {
347
+ bottomBorderIndex = i;
348
+ break;
349
+ }
350
+ }
351
+
352
+ const result: string[] = [];
353
+ result.push(" " + bc("─".repeat(width - 2)));
354
+
355
+ for (let i = 1; i < bottomBorderIndex; i++) {
356
+ const prefix = i === 1 ? promptPrefix : contPrefix;
357
+ result.push(`${prefix}${lines[i] || ""}`);
358
+ }
359
+
360
+ if (bottomBorderIndex === 1) {
361
+ result.push(`${promptPrefix}${" ".repeat(contentWidth)}`);
362
+ }
363
+
364
+ result.push(" " + bc("─".repeat(width - 2)));
365
+
366
+ for (let i = bottomBorderIndex + 1; i < lines.length; i++) {
367
+ result.push(lines[i] || "");
368
+ }
369
+
370
+ return result;
371
+ };
372
+
373
+ return editor;
374
+ };
375
+
376
+ ctx.ui.setEditorComponent(editorFactory);
377
+
378
+ ctx.ui.setFooter(
379
+ (tui: any, _theme: Theme, footerData: ReadonlyFooterDataProvider) => {
380
+ rt.footerDataRef = footerData;
381
+ rt.tuiRef = tui;
382
+ installFooterStatusRepaintHook(rt, footerData);
383
+ const unsub = footerData.onBranchChange(() => requestStatusRender(rt));
384
+ const unsubGitUpdates = subscribeGitUpdates(() =>
385
+ requestStatusRender(rt),
386
+ );
387
+
388
+ return {
389
+ dispose() {
390
+ unsub();
391
+ unsubGitUpdates();
392
+ rt.restoreFooterStatusRepaintHook?.();
393
+ rt.restoreFooterStatusRepaintHook = null;
394
+ },
395
+ invalidate() {
396
+ requestStatusRender(rt);
397
+ },
398
+ render(): string[] {
399
+ return [""];
400
+ },
401
+ };
402
+ },
403
+ );
404
+
405
+ installPowerlineWidgets(rt, ctx);
406
+ }
@@ -0,0 +1,40 @@
1
+ import { invalidateGitBranch, invalidateGitStatus } from "../git/status.ts";
2
+ import { requestStatusRender } from "./segment-context.ts";
3
+ import type { RuntimeState } from "./types.ts";
4
+
5
+ // Check if a bash command might change git branch
6
+ export function mightChangeGitBranch(cmd: string): boolean {
7
+ const gitBranchPatterns = [
8
+ /\bgit\s+(checkout|switch|branch\s+-[dDmM]|merge|rebase|pull|reset|worktree)/,
9
+ /\bgit\s+stash\s+(pop|apply)/,
10
+ ];
11
+ return gitBranchPatterns.some((p) => p.test(cmd));
12
+ }
13
+
14
+ /**
15
+ * Invalidate cached git state and schedule a status re-render when a command
16
+ * may have changed the working tree or branch. Returns true when the command
17
+ * matched a git-changing pattern.
18
+ */
19
+ export function invalidateGitForCommand(
20
+ rt: RuntimeState,
21
+ cmd: string,
22
+ options: { stagger?: boolean } = {},
23
+ ): boolean {
24
+ if (!mightChangeGitBranch(cmd)) {
25
+ return false;
26
+ }
27
+ invalidateGitStatus();
28
+ invalidateGitBranch();
29
+ if (options.stagger) {
30
+ // user_bash fires BEFORE execution, so use staggered re-renders to catch
31
+ // both fast and slow commands after they complete.
32
+ setTimeout(() => requestStatusRender(rt), 100);
33
+ setTimeout(() => requestStatusRender(rt), 300);
34
+ setTimeout(() => requestStatusRender(rt), 500);
35
+ } else {
36
+ // The command has completed, so start refreshing immediately.
37
+ requestStatusRender(rt);
38
+ }
39
+ return true;
40
+ }
@@ -0,0 +1,160 @@
1
+ import { visibleWidth } from "@earendil-works/pi-tui";
2
+
3
+ import type {
4
+ SegmentContext,
5
+ StatusLineSegmentId,
6
+ StatusLineSeparatorStyle,
7
+ } from "../config/types.ts";
8
+ import { getPreset } from "../config/presets.ts";
9
+ import { mergeSegmentsWithCustomItems } from "../config/powerline-config.ts";
10
+ import { getSeparator } from "../theme/separators.ts";
11
+ import { renderSegment } from "../segments/index.ts";
12
+ import { ansi, getFgAnsiCode } from "../theme/colors.ts";
13
+ import { config } from "./state.ts";
14
+
15
+ // ═══════════════════════════════════════════════════════════════════════════
16
+ // Status Line Builder
17
+ // ═══════════════════════════════════════════════════════════════════════════
18
+
19
+ /** Render a single segment and return its content with width */
20
+ export function renderSegmentWithWidth(
21
+ segId: StatusLineSegmentId,
22
+ ctx: SegmentContext,
23
+ ): { content: string; width: number; visible: boolean } {
24
+ const rendered = renderSegment(segId, ctx);
25
+ if (!rendered.visible || !rendered.content) {
26
+ return { content: "", width: 0, visible: false };
27
+ }
28
+ return {
29
+ content: rendered.content,
30
+ width: visibleWidth(rendered.content),
31
+ visible: true,
32
+ };
33
+ }
34
+
35
+ /** Build content string from pre-rendered parts */
36
+ export function buildContentFromParts(
37
+ parts: string[],
38
+ separatorStyle: StatusLineSeparatorStyle,
39
+ ): string {
40
+ if (parts.length === 0) return "";
41
+ const separatorDef = getSeparator(separatorStyle);
42
+ const sepAnsi = getFgAnsiCode("sep");
43
+ const sep = separatorDef.left;
44
+ return " " + parts.join(` ${sepAnsi}${sep}${ansi.reset} `) + ansi.reset + " ";
45
+ }
46
+
47
+ /**
48
+ * Responsive segment layout - fits segments into top bar, overflows to secondary row.
49
+ * When terminal is wide enough, secondary segments move up to top bar.
50
+ * When narrow, top bar segments overflow down to secondary row.
51
+ */
52
+ export function computeResponsiveLayout(
53
+ ctx: SegmentContext,
54
+ presetDef: ReturnType<typeof getPreset>,
55
+ availableWidth: number,
56
+ ): { topContent: string; secondaryContent: string } {
57
+ const separatorStyle = config.separator ?? presetDef.separator;
58
+ const separatorDef = getSeparator(separatorStyle);
59
+ const sepWidth = visibleWidth(separatorDef.left) + 2; // separator + spaces around it
60
+
61
+ // Get all segments: primary first, then secondary
62
+ const mergedSegments = mergeSegmentsWithCustomItems(
63
+ presetDef,
64
+ config.customItems,
65
+ {
66
+ layout: config.layout,
67
+ disabledSegments: config.disabledSegments,
68
+ },
69
+ );
70
+ const primaryIds = [
71
+ ...mergedSegments.leftSegments,
72
+ ...mergedSegments.rightSegments,
73
+ ];
74
+ const secondaryIds = mergedSegments.secondarySegments;
75
+ const allSegmentIds = [...primaryIds, ...secondaryIds];
76
+
77
+ // Render all segments and get their widths
78
+ const renderedSegments: { content: string; width: number }[] = [];
79
+ for (const segId of allSegmentIds) {
80
+ const { content, width, visible } = renderSegmentWithWidth(segId, ctx);
81
+ if (visible) {
82
+ renderedSegments.push({ content, width });
83
+ }
84
+ }
85
+
86
+ if (renderedSegments.length === 0) {
87
+ return { topContent: "", secondaryContent: "" };
88
+ }
89
+
90
+ // Calculate how many segments fit in top bar
91
+ // Account for: leading space (1) + trailing space (1) = 2 chars overhead
92
+ const baseOverhead = 2;
93
+ let currentWidth = baseOverhead;
94
+ let topSegments: string[] = [];
95
+ let overflowSegments: { content: string; width: number }[] = [];
96
+ let overflow = false;
97
+
98
+ for (const seg of renderedSegments) {
99
+ const neededWidth = seg.width + (topSegments.length > 0 ? sepWidth : 0);
100
+
101
+ if (!overflow && currentWidth + neededWidth <= availableWidth) {
102
+ topSegments.push(seg.content);
103
+ currentWidth += neededWidth;
104
+ } else {
105
+ overflow = true;
106
+ overflowSegments.push(seg);
107
+ }
108
+ }
109
+
110
+ // Fit overflow segments into secondary row (same width constraint)
111
+ // Stop at first non-fitting segment to preserve ordering
112
+ let secondaryWidth = baseOverhead;
113
+ let secondarySegments: string[] = [];
114
+
115
+ for (const seg of overflowSegments) {
116
+ const neededWidth =
117
+ seg.width + (secondarySegments.length > 0 ? sepWidth : 0);
118
+ if (secondaryWidth + neededWidth <= availableWidth) {
119
+ secondarySegments.push(seg.content);
120
+ secondaryWidth += neededWidth;
121
+ } else {
122
+ break;
123
+ }
124
+ }
125
+
126
+ return {
127
+ topContent: buildContentFromParts(topSegments, separatorStyle),
128
+ secondaryContent: buildContentFromParts(secondarySegments, separatorStyle),
129
+ };
130
+ }
131
+
132
+ // ═══════════════════════════════════════════════════════════════════════════
133
+ // Extension
134
+ // ═══════════════════════════════════════════════════════════════════════════
135
+
136
+ export function warnInvalidSegmentSettings(ctx: any): void {
137
+ if (config.invalidDisabledSegments.length > 0) {
138
+ const invalid = config.invalidDisabledSegments
139
+ .map((id) => JSON.stringify(id))
140
+ .join(", ");
141
+ const message = `Ignoring unknown powerline disabled segment${config.invalidDisabledSegments.length === 1 ? "" : "s"}: ${invalid}`;
142
+ console.warn(`[powerline-footer] ${message}`);
143
+ if (ctx.hasUI) ctx.ui.notify(message, "warning");
144
+ }
145
+
146
+ if (config.invalidLayoutSegments.length > 0) {
147
+ const invalid = config.invalidLayoutSegments
148
+ .map((id) => JSON.stringify(id))
149
+ .join(", ");
150
+ const message = `Ignoring unknown powerline layout segment${config.invalidLayoutSegments.length === 1 ? "" : "s"}: ${invalid}`;
151
+ console.warn(`[powerline-footer] ${message}`);
152
+ if (ctx.hasUI) ctx.ui.notify(message, "warning");
153
+ }
154
+
155
+ if (config.invalidPlacement !== null) {
156
+ const message = `Ignoring invalid powerline placement: ${JSON.stringify(config.invalidPlacement)}`;
157
+ console.warn(`[powerline-footer] ${message}`);
158
+ if (ctx.hasUI) ctx.ui.notify(message, "warning");
159
+ }
160
+ }