@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.
- package/AGENTS.md +68 -0
- package/CHANGELOG.md +724 -0
- package/CONTRIBUTING.md +37 -0
- package/README.md +648 -0
- package/RELEASE.md +117 -0
- package/ROADMAP.md +52 -0
- package/bash-mode/completion-providers.ts +269 -0
- package/bash-mode/completion.ts +416 -0
- package/bash-mode/editor-ghost.ts +40 -0
- package/bash-mode/editor-input.ts +80 -0
- package/bash-mode/editor.ts +437 -0
- package/bash-mode/history.ts +263 -0
- package/bash-mode/shell-session.ts +286 -0
- package/bash-mode/transcript.ts +108 -0
- package/bash-mode/types.ts +80 -0
- package/index.ts +6 -0
- package/package.json +55 -0
- package/queue/store.ts +443 -0
- package/queue/types.ts +54 -0
- package/src/config/custom-items.ts +182 -0
- package/src/config/extension-statuses.ts +51 -0
- package/src/config/layout.ts +60 -0
- package/src/config/parse.ts +127 -0
- package/src/config/powerline-config.ts +18 -0
- package/src/config/presets.ts +245 -0
- package/src/config/primitives.ts +117 -0
- package/src/config/segment-ids.ts +114 -0
- package/src/config/segment-options.ts +128 -0
- package/src/config/settings-patch.ts +26 -0
- package/src/config/types.ts +277 -0
- package/src/core/frontmatter.ts +40 -0
- package/src/editor/autocomplete-chain.ts +41 -0
- package/src/extension/activate.ts +28 -0
- package/src/extension/bash-mode-actions.ts +104 -0
- package/src/extension/commands.ts +268 -0
- package/src/extension/constants.ts +46 -0
- package/src/extension/custom-editor.ts +406 -0
- package/src/extension/git-invalidation.ts +40 -0
- package/src/extension/layout.ts +160 -0
- package/src/extension/menu-views.ts +393 -0
- package/src/extension/powerline-widgets.ts +95 -0
- package/src/extension/prompt-history.ts +219 -0
- package/src/extension/queue-commands.ts +245 -0
- package/src/extension/queue-context.ts +12 -0
- package/src/extension/queue-integration.ts +434 -0
- package/src/extension/segment-context.ts +212 -0
- package/src/extension/session-lifecycle.ts +373 -0
- package/src/extension/settings-io.ts +202 -0
- package/src/extension/shortcuts-config.ts +357 -0
- package/src/extension/shortcuts-router.ts +383 -0
- package/src/extension/skills/inline-invocation.ts +174 -0
- package/src/extension/skills/ook.md +6 -0
- package/src/extension/skills/test.md +6 -0
- package/src/extension/stale-context.ts +10 -0
- package/src/extension/stash-history.ts +103 -0
- package/src/extension/state.ts +159 -0
- package/src/extension/status-line-renderers.ts +222 -0
- package/src/extension/types.ts +97 -0
- package/src/extension/vibe-command.ts +160 -0
- package/src/extension/welcome-control.ts +27 -0
- package/src/extension/welcome-integration.ts +153 -0
- package/src/git/status.ts +332 -0
- package/src/paths/agent-dirs.ts +67 -0
- package/src/render/timer.ts +46 -0
- package/src/segments/core.ts +256 -0
- package/src/segments/custom.ts +114 -0
- package/src/segments/index.ts +3 -0
- package/src/segments/registry.ts +87 -0
- package/src/segments/shared.ts +36 -0
- package/src/segments/system.ts +235 -0
- package/src/segments/usage.ts +178 -0
- package/src/shell/cd-command.ts +190 -0
- package/src/shortcuts/matching.ts +61 -0
- package/src/theme/colors.ts +60 -0
- package/src/theme/icons.ts +175 -0
- package/src/theme/separators.ts +41 -0
- package/src/theme/theme.ts +211 -0
- package/src/tools/graph.ts +75 -0
- package/src/tools/patch.ts +179 -0
- package/src/tools/ripgrep.ts +104 -0
- package/src/usage/context.ts +97 -0
- package/src/usage/ledger.ts +293 -0
- package/src/usage/rates.ts +155 -0
- package/src/welcome/auto-dismiss.ts +43 -0
- package/src/welcome/banner.ts +68 -0
- package/src/welcome/discover.ts +234 -0
- package/src/welcome/format.ts +18 -0
- package/src/welcome/index.ts +5 -0
- package/src/welcome/layout.ts +36 -0
- package/src/welcome/overlay.ts +80 -0
- package/src/welcome/renderer.ts +157 -0
- package/src/welcome/sessions.ts +107 -0
- package/src/welcome/types.ts +41 -0
- package/src/welcome/widgets/graph-widget.ts +25 -0
- package/src/welcome/widgets/index.ts +20 -0
- package/src/welcome/widgets/queue-widget.ts +26 -0
- package/src/welcome/widgets/sessions-widget.ts +23 -0
- package/src/welcome/widgets/shortcuts-widget.ts +17 -0
- package/src/welcome/widgets/system-widget.ts +29 -0
- package/src/working-vibes/generate.ts +144 -0
- package/src/working-vibes/index.ts +24 -0
- package/src/working-vibes/manager.ts +198 -0
- package/src/working-vibes/provider.ts +163 -0
- package/src/working-vibes/storage.ts +357 -0
- package/theme.example.json +24 -0
- 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
|
+
}
|