@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,393 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
type SelectItem,
|
|
4
|
+
SelectList,
|
|
5
|
+
truncateToWidth,
|
|
6
|
+
} from "@earendil-works/pi-tui";
|
|
7
|
+
import { execSync } from "node:child_process";
|
|
8
|
+
|
|
9
|
+
import type { StatusLinePreset } from "../config/types.ts";
|
|
10
|
+
import { getPreset, PRESETS } from "../config/presets.ts";
|
|
11
|
+
import { mergeSegmentsWithCustomItems } from "../config/powerline-config.ts";
|
|
12
|
+
import { writePowerlinePresetSetting } from "./settings-io.ts";
|
|
13
|
+
import { renderSegmentWithWidth } from "./layout.ts";
|
|
14
|
+
import {
|
|
15
|
+
buildSegmentContext,
|
|
16
|
+
requestImmediateStatusRender,
|
|
17
|
+
} from "./segment-context.ts";
|
|
18
|
+
import { config, setConfig } from "./state.ts";
|
|
19
|
+
import type { RuntimeState } from "./types.ts";
|
|
20
|
+
|
|
21
|
+
export function overlaySelectListTheme(theme: Theme) {
|
|
22
|
+
return {
|
|
23
|
+
selectedPrefix: (text: string) => theme.fg("accent", text),
|
|
24
|
+
selectedText: (text: string) => theme.fg("accent", text),
|
|
25
|
+
description: (text: string) => theme.fg("muted", text),
|
|
26
|
+
scrollInfo: (text: string) => theme.fg("dim", text),
|
|
27
|
+
noMatch: (text: string) => theme.fg("warning", text),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function showSelectOverlay(
|
|
32
|
+
ctx: any,
|
|
33
|
+
title: string,
|
|
34
|
+
hint: string,
|
|
35
|
+
items: SelectItem[],
|
|
36
|
+
maxVisible: number,
|
|
37
|
+
): Promise<SelectItem | null> {
|
|
38
|
+
return ctx.ui.custom(
|
|
39
|
+
(
|
|
40
|
+
tui: any,
|
|
41
|
+
theme: Theme,
|
|
42
|
+
_keybindings: any,
|
|
43
|
+
done: (result: SelectItem | null) => void,
|
|
44
|
+
) => {
|
|
45
|
+
const selectList = new SelectList(
|
|
46
|
+
items,
|
|
47
|
+
maxVisible,
|
|
48
|
+
overlaySelectListTheme(theme),
|
|
49
|
+
);
|
|
50
|
+
const border = (text: string) => theme.fg("dim", text);
|
|
51
|
+
const wrapRow = (text: string, innerWidth: number): string => {
|
|
52
|
+
return `${border("│")}${truncateToWidth(text, innerWidth, "…", true)}${border("│")}`;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
selectList.onSelect = (item) => done(item);
|
|
56
|
+
selectList.onCancel = () => done(null);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
render: (width: number) => {
|
|
60
|
+
const innerWidth = Math.max(1, width - 2);
|
|
61
|
+
const lines: string[] = [];
|
|
62
|
+
|
|
63
|
+
lines.push(border(`╭${"─".repeat(innerWidth)}╮`));
|
|
64
|
+
lines.push(
|
|
65
|
+
wrapRow(theme.fg("accent", theme.bold(title)), innerWidth),
|
|
66
|
+
);
|
|
67
|
+
lines.push(border(`├${"─".repeat(innerWidth)}┤`));
|
|
68
|
+
|
|
69
|
+
for (const line of selectList.render(innerWidth)) {
|
|
70
|
+
lines.push(wrapRow(line, innerWidth));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
lines.push(border(`├${"─".repeat(innerWidth)}┤`));
|
|
74
|
+
lines.push(wrapRow(theme.fg("dim", hint), innerWidth));
|
|
75
|
+
lines.push(border(`╰${"─".repeat(innerWidth)}╯`));
|
|
76
|
+
|
|
77
|
+
return lines;
|
|
78
|
+
},
|
|
79
|
+
invalidate: () => selectList.invalidate(),
|
|
80
|
+
handleInput: (data: string) => {
|
|
81
|
+
selectList.handleInput(data);
|
|
82
|
+
tui.requestRender();
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
overlay: true,
|
|
88
|
+
overlayOptions: () => ({
|
|
89
|
+
verticalAlign: "center",
|
|
90
|
+
horizontalAlign: "center",
|
|
91
|
+
}),
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Full open-ports list as a scrollable overlay (the Info view). */
|
|
97
|
+
export async function showOpenPortsList(ctx: any): Promise<void> {
|
|
98
|
+
try {
|
|
99
|
+
const includeUdp = config.segmentOptions?.openPorts?.includeUdp === true;
|
|
100
|
+
const proto = includeUdp ? "-tuln" : "-tln";
|
|
101
|
+
const stdout = execSync(`ss ${proto} 2>/dev/null`, { encoding: "utf8" });
|
|
102
|
+
const lines = stdout
|
|
103
|
+
.split("\n")
|
|
104
|
+
.map((l) => l.trim())
|
|
105
|
+
.filter(Boolean);
|
|
106
|
+
const start = /^(Proto|Netid|State|Local)/.test(lines[0] ?? "") ? 1 : 0;
|
|
107
|
+
const rows = lines.slice(start);
|
|
108
|
+
if (rows.length === 0) {
|
|
109
|
+
ctx.ui.notify("No listening sockets", "info");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const items: SelectItem[] = rows.map((line) => ({
|
|
113
|
+
label: line,
|
|
114
|
+
value: line,
|
|
115
|
+
}));
|
|
116
|
+
const picked = await showSelectOverlay(
|
|
117
|
+
ctx,
|
|
118
|
+
"Open ports",
|
|
119
|
+
"↑↓ navigate · enter copy · esc close",
|
|
120
|
+
items,
|
|
121
|
+
Math.min(items.length, 24),
|
|
122
|
+
);
|
|
123
|
+
if (picked) ctx.ui.notify(`Port: ${picked.value}`, "info");
|
|
124
|
+
} catch (error) {
|
|
125
|
+
ctx.ui.notify(
|
|
126
|
+
`Failed to list ports: ${error instanceof Error ? error.message : String(error)}`,
|
|
127
|
+
"error",
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Configure sub-menu: preset, TPS value/label, ports UDP, segment labels. */
|
|
133
|
+
export async function configurePowerline(
|
|
134
|
+
rt: RuntimeState,
|
|
135
|
+
ctx: any,
|
|
136
|
+
): Promise<void> {
|
|
137
|
+
const choice = await ctx.ui.select("Powerline · configure", [
|
|
138
|
+
"Change preset",
|
|
139
|
+
"Set TPS value (POWERLINE_TPS)",
|
|
140
|
+
"Clear TPS override (use live)",
|
|
141
|
+
"Toggle UDP in open-ports",
|
|
142
|
+
"Set segment label…",
|
|
143
|
+
"Show current config",
|
|
144
|
+
]);
|
|
145
|
+
if (!choice) return;
|
|
146
|
+
if (choice === "Change preset") {
|
|
147
|
+
const names = Object.keys(PRESETS) as StatusLinePreset[];
|
|
148
|
+
const picked = await ctx.ui.select("Preset", names);
|
|
149
|
+
if (picked) {
|
|
150
|
+
setConfig({ ...config, preset: picked as StatusLinePreset });
|
|
151
|
+
writePowerlinePresetSetting(
|
|
152
|
+
picked as StatusLinePreset,
|
|
153
|
+
ctx.cwd ?? process.cwd(),
|
|
154
|
+
);
|
|
155
|
+
ctx.ui.notify(`Preset: ${picked} (saved)`, "info");
|
|
156
|
+
requestImmediateStatusRender(rt, { deferDuringTyping: false });
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (choice === "Set TPS value (POWERLINE_TPS)") {
|
|
161
|
+
const val = await ctx.ui.input(
|
|
162
|
+
"TPS value",
|
|
163
|
+
process.env.POWERLINE_TPS || "",
|
|
164
|
+
);
|
|
165
|
+
if (val && val.trim()) {
|
|
166
|
+
process.env.POWERLINE_TPS = val.trim();
|
|
167
|
+
ctx.ui.notify(`TPS override set: ${val.trim()}`, "info");
|
|
168
|
+
requestImmediateStatusRender(rt, { deferDuringTyping: false });
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (choice === "Clear TPS override (use live)") {
|
|
173
|
+
delete process.env.POWERLINE_TPS;
|
|
174
|
+
ctx.ui.notify("TPS override cleared (live rate)", "info");
|
|
175
|
+
requestImmediateStatusRender(rt, { deferDuringTyping: false });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (choice === "Toggle UDP in open-ports") {
|
|
179
|
+
const now = config.segmentOptions?.openPorts?.includeUdp === true;
|
|
180
|
+
setConfig({
|
|
181
|
+
...config,
|
|
182
|
+
segmentOptions: {
|
|
183
|
+
...config.segmentOptions,
|
|
184
|
+
openPorts: { includeUdp: !now },
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
ctx.ui.notify(`Open-ports UDP: ${!now ? "on" : "off"}`, "info");
|
|
188
|
+
requestImmediateStatusRender(rt, { deferDuringTyping: false });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (choice === "Set segment label…") {
|
|
192
|
+
const id = await ctx.ui.input("Segment id", "tps");
|
|
193
|
+
if (!id) return;
|
|
194
|
+
const label = await ctx.ui.input(
|
|
195
|
+
"Label text",
|
|
196
|
+
config.segmentLabels[id] ?? "",
|
|
197
|
+
);
|
|
198
|
+
if (label === undefined) return;
|
|
199
|
+
const labels = { ...config.segmentLabels };
|
|
200
|
+
if (label.trim()) labels[id] = label.trim();
|
|
201
|
+
else delete labels[id];
|
|
202
|
+
setConfig({ ...config, segmentLabels: labels });
|
|
203
|
+
ctx.ui.notify(`Label ${id}: ${label.trim() || "(cleared)"}`, "info");
|
|
204
|
+
requestImmediateStatusRender(rt, { deferDuringTyping: false });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (choice === "Show current config") {
|
|
208
|
+
const summary = [
|
|
209
|
+
`preset: ${config.preset}`,
|
|
210
|
+
`separator: ${config.separator ?? "(preset default)"}`,
|
|
211
|
+
`tps: ${process.env.POWERLINE_TPS || "(live)"}`,
|
|
212
|
+
`open_ports UDP: ${config.segmentOptions?.openPorts?.includeUdp ? "on" : "off"}`,
|
|
213
|
+
`disabled: ${config.disabledSegments.join(",") || "(none)"}`,
|
|
214
|
+
`labels: ${Object.keys(config.segmentLabels).join(",") || "(none)"}`,
|
|
215
|
+
].join(" · ");
|
|
216
|
+
ctx.ui.notify(summary, "info");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Activate a segment picked from the navigator (Enter). */
|
|
222
|
+
export async function activateSegment(
|
|
223
|
+
rt: RuntimeState,
|
|
224
|
+
ctx: any,
|
|
225
|
+
picked: { id: string; label: string },
|
|
226
|
+
): Promise<void> {
|
|
227
|
+
const id = picked.id;
|
|
228
|
+
if (id === "__none__") return;
|
|
229
|
+
if (id === "tps") {
|
|
230
|
+
const current = process.env.POWERLINE_TPS || "(live, auto)";
|
|
231
|
+
ctx.ui.notify(
|
|
232
|
+
`TPS: ${current} — set via Configure or /tps <value>`,
|
|
233
|
+
"info",
|
|
234
|
+
);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (id === "open_ports") {
|
|
238
|
+
await showOpenPortsList(ctx);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (id === "git") {
|
|
242
|
+
ctx.ui.notify(
|
|
243
|
+
`git branch: ${rt.footerDataRef?.getGitBranch() ?? "(no repo)"}`,
|
|
244
|
+
"info",
|
|
245
|
+
);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (id === "cost") {
|
|
249
|
+
ctx.ui.notify("Use /cost for the cost breakdown", "info");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (id === "context_pct" || id === "context_total") {
|
|
253
|
+
ctx.ui.notify("Context window shown in the bar", "info");
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (id === "queue") {
|
|
257
|
+
ctx.ui.notify("Use /ideas to work the queue", "info");
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const value = picked.label.replace(/^\S+\s+/, "");
|
|
261
|
+
ctx.ui.notify(`${id}: ${value}`, "info");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Main powerline menu: navigate / configure / info / toggle. */
|
|
265
|
+
export async function showPowerlineMainMenu(
|
|
266
|
+
rt: RuntimeState,
|
|
267
|
+
ctx: any,
|
|
268
|
+
): Promise<void> {
|
|
269
|
+
rt.currentCtx = ctx;
|
|
270
|
+
const choice = await ctx.ui.select("Powerline", [
|
|
271
|
+
"Navigate segments",
|
|
272
|
+
"Configure…",
|
|
273
|
+
"Open ports (full list)",
|
|
274
|
+
"TPS detail",
|
|
275
|
+
"Toggle powerline",
|
|
276
|
+
]);
|
|
277
|
+
if (!choice) return;
|
|
278
|
+
if (choice === "Navigate segments") {
|
|
279
|
+
const picked = await showSegmentNavigator(rt, ctx);
|
|
280
|
+
if (picked) await activateSegment(rt, ctx, picked);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (choice === "Configure…") {
|
|
284
|
+
await configurePowerline(rt, ctx);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (choice === "Open ports (full list)") {
|
|
288
|
+
await showOpenPortsList(ctx);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (choice === "TPS detail") {
|
|
292
|
+
const live = process.env.POWERLINE_TPS
|
|
293
|
+
? `(override ${process.env.POWERLINE_TPS})`
|
|
294
|
+
: "(live 1s window)";
|
|
295
|
+
ctx.ui.notify(`TPS ${live}`, "info");
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (choice === "Toggle powerline") {
|
|
299
|
+
ctx.ui.notify("Use /powerline to toggle", "info");
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Navigable overlay that mirrors the live powerline segments; arrow keys move, Enter activates. */
|
|
305
|
+
export async function showSegmentNavigator(
|
|
306
|
+
rt: RuntimeState,
|
|
307
|
+
ctx: any,
|
|
308
|
+
): Promise<{ id: string; label: string } | null> {
|
|
309
|
+
return ctx.ui.custom(
|
|
310
|
+
(
|
|
311
|
+
tui: any,
|
|
312
|
+
theme: Theme,
|
|
313
|
+
_keybindings: any,
|
|
314
|
+
done: (result: { id: string; label: string } | null) => void,
|
|
315
|
+
) => {
|
|
316
|
+
// Build live segment values now that we have a theme
|
|
317
|
+
const segCtx = buildSegmentContext(rt, ctx, theme);
|
|
318
|
+
const presetDef = getPreset(config.preset);
|
|
319
|
+
const merged = mergeSegmentsWithCustomItems(
|
|
320
|
+
presetDef,
|
|
321
|
+
config.customItems,
|
|
322
|
+
{
|
|
323
|
+
layout: config.layout,
|
|
324
|
+
disabledSegments: config.disabledSegments,
|
|
325
|
+
},
|
|
326
|
+
);
|
|
327
|
+
const ids = [
|
|
328
|
+
...merged.leftSegments,
|
|
329
|
+
...merged.rightSegments,
|
|
330
|
+
...merged.secondarySegments,
|
|
331
|
+
];
|
|
332
|
+
const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
333
|
+
const items: SelectItem[] = [];
|
|
334
|
+
for (const id of ids) {
|
|
335
|
+
const rendered = renderSegmentWithWidth(id, segCtx);
|
|
336
|
+
if (!rendered.visible) continue;
|
|
337
|
+
const value = stripAnsi(rendered.content).trim() || "(empty)";
|
|
338
|
+
items.push({ label: `${id} ${value}`, value: id });
|
|
339
|
+
}
|
|
340
|
+
if (items.length === 0) {
|
|
341
|
+
items.push({ label: "(no visible segments)", value: "__none__" });
|
|
342
|
+
}
|
|
343
|
+
const selectList = new SelectList(
|
|
344
|
+
items,
|
|
345
|
+
Math.min(items.length, 20),
|
|
346
|
+
overlaySelectListTheme(theme),
|
|
347
|
+
);
|
|
348
|
+
const border = (text: string) => theme.fg("dim", text);
|
|
349
|
+
const wrapRow = (text: string, innerWidth: number) =>
|
|
350
|
+
`${border("│")}${truncateToWidth(text, innerWidth, "…", true)}${border("│")}`;
|
|
351
|
+
selectList.onSelect = (item) =>
|
|
352
|
+
done({ id: item.value, label: item.label });
|
|
353
|
+
selectList.onCancel = () => done(null);
|
|
354
|
+
return {
|
|
355
|
+
render: (width: number) => {
|
|
356
|
+
const innerWidth = Math.max(1, width - 2);
|
|
357
|
+
const lines: string[] = [];
|
|
358
|
+
lines.push(border(`╭${"─".repeat(innerWidth)}╮`));
|
|
359
|
+
lines.push(
|
|
360
|
+
wrapRow(
|
|
361
|
+
theme.fg("accent", theme.bold("Powerline segments")),
|
|
362
|
+
innerWidth,
|
|
363
|
+
),
|
|
364
|
+
);
|
|
365
|
+
lines.push(border(`├${"─".repeat(innerWidth)}┤`));
|
|
366
|
+
for (const line of selectList.render(innerWidth))
|
|
367
|
+
lines.push(wrapRow(line, innerWidth));
|
|
368
|
+
lines.push(border(`├${"─".repeat(innerWidth)}┤`));
|
|
369
|
+
lines.push(
|
|
370
|
+
wrapRow(
|
|
371
|
+
theme.fg("dim", "↑↓ navigate · enter activate · esc cancel"),
|
|
372
|
+
innerWidth,
|
|
373
|
+
),
|
|
374
|
+
);
|
|
375
|
+
lines.push(border(`╰${"─".repeat(innerWidth)}╯`));
|
|
376
|
+
return lines;
|
|
377
|
+
},
|
|
378
|
+
invalidate: () => selectList.invalidate(),
|
|
379
|
+
handleInput: (data: string) => {
|
|
380
|
+
selectList.handleInput(data);
|
|
381
|
+
tui.requestRender();
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
overlay: true,
|
|
387
|
+
overlayOptions: () => ({
|
|
388
|
+
verticalAlign: "center",
|
|
389
|
+
horizontalAlign: "center",
|
|
390
|
+
}),
|
|
391
|
+
},
|
|
392
|
+
);
|
|
393
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import { resetLayoutCache, requestStatusRender } from "./segment-context.ts";
|
|
4
|
+
import {
|
|
5
|
+
renderBashTranscriptLines,
|
|
6
|
+
renderLastPromptLines,
|
|
7
|
+
renderPowerlinePrimaryLines,
|
|
8
|
+
renderPowerlineQueuePreviewLines,
|
|
9
|
+
renderPowerlineSecondaryLines,
|
|
10
|
+
renderPowerlineStatusLines,
|
|
11
|
+
} from "./status-line-renderers.ts";
|
|
12
|
+
import { config } from "./state.ts";
|
|
13
|
+
import type { RuntimeState } from "./types.ts";
|
|
14
|
+
|
|
15
|
+
export function installPowerlineWidgets(rt: RuntimeState, ctx: any) {
|
|
16
|
+
ctx.ui.setWidget(
|
|
17
|
+
"powerline-status",
|
|
18
|
+
() => ({
|
|
19
|
+
dispose() {},
|
|
20
|
+
invalidate() {
|
|
21
|
+
requestStatusRender(rt);
|
|
22
|
+
},
|
|
23
|
+
render(width: number): string[] {
|
|
24
|
+
return renderPowerlineStatusLines(rt, width);
|
|
25
|
+
},
|
|
26
|
+
}),
|
|
27
|
+
{ placement: "aboveEditor" },
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
ctx.ui.setWidget(
|
|
31
|
+
"powerline-top",
|
|
32
|
+
(_tui: any, theme: Theme) => ({
|
|
33
|
+
dispose() {},
|
|
34
|
+
invalidate() {
|
|
35
|
+
resetLayoutCache(rt);
|
|
36
|
+
},
|
|
37
|
+
render(width: number): string[] {
|
|
38
|
+
return renderPowerlinePrimaryLines(rt, width, theme);
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
{
|
|
42
|
+
placement: config.placement === "below" ? "belowEditor" : "aboveEditor",
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
ctx.ui.setWidget(
|
|
47
|
+
"powerline-secondary",
|
|
48
|
+
(_tui: any, theme: Theme) => ({
|
|
49
|
+
dispose() {},
|
|
50
|
+
invalidate() {
|
|
51
|
+
resetLayoutCache(rt);
|
|
52
|
+
},
|
|
53
|
+
render(width: number): string[] {
|
|
54
|
+
return renderPowerlineSecondaryLines(rt, width, theme);
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
{ placement: "belowEditor" },
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
ctx.ui.setWidget(
|
|
61
|
+
"powerline-bash-transcript",
|
|
62
|
+
(_tui: any, theme: Theme) => ({
|
|
63
|
+
dispose() {},
|
|
64
|
+
invalidate() {},
|
|
65
|
+
render(width: number): string[] {
|
|
66
|
+
return renderBashTranscriptLines(rt, width, theme);
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
{ placement: "belowEditor" },
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
ctx.ui.setWidget(
|
|
73
|
+
"powerline-queue-preview",
|
|
74
|
+
(_tui: any, theme: Theme) => ({
|
|
75
|
+
dispose() {},
|
|
76
|
+
invalidate() {},
|
|
77
|
+
render(width: number): string[] {
|
|
78
|
+
return renderPowerlineQueuePreviewLines(rt, width, theme);
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
81
|
+
{ placement: "belowEditor" },
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
ctx.ui.setWidget(
|
|
85
|
+
"powerline-last-prompt",
|
|
86
|
+
() => ({
|
|
87
|
+
dispose() {},
|
|
88
|
+
invalidate() {},
|
|
89
|
+
render(width: number): string[] {
|
|
90
|
+
return renderLastPromptLines(rt, width);
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
{ placement: "belowEditor" },
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { getAgentPath } from "../paths/agent-dirs.ts";
|
|
5
|
+
import { isRecord } from "./settings-io.ts";
|
|
6
|
+
import { PROMPT_HISTORY_LIMIT } from "./constants.ts";
|
|
7
|
+
|
|
8
|
+
const PROMPT_HISTORY_TRACKED = Symbol.for("powerlinePromptHistoryTracked");
|
|
9
|
+
const PROMPT_HISTORY_STATE_KEY = Symbol.for("powerlinePromptHistoryState");
|
|
10
|
+
|
|
11
|
+
export interface PromptHistoryEditor {
|
|
12
|
+
addToHistory?: (text: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type PromptHistoryState = { savedPromptHistory: string[] };
|
|
16
|
+
|
|
17
|
+
export function hasNonWhitespaceText(text: string): boolean {
|
|
18
|
+
return text.trim().length > 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isPromptHistoryState(
|
|
22
|
+
value: unknown,
|
|
23
|
+
): value is PromptHistoryState {
|
|
24
|
+
return (
|
|
25
|
+
isRecord(value) &&
|
|
26
|
+
Array.isArray(value.savedPromptHistory) &&
|
|
27
|
+
value.savedPromptHistory.every((entry) => typeof entry === "string")
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getPromptHistoryState(): PromptHistoryState {
|
|
32
|
+
const existing = Reflect.get(globalThis, PROMPT_HISTORY_STATE_KEY);
|
|
33
|
+
if (isPromptHistoryState(existing)) {
|
|
34
|
+
return existing;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const state: PromptHistoryState = { savedPromptHistory: [] };
|
|
38
|
+
Reflect.set(globalThis, PROMPT_HISTORY_STATE_KEY, state);
|
|
39
|
+
return state;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function readPromptHistory(
|
|
43
|
+
editor: PromptHistoryEditor | null | undefined,
|
|
44
|
+
): string[] {
|
|
45
|
+
if (!editor) return [];
|
|
46
|
+
const history = Reflect.get(editor, "history");
|
|
47
|
+
if (!Array.isArray(history)) return [];
|
|
48
|
+
|
|
49
|
+
const normalized: string[] = [];
|
|
50
|
+
for (const entry of history) {
|
|
51
|
+
if (typeof entry !== "string") continue;
|
|
52
|
+
const trimmed = entry.trim();
|
|
53
|
+
if (!trimmed) continue;
|
|
54
|
+
if (normalized.length > 0 && normalized[normalized.length - 1] === trimmed)
|
|
55
|
+
continue;
|
|
56
|
+
normalized.push(trimmed);
|
|
57
|
+
if (normalized.length >= PROMPT_HISTORY_LIMIT) break;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return normalized;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function snapshotPromptHistory(
|
|
64
|
+
editor: PromptHistoryEditor | null | undefined,
|
|
65
|
+
): void {
|
|
66
|
+
const history = readPromptHistory(editor);
|
|
67
|
+
if (history.length > 0) {
|
|
68
|
+
getPromptHistoryState().savedPromptHistory = [...history];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function restorePromptHistory(
|
|
73
|
+
editor: PromptHistoryEditor | null | undefined,
|
|
74
|
+
): void {
|
|
75
|
+
const { savedPromptHistory } = getPromptHistoryState();
|
|
76
|
+
if (!savedPromptHistory.length || typeof editor?.addToHistory !== "function")
|
|
77
|
+
return;
|
|
78
|
+
|
|
79
|
+
for (let i = savedPromptHistory.length - 1; i >= 0; i--) {
|
|
80
|
+
editor.addToHistory(savedPromptHistory[i]);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function trackPromptHistory(
|
|
85
|
+
editor: PromptHistoryEditor | null | undefined,
|
|
86
|
+
): void {
|
|
87
|
+
if (!editor || typeof editor.addToHistory !== "function") return;
|
|
88
|
+
if (Reflect.get(editor, PROMPT_HISTORY_TRACKED)) {
|
|
89
|
+
snapshotPromptHistory(editor);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const originalAddToHistory = editor.addToHistory.bind(editor);
|
|
94
|
+
editor.addToHistory = (text: string) => {
|
|
95
|
+
originalAddToHistory(text);
|
|
96
|
+
snapshotPromptHistory(editor);
|
|
97
|
+
};
|
|
98
|
+
Reflect.set(editor, PROMPT_HISTORY_TRACKED, true);
|
|
99
|
+
snapshotPromptHistory(editor);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getSessionsPath(): string {
|
|
103
|
+
return getAgentPath("sessions");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getProjectSessionsPath(cwd: string): string {
|
|
107
|
+
const projectKey = cwd
|
|
108
|
+
.replace(/^[/\\]+|[/\\]+$/g, "")
|
|
109
|
+
.replace(/[\\/]+/g, "-");
|
|
110
|
+
|
|
111
|
+
return join(getSessionsPath(), `--${projectKey}--`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function getPromptHistoryText(content: unknown): string {
|
|
115
|
+
if (typeof content === "string") {
|
|
116
|
+
return content.replace(/\s+/g, " ").trim();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!Array.isArray(content)) {
|
|
120
|
+
return "";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const parts: string[] = [];
|
|
124
|
+
for (const block of content) {
|
|
125
|
+
if (
|
|
126
|
+
!isRecord(block) ||
|
|
127
|
+
block.type !== "text" ||
|
|
128
|
+
typeof block.text !== "string"
|
|
129
|
+
) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
parts.push(block.text);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return parts.join("\n").replace(/\s+/g, " ").trim();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function readRecentProjectPrompts(cwd: string, limit: number): string[] {
|
|
139
|
+
const sessionsPath = getProjectSessionsPath(cwd);
|
|
140
|
+
if (!existsSync(sessionsPath)) {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const promptEntries: { text: string; timestamp: number }[] = [];
|
|
145
|
+
const fileNames = readdirSync(sessionsPath).filter((fileName) =>
|
|
146
|
+
fileName.endsWith(".jsonl"),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
for (const fileName of fileNames) {
|
|
150
|
+
const filePath = join(sessionsPath, fileName);
|
|
151
|
+
const lines = readFileSync(filePath, "utf-8").split("\n");
|
|
152
|
+
|
|
153
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
154
|
+
const line = lines[i];
|
|
155
|
+
if (
|
|
156
|
+
!line ||
|
|
157
|
+
!line.includes('"type":"message"') ||
|
|
158
|
+
!line.includes('"role":"user"')
|
|
159
|
+
) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let entry: unknown;
|
|
164
|
+
try {
|
|
165
|
+
entry = JSON.parse(line);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
168
|
+
throw new Error(
|
|
169
|
+
`Failed to parse session file ${filePath}: ${message}`,
|
|
170
|
+
{ cause: error },
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (
|
|
175
|
+
!isRecord(entry) ||
|
|
176
|
+
entry.type !== "message" ||
|
|
177
|
+
!isRecord(entry.message) ||
|
|
178
|
+
entry.message.role !== "user"
|
|
179
|
+
) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const text = getPromptHistoryText(entry.message.content);
|
|
184
|
+
if (!hasNonWhitespaceText(text)) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const timestamp =
|
|
189
|
+
typeof entry.message.timestamp === "number"
|
|
190
|
+
? entry.message.timestamp
|
|
191
|
+
: typeof entry.timestamp === "string"
|
|
192
|
+
? Date.parse(entry.timestamp)
|
|
193
|
+
: 0;
|
|
194
|
+
|
|
195
|
+
promptEntries.push({
|
|
196
|
+
text,
|
|
197
|
+
timestamp: Number.isFinite(timestamp) ? timestamp : 0,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
promptEntries.sort((a, b) => b.timestamp - a.timestamp);
|
|
203
|
+
|
|
204
|
+
const prompts: string[] = [];
|
|
205
|
+
const seen = new Set<string>();
|
|
206
|
+
for (const entry of promptEntries) {
|
|
207
|
+
if (seen.has(entry.text)) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
seen.add(entry.text);
|
|
212
|
+
prompts.push(entry.text);
|
|
213
|
+
if (prompts.length >= limit) {
|
|
214
|
+
return prompts;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return prompts;
|
|
219
|
+
}
|