@groeponline/pi-wishcraft 0.20.0 → 0.22.0
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/CHANGELOG.md +17 -0
- package/README.md +83 -0
- package/ROADMAP.md +18 -20
- package/bash-mode/editor-ghost.ts +63 -0
- package/bash-mode/editor-history.ts +81 -0
- package/bash-mode/editor-input.ts +35 -0
- package/bash-mode/editor.ts +16 -134
- package/bash-mode/shell-session.ts +12 -15
- package/bash-mode/types.ts +2 -0
- package/docs/bash-mode.md +46 -0
- package/docs/commands.md +122 -0
- package/docs/configuration.md +307 -0
- package/docs/index.md +18 -0
- package/docs/segments.md +168 -0
- package/docs/skill-manager.md +7 -0
- package/docs/stash-and-shortcuts.md +64 -0
- package/docs/working-vibes.md +71 -0
- package/package.json +2 -1
- package/queue/store.ts +72 -3
- package/src/config/custom-items.ts +45 -0
- package/src/config/parse.ts +19 -3
- package/src/config/powerline-config.ts +5 -0
- package/src/config/primitives.ts +13 -1
- package/src/config/segment-options.ts +29 -24
- package/src/config/settings-patch.ts +23 -2
- package/src/config/types.ts +32 -17
- package/src/extension/commands/bash-mode-actions.ts +1 -0
- package/src/extension/commands/commands.ts +36 -18
- package/src/extension/commands/powerline-completions.ts +8 -0
- package/src/extension/commands/powerline-doctor.ts +247 -0
- package/src/extension/commands/powerline-export.ts +94 -0
- package/src/extension/commands/queue-commands.ts +29 -1
- package/src/extension/commands/vibe-command.ts +49 -1
- package/src/extension/core/constants.ts +1 -0
- package/src/extension/core/segment-context.ts +38 -2
- package/src/extension/core/state.ts +5 -1
- package/src/extension/core/status-export.ts +68 -0
- package/src/extension/core/types.ts +4 -0
- package/src/extension/hooks/repairs.ts +89 -16
- package/src/extension/session/activate.ts +1 -2
- package/src/extension/session/cost-alert.ts +36 -0
- package/src/extension/session/session-lifecycle.ts +82 -1
- package/src/extension/settings/settings-io.ts +23 -17
- package/src/extension/settings/wishcraft-config.ts +2 -1
- package/src/extension/shortcuts/shortcuts-config.ts +5 -0
- package/src/extension/ui/layout.ts +1 -1
- package/src/extension/ui/menu-items.ts +379 -0
- package/src/extension/ui/menu-views.ts +377 -191
- package/src/extension/ui/overlay-chrome.ts +163 -0
- package/src/extension/ui/powerline-menu-view.ts +2 -4
- package/src/extension/ui/token-overlays.ts +82 -0
- package/src/extension/welcome/welcome-integration.ts +5 -9
- package/src/segments/registry.ts +100 -39
- package/src/segments/system.ts +204 -64
- package/src/segments/usage.ts +10 -4
- package/src/usage/ledger.ts +6 -0
- package/src/usage/token-budget.ts +57 -0
- package/src/usage/tps-ring.ts +151 -0
- package/src/usage/usage-store.ts +351 -0
- package/src/welcome/banner.ts +2 -1
- package/src/welcome/index.ts +3 -1
- package/src/welcome/overlay.ts +2 -0
- package/src/welcome/renderer.ts +52 -10
- package/src/welcome/types.ts +1 -2
- package/src/welcome/whats-new.ts +156 -0
- package/src/welcome/widgets/index.ts +3 -0
- package/src/welcome/widgets/whats-new-widget.ts +19 -0
- package/src/working-vibes/generate.ts +144 -58
- package/src/working-vibes/index.ts +10 -2
- package/src/working-vibes/storage.ts +5 -0
|
@@ -7,6 +7,8 @@ import { PRESETS } from "../../config/presets.ts";
|
|
|
7
7
|
import { registerCdCommand } from "../../shell/cd-command.ts";
|
|
8
8
|
import { registerVibeCommand } from "./vibe-command.ts";
|
|
9
9
|
import { registerQueueCommands } from "./queue-commands.ts";
|
|
10
|
+
import { runPowerlineDoctor } from "./powerline-doctor.ts";
|
|
11
|
+
import { runPowerlineExport } from "./powerline-export.ts";
|
|
10
12
|
import { registerSkillManagerCommand } from "../skills/skill-manager.ts";
|
|
11
13
|
import { registerWishcraftConfigCommand } from "../settings/wishcraft-config.ts";
|
|
12
14
|
import { getRepairCounts } from "../hooks/index.ts";
|
|
@@ -14,10 +16,8 @@ import {
|
|
|
14
16
|
writePowerlineOptionSetting,
|
|
15
17
|
writePowerlinePresetSetting,
|
|
16
18
|
} from "../settings/settings-io.ts";
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
showSelectOverlay,
|
|
20
|
-
} from "../ui/menu-views.ts";
|
|
19
|
+
import { showOpenPortsList, showSelectOverlay } from "../ui/menu-views.ts";
|
|
20
|
+
import { showTpsOverlay, showUsageOverlay } from "../ui/token-overlays.ts";
|
|
21
21
|
import { showPowerlineMainMenu } from "../ui/powerline-menu-view.ts";
|
|
22
22
|
import { openStashHistory } from "../shortcuts/shortcuts-router.ts";
|
|
23
23
|
import { ensureShellSession, setBashModeActive } from "./bash-mode-actions.ts";
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
requestStatusRender,
|
|
28
28
|
resetLayoutCache,
|
|
29
29
|
} from "../core/segment-context.ts";
|
|
30
|
+
import { publishPowerlineStatuses } from "../core/status-export.ts";
|
|
30
31
|
import { config, normalizePreset } from "../core/state.ts";
|
|
31
32
|
import type { RuntimeState } from "../core/types.ts";
|
|
32
33
|
import { getPowerlineArgumentCompletions } from "./powerline-completions.ts";
|
|
@@ -92,6 +93,14 @@ export function registerCommands(pi: ExtensionAPI, rt: RuntimeState): void {
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
const normalizedArgs = args.trim().toLowerCase();
|
|
96
|
+
if (normalizedArgs === "doctor") {
|
|
97
|
+
await runPowerlineDoctor(rt, ctx);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (normalizedArgs === "export") {
|
|
101
|
+
await runPowerlineExport(ctx);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
95
104
|
const placementMatch = /^placement(?:\s+(above|below|toggle))?$/.exec(
|
|
96
105
|
normalizedArgs,
|
|
97
106
|
);
|
|
@@ -129,6 +138,7 @@ export function registerCommands(pi: ExtensionAPI, rt: RuntimeState): void {
|
|
|
129
138
|
const preset = normalizePreset(args);
|
|
130
139
|
if (preset) {
|
|
131
140
|
config.preset = preset;
|
|
141
|
+
publishPowerlineStatuses(ctx, { preset });
|
|
132
142
|
resetLayoutCache(rt);
|
|
133
143
|
if (rt.enabled) {
|
|
134
144
|
setupCustomEditor(pi, rt, ctx);
|
|
@@ -207,34 +217,42 @@ export function registerCommands(pi: ExtensionAPI, rt: RuntimeState): void {
|
|
|
207
217
|
},
|
|
208
218
|
});
|
|
209
219
|
|
|
220
|
+
pi.registerCommand("repairs", {
|
|
221
|
+
description: "Show tool-input repair stats (wishcraft harness layer)",
|
|
222
|
+
handler: async (_args, ctx) => {
|
|
223
|
+
const counts = getRepairCounts();
|
|
224
|
+
if (counts.size === 0) {
|
|
225
|
+
ctx.ui.notify("No tool-input repairs applied yet", "info");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const summary = [...counts.entries()]
|
|
229
|
+
.map(([key, n]) => `${key}: ${n}×`)
|
|
230
|
+
.join(" · ");
|
|
231
|
+
ctx.ui.notify(`Repairs — ${summary}`, "info");
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
210
235
|
pi.registerCommand("tps", {
|
|
211
|
-
description: "Show or set POWERLINE_TPS
|
|
236
|
+
description: "Show the live TPS overlay, or set POWERLINE_TPS",
|
|
212
237
|
handler: async (args, ctx) => {
|
|
213
238
|
rt.currentCtx = ctx;
|
|
214
239
|
const value = args?.trim();
|
|
215
240
|
if (!value) {
|
|
216
|
-
|
|
217
|
-
ctx.ui.notify(`TPS: ${current}`, "info");
|
|
241
|
+
await showTpsOverlay(rt, ctx);
|
|
218
242
|
return;
|
|
219
243
|
}
|
|
220
244
|
process.env.POWERLINE_TPS = value;
|
|
245
|
+
publishPowerlineStatuses(ctx, { tps: value });
|
|
221
246
|
ctx.ui.notify(`TPS set to: ${value}`, "info");
|
|
222
247
|
rt.tuiRef?.requestRender();
|
|
223
248
|
},
|
|
224
249
|
});
|
|
225
250
|
|
|
226
|
-
pi.registerCommand("
|
|
227
|
-
description: "Show
|
|
251
|
+
pi.registerCommand("usage", {
|
|
252
|
+
description: "Show session / today / week token usage overlay",
|
|
228
253
|
handler: async (_args, ctx) => {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
ctx.ui.notify("No tool-input repairs applied yet", "info");
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
const summary = [...counts.entries()]
|
|
235
|
-
.map(([key, n]) => `${key}: ${n}×`)
|
|
236
|
-
.join(" · ");
|
|
237
|
-
ctx.ui.notify(`Repairs — ${summary}`, "info");
|
|
254
|
+
rt.currentCtx = ctx;
|
|
255
|
+
await showUsageOverlay(rt, ctx);
|
|
238
256
|
},
|
|
239
257
|
});
|
|
240
258
|
|
|
@@ -7,6 +7,14 @@ export const POWERLINE_SUBCOMMANDS: ReadonlyArray<{
|
|
|
7
7
|
value: string;
|
|
8
8
|
description: string;
|
|
9
9
|
}> = [
|
|
10
|
+
{
|
|
11
|
+
value: "doctor",
|
|
12
|
+
description: "Run environment and settings diagnostics",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
value: "export",
|
|
16
|
+
description: "Export the current status-line configuration",
|
|
17
|
+
},
|
|
10
18
|
{
|
|
11
19
|
value: "placement",
|
|
12
20
|
description: "Move the primary row above or below the editor",
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import type { SelectItem } from "@earendil-works/pi-tui";
|
|
4
|
+
|
|
5
|
+
import { PRESETS } from "../../config/presets.ts";
|
|
6
|
+
import {
|
|
7
|
+
getProjectSettingsPath,
|
|
8
|
+
getSettingsPath,
|
|
9
|
+
readSettings,
|
|
10
|
+
} from "../settings/settings-io.ts";
|
|
11
|
+
import { getQueueStorePaths } from "../../../queue/store.ts";
|
|
12
|
+
import { hasNerdFonts } from "../../theme/icons.ts";
|
|
13
|
+
import { config } from "../core/state.ts";
|
|
14
|
+
import type { RuntimeState } from "../core/types.ts";
|
|
15
|
+
import { showSelectOverlay } from "../ui/menu-views.ts";
|
|
16
|
+
|
|
17
|
+
export type DoctorStatus = "ok" | "warn" | "fail";
|
|
18
|
+
|
|
19
|
+
export interface DoctorCheck {
|
|
20
|
+
status: DoctorStatus;
|
|
21
|
+
name: string;
|
|
22
|
+
detail: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function settingsFileHealth(path: string): DoctorStatus {
|
|
26
|
+
if (!existsSync(path)) return "warn";
|
|
27
|
+
try {
|
|
28
|
+
JSON.parse(readFileSync(path, "utf-8"));
|
|
29
|
+
return "ok";
|
|
30
|
+
} catch {
|
|
31
|
+
return "fail";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function gitPollingMode(): string {
|
|
36
|
+
const polling = config.segmentOptions?.git?.polling;
|
|
37
|
+
if (polling === "branch") return "branch-only";
|
|
38
|
+
if (polling === "off") return "off";
|
|
39
|
+
return "full";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build the diagnostic report for `/powerline doctor`. Pure-ish: reads the
|
|
44
|
+
* filesystem/env for the checks, but performs no UI and never mutates state,
|
|
45
|
+
* so it can be unit-tested headlessly.
|
|
46
|
+
*/
|
|
47
|
+
export function buildDoctorReport(
|
|
48
|
+
rt: RuntimeState,
|
|
49
|
+
ctx: any,
|
|
50
|
+
): DoctorCheck[] {
|
|
51
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
52
|
+
const checks: DoctorCheck[] = [];
|
|
53
|
+
|
|
54
|
+
// ── Settings ─────────────────────────────────────────────────────────────
|
|
55
|
+
const globalPath = getSettingsPath();
|
|
56
|
+
const projectPath = getProjectSettingsPath(cwd);
|
|
57
|
+
const globalHealth = settingsFileHealth(globalPath);
|
|
58
|
+
checks.push({
|
|
59
|
+
status: globalHealth,
|
|
60
|
+
name: "settings.global",
|
|
61
|
+
detail:
|
|
62
|
+
globalHealth === "fail"
|
|
63
|
+
? `invalid JSON at ${globalPath}`
|
|
64
|
+
: globalHealth === "warn"
|
|
65
|
+
? `not found (defaults in use) — ${globalPath}`
|
|
66
|
+
: `valid — ${globalPath}`,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const projectHealth = settingsFileHealth(projectPath);
|
|
70
|
+
checks.push({
|
|
71
|
+
status: projectHealth,
|
|
72
|
+
name: "settings.project",
|
|
73
|
+
detail:
|
|
74
|
+
projectHealth === "fail"
|
|
75
|
+
? `invalid JSON at ${projectPath}`
|
|
76
|
+
: projectHealth === "warn"
|
|
77
|
+
? "not present (global settings only)"
|
|
78
|
+
: `valid — ${projectPath}`,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const mergedSettings = readSettings(cwd);
|
|
82
|
+
const hasPowerlineKey =
|
|
83
|
+
typeof mergedSettings.powerline === "object" &&
|
|
84
|
+
mergedSettings.powerline !== null;
|
|
85
|
+
checks.push({
|
|
86
|
+
status: hasPowerlineKey ? "ok" : "warn",
|
|
87
|
+
name: "settings.powerline",
|
|
88
|
+
detail: hasPowerlineKey
|
|
89
|
+
? "powerline key present"
|
|
90
|
+
: "no powerline key — defaults in use",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ── Config validity ──────────────────────────────────────────────────────
|
|
94
|
+
const invalidEntries: string[] = [];
|
|
95
|
+
if (config.invalidPlacement)
|
|
96
|
+
invalidEntries.push(`placement "${config.invalidPlacement}"`);
|
|
97
|
+
for (const id of config.invalidDisabledSegments)
|
|
98
|
+
invalidEntries.push(`disabledSegment "${id}"`);
|
|
99
|
+
for (const id of config.invalidLayoutSegments)
|
|
100
|
+
invalidEntries.push(`layout "${id}"`);
|
|
101
|
+
checks.push({
|
|
102
|
+
status: invalidEntries.length > 0 ? "warn" : "ok",
|
|
103
|
+
name: "config",
|
|
104
|
+
detail:
|
|
105
|
+
invalidEntries.length > 0
|
|
106
|
+
? `ignored: ${invalidEntries.join(", ")}`
|
|
107
|
+
: `no invalid entries (preset: ${config.preset})`,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const rawPowerline = mergedSettings.powerline;
|
|
111
|
+
const requestedPreset =
|
|
112
|
+
typeof rawPowerline === "object" &&
|
|
113
|
+
rawPowerline !== null &&
|
|
114
|
+
typeof (rawPowerline as Record<string, unknown>).preset === "string"
|
|
115
|
+
? ((rawPowerline as Record<string, unknown>).preset as string).trim()
|
|
116
|
+
: null;
|
|
117
|
+
const customPresetNames = Object.keys(config.presets);
|
|
118
|
+
const unknownPreset =
|
|
119
|
+
requestedPreset &&
|
|
120
|
+
!Object.prototype.hasOwnProperty.call(PRESETS, requestedPreset) &&
|
|
121
|
+
!customPresetNames.includes(requestedPreset)
|
|
122
|
+
? requestedPreset
|
|
123
|
+
: null;
|
|
124
|
+
checks.push({
|
|
125
|
+
status: unknownPreset ? "warn" : "ok",
|
|
126
|
+
name: "preset",
|
|
127
|
+
detail: unknownPreset
|
|
128
|
+
? `unknown preset "${unknownPreset}" — using ${config.preset}`
|
|
129
|
+
: `active: ${config.preset}`,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ── Nerd Fonts ───────────────────────────────────────────────────────────
|
|
133
|
+
const override = process.env.POWERLINE_NERD_FONTS;
|
|
134
|
+
const termProgram = process.env.TERM_PROGRAM || "";
|
|
135
|
+
let nerdReason = "";
|
|
136
|
+
if (override === "1") nerdReason = "forced via POWERLINE_NERD_FONTS=1";
|
|
137
|
+
else if (override === "0") nerdReason = "POWERLINE_NERD_FONTS=0 forces ASCII";
|
|
138
|
+
else if (process.env.GHOSTTY_RESOURCES_DIR)
|
|
139
|
+
nerdReason = "Ghostty detected";
|
|
140
|
+
else if (termProgram) nerdReason = `TERM_PROGRAM=${termProgram}`;
|
|
141
|
+
else nerdReason = "no known Nerd Font terminal detected";
|
|
142
|
+
checks.push({
|
|
143
|
+
status: hasNerdFonts() ? "ok" : "warn",
|
|
144
|
+
name: "nerd-fonts",
|
|
145
|
+
detail: hasNerdFonts()
|
|
146
|
+
? `enabled (${nerdReason})`
|
|
147
|
+
: `disabled — ASCII glyphs (${nerdReason})`,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ── Git ──────────────────────────────────────────────────────────────────
|
|
151
|
+
const gitProbe = spawnSync("git", ["--version"], {
|
|
152
|
+
encoding: "utf8",
|
|
153
|
+
timeout: 2000,
|
|
154
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
155
|
+
});
|
|
156
|
+
if (gitProbe.error || gitProbe.status !== 0) {
|
|
157
|
+
checks.push({
|
|
158
|
+
status: "fail",
|
|
159
|
+
name: "git",
|
|
160
|
+
detail: "git binary not found — git segment cannot poll",
|
|
161
|
+
});
|
|
162
|
+
} else {
|
|
163
|
+
const repoProbe = spawnSync(
|
|
164
|
+
"git",
|
|
165
|
+
["rev-parse", "--is-inside-work-tree"],
|
|
166
|
+
{
|
|
167
|
+
cwd,
|
|
168
|
+
encoding: "utf8",
|
|
169
|
+
timeout: 2000,
|
|
170
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
171
|
+
},
|
|
172
|
+
);
|
|
173
|
+
checks.push({
|
|
174
|
+
status: repoProbe.status === 0 ? "ok" : "warn",
|
|
175
|
+
name: "git",
|
|
176
|
+
detail:
|
|
177
|
+
repoProbe.status === 0
|
|
178
|
+
? `polling: ${gitPollingMode()} — inside a git repo`
|
|
179
|
+
: `polling: ${gitPollingMode()} — not a git repo (segment hidden)`,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Bash shell ───────────────────────────────────────────────────────────
|
|
184
|
+
const shell = rt.shellSession?.state;
|
|
185
|
+
const transcript = rt.bashTranscript.getSnapshot();
|
|
186
|
+
checks.push({
|
|
187
|
+
status: "ok",
|
|
188
|
+
name: "bash",
|
|
189
|
+
detail: rt.bashModeActive
|
|
190
|
+
? `active — shell ${shell?.running ? "running" : "idle"}${shell?.shellName ? ` (${shell.shellName})` : ""}, transcript ${transcript.totalLines} lines`
|
|
191
|
+
: `inactive (ctrl+shift+b) — transcript ${transcript.totalLines} lines`,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// ── Queue store files ────────────────────────────────────────────────────
|
|
195
|
+
const { inboxPath, aliasesPath, archivePath } = getQueueStorePaths();
|
|
196
|
+
let malformed = 0;
|
|
197
|
+
let totalLines = 0;
|
|
198
|
+
if (existsSync(inboxPath)) {
|
|
199
|
+
for (const line of readFileSync(inboxPath, "utf-8").split("\n")) {
|
|
200
|
+
if (!line.trim()) continue;
|
|
201
|
+
totalLines += 1;
|
|
202
|
+
try {
|
|
203
|
+
JSON.parse(line);
|
|
204
|
+
} catch {
|
|
205
|
+
malformed += 1;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const staleLock = existsSync(`${inboxPath}.lock`);
|
|
210
|
+
const aliasesHealth = settingsFileHealth(aliasesPath);
|
|
211
|
+
const queueDetail = staleLock
|
|
212
|
+
? `stale lock at ${inboxPath}.lock — a crashed writer may hold it`
|
|
213
|
+
: malformed > 0
|
|
214
|
+
? `${malformed} malformed line${malformed === 1 ? "" : "s"} in inbox.jsonl (${totalLines} total)`
|
|
215
|
+
: totalLines === 0
|
|
216
|
+
? "no items yet"
|
|
217
|
+
: `${totalLines} item${totalLines === 1 ? "" : "s"} in inbox.jsonl`;
|
|
218
|
+
checks.push({
|
|
219
|
+
status: staleLock || malformed > 0 || aliasesHealth === "fail"
|
|
220
|
+
? "fail"
|
|
221
|
+
: "ok",
|
|
222
|
+
name: "queue",
|
|
223
|
+
detail: `${queueDetail}${aliasesHealth === "fail" ? ` — projects.json is invalid JSON` : ""}${existsSync(archivePath) ? ` — archive present` : ""}`,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return checks;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Render the doctor report as a scrollable overlay; enter copies a line. */
|
|
230
|
+
export async function runPowerlineDoctor(
|
|
231
|
+
rt: RuntimeState,
|
|
232
|
+
ctx: any,
|
|
233
|
+
): Promise<void> {
|
|
234
|
+
const checks = buildDoctorReport(rt, ctx);
|
|
235
|
+
const items: SelectItem[] = checks.map((check) => ({
|
|
236
|
+
label: `${check.status === "ok" ? "[ok] " : check.status === "warn" ? "[warn]" : "[fail]"} ${check.name} — ${check.detail}`,
|
|
237
|
+
value: `${check.name}: ${check.detail}`,
|
|
238
|
+
}));
|
|
239
|
+
const picked = await showSelectOverlay(
|
|
240
|
+
ctx,
|
|
241
|
+
"Powerline doctor",
|
|
242
|
+
"↑↓ navigate · enter copy · esc close",
|
|
243
|
+
items,
|
|
244
|
+
Math.min(items.length, 20),
|
|
245
|
+
);
|
|
246
|
+
if (picked) ctx.ui.notify(picked.value, "info");
|
|
247
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { copyToClipboard } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { SelectItem } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
import { getPreset } from "../../config/presets.ts";
|
|
5
|
+
import {
|
|
6
|
+
mergeSegmentsWithCustomItems,
|
|
7
|
+
type PowerlineConfig,
|
|
8
|
+
} from "../../config/powerline-config.ts";
|
|
9
|
+
import type { StatusLineSegmentId } from "../../config/types.ts";
|
|
10
|
+
import { config } from "../core/state.ts";
|
|
11
|
+
import { showSelectOverlay } from "../ui/menu-views.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A settings-ready snapshot of the currently rendered powerline look. Pasted
|
|
15
|
+
* under `powerline` it reproduces the same preset + effective layout + labels
|
|
16
|
+
* (disabled segments are already filtered out of the layout groups).
|
|
17
|
+
*/
|
|
18
|
+
export interface PowerlineExportSnippet {
|
|
19
|
+
preset: string;
|
|
20
|
+
/** Included only when the user explicitly overrode the preset separator. */
|
|
21
|
+
separator?: string;
|
|
22
|
+
layout: {
|
|
23
|
+
left: StatusLineSegmentId[];
|
|
24
|
+
right: StatusLineSegmentId[];
|
|
25
|
+
secondary?: StatusLineSegmentId[];
|
|
26
|
+
};
|
|
27
|
+
/** Included only when at least one label is configured. */
|
|
28
|
+
segmentLabels?: Record<string, string>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Resolve the effective segment layout for the current config (pure). */
|
|
32
|
+
export function buildPowerlineExportSnippet(
|
|
33
|
+
cfg: PowerlineConfig,
|
|
34
|
+
): PowerlineExportSnippet {
|
|
35
|
+
const presetDef = getPreset(cfg.preset);
|
|
36
|
+
const merged = mergeSegmentsWithCustomItems(presetDef, cfg.customItems, {
|
|
37
|
+
layout: cfg.layout,
|
|
38
|
+
disabledSegments: cfg.disabledSegments,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const layout: PowerlineExportSnippet["layout"] = {
|
|
42
|
+
left: [...merged.leftSegments],
|
|
43
|
+
right: [...merged.rightSegments],
|
|
44
|
+
};
|
|
45
|
+
if (merged.secondarySegments.length > 0) {
|
|
46
|
+
layout.secondary = [...merged.secondarySegments];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const snippet: PowerlineExportSnippet = { preset: cfg.preset, layout };
|
|
50
|
+
if (cfg.separator) snippet.separator = cfg.separator;
|
|
51
|
+
if (Object.keys(cfg.segmentLabels).length > 0) {
|
|
52
|
+
snippet.segmentLabels = { ...cfg.segmentLabels };
|
|
53
|
+
}
|
|
54
|
+
return snippet;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Pretty-print the export as the JSON users paste into settings.json. */
|
|
58
|
+
export function formatPowerlineExport(snippet: PowerlineExportSnippet): string {
|
|
59
|
+
return JSON.stringify(snippet, null, 2);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Review the export in a scrollable overlay; Enter copies the full snippet. */
|
|
63
|
+
export async function runPowerlineExport(ctx: any): Promise<void> {
|
|
64
|
+
const snippet = buildPowerlineExportSnippet(config);
|
|
65
|
+
const json = formatPowerlineExport(snippet);
|
|
66
|
+
const lines = json.split("\n");
|
|
67
|
+
const items: SelectItem[] = lines.map((line) => ({
|
|
68
|
+
label: line,
|
|
69
|
+
value: line,
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
const picked = await showSelectOverlay(
|
|
73
|
+
ctx,
|
|
74
|
+
"Powerline export",
|
|
75
|
+
"↑↓ scroll · enter copy full snippet · esc close",
|
|
76
|
+
items,
|
|
77
|
+
Math.min(items.length, 24),
|
|
78
|
+
);
|
|
79
|
+
if (!picked) return;
|
|
80
|
+
|
|
81
|
+
let copied = false;
|
|
82
|
+
try {
|
|
83
|
+
await copyToClipboard(json);
|
|
84
|
+
copied = true;
|
|
85
|
+
} catch {
|
|
86
|
+
copied = false;
|
|
87
|
+
}
|
|
88
|
+
ctx.ui.notify(
|
|
89
|
+
copied
|
|
90
|
+
? "Powerline config copied to clipboard"
|
|
91
|
+
: "Could not copy to clipboard; the snippet is shown above",
|
|
92
|
+
copied ? "info" : "warning",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "../queue/queue-integration.ts";
|
|
13
13
|
import { getCurrentEditorText } from "../shortcuts/shortcuts-router.ts";
|
|
14
14
|
import { getQueueContext } from "../queue/queue-context.ts";
|
|
15
|
+
import { config } from "../core/state.ts";
|
|
15
16
|
import type { RuntimeState } from "../core/types.ts";
|
|
16
17
|
|
|
17
18
|
export function registerQueueCommands(
|
|
@@ -210,6 +211,30 @@ export function registerQueueCommands(
|
|
|
210
211
|
return;
|
|
211
212
|
}
|
|
212
213
|
|
|
214
|
+
if (action === "archive") {
|
|
215
|
+
const hoursArg = Number(parts[1]);
|
|
216
|
+
const hours =
|
|
217
|
+
Number.isFinite(hoursArg) && hoursArg > 0
|
|
218
|
+
? Math.min(hoursArg, 24 * 365)
|
|
219
|
+
: config.queue.retentionHours;
|
|
220
|
+
const result = rt.queueStore.archiveSentItems(
|
|
221
|
+
hours * 60 * 60 * 1000,
|
|
222
|
+
);
|
|
223
|
+
if (result.archived === 0) {
|
|
224
|
+
ctx.ui.notify(
|
|
225
|
+
`Nothing to archive — ${result.remainingSent} sent item${result.remainingSent === 1 ? " is" : "s are"} newer than ${hours}h (retention: powerline.queue.retentionHours)`,
|
|
226
|
+
"info",
|
|
227
|
+
);
|
|
228
|
+
} else {
|
|
229
|
+
ctx.ui.notify(
|
|
230
|
+
`Archived ${result.archived} sent item${result.archived === 1 ? "" : "s"} older than ${hours}h to inbox.archive.jsonl`,
|
|
231
|
+
"info",
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
requestQueueRender(rt);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
213
238
|
if (action === "target") {
|
|
214
239
|
const id = parts[1];
|
|
215
240
|
const spec = parts[2];
|
|
@@ -239,7 +264,10 @@ export function registerQueueCommands(
|
|
|
239
264
|
return;
|
|
240
265
|
}
|
|
241
266
|
|
|
242
|
-
ctx.ui.notify(
|
|
267
|
+
ctx.ui.notify(
|
|
268
|
+
"Usage: /queue [send|retry|clear|target|alias|archive [hours]]",
|
|
269
|
+
"info",
|
|
270
|
+
);
|
|
243
271
|
},
|
|
244
272
|
});
|
|
245
273
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { copyToClipboard } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { SelectItem } from "@earendil-works/pi-tui";
|
|
2
4
|
|
|
3
5
|
import {
|
|
4
6
|
generateVibesBatch,
|
|
7
|
+
generateVibeSamples,
|
|
5
8
|
getVibeFileCount,
|
|
6
9
|
getVibeMode,
|
|
7
10
|
getVibeModel,
|
|
@@ -12,6 +15,7 @@ import {
|
|
|
12
15
|
setVibeModel,
|
|
13
16
|
setVibeTheme,
|
|
14
17
|
} from "../../working-vibes/index.ts";
|
|
18
|
+
import { showSelectOverlay } from "../ui/menu-views.ts";
|
|
15
19
|
|
|
16
20
|
export async function runVibeCommand(args: string, ctx: any): Promise<void> {
|
|
17
21
|
const parts = args?.trim().split(/\s+/) || [];
|
|
@@ -114,6 +118,50 @@ export async function runVibeCommand(args: string, ctx: any): Promise<void> {
|
|
|
114
118
|
return;
|
|
115
119
|
}
|
|
116
120
|
|
|
121
|
+
// /vibe test <theme> - preview 3 sample vibes without committing the theme
|
|
122
|
+
if (subcommand === "test") {
|
|
123
|
+
const theme = parts.slice(1).join(" ").trim();
|
|
124
|
+
if (!theme) {
|
|
125
|
+
ctx.ui.notify("Usage: /vibe test <theme>", "error");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
ctx.ui.notify(`Previewing 3 sample vibes for "${theme}"...`, "info");
|
|
129
|
+
|
|
130
|
+
const result = await generateVibeSamples(theme, 3);
|
|
131
|
+
if (!result.success) {
|
|
132
|
+
ctx.ui.notify(`Failed to preview vibes: ${result.error}`, "error");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const items: SelectItem[] = result.samples.map((sample, index) => ({
|
|
137
|
+
label: `${index + 1}. ${sample}`,
|
|
138
|
+
value: sample,
|
|
139
|
+
}));
|
|
140
|
+
const picked = await showSelectOverlay(
|
|
141
|
+
ctx,
|
|
142
|
+
`Vibe preview — "${result.theme}"`,
|
|
143
|
+
"↑↓ scroll · enter copy /vibe command · esc close",
|
|
144
|
+
items,
|
|
145
|
+
Math.min(items.length, 6),
|
|
146
|
+
);
|
|
147
|
+
if (!picked) return;
|
|
148
|
+
|
|
149
|
+
let copied = false;
|
|
150
|
+
try {
|
|
151
|
+
await copyToClipboard(`/vibe ${result.theme}`);
|
|
152
|
+
copied = true;
|
|
153
|
+
} catch {
|
|
154
|
+
copied = false;
|
|
155
|
+
}
|
|
156
|
+
ctx.ui.notify(
|
|
157
|
+
copied
|
|
158
|
+
? `Copied: /vibe ${result.theme}`
|
|
159
|
+
: `Run /vibe ${result.theme} to apply this theme`,
|
|
160
|
+
copied ? "info" : "warning",
|
|
161
|
+
);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
117
165
|
// /vibe off - disable
|
|
118
166
|
if (subcommand === "off") {
|
|
119
167
|
const persisted = setVibeTheme(null);
|
|
@@ -151,7 +199,7 @@ export async function runVibeCommand(args: string, ctx: any): Promise<void> {
|
|
|
151
199
|
export function registerVibeCommand(pi: ExtensionAPI): void {
|
|
152
200
|
pi.registerCommand("vibe", {
|
|
153
201
|
description:
|
|
154
|
-
"Set working message theme. Usage: /vibe [theme|off|mode|model|generate]",
|
|
202
|
+
"Set working message theme. Usage: /vibe [theme|off|mode|model|generate|test]",
|
|
155
203
|
handler: async (args, ctx) => {
|
|
156
204
|
runVibeCommand(args, ctx);
|
|
157
205
|
},
|
|
@@ -22,6 +22,7 @@ export const DEFAULT_BASH_MODE_SETTINGS = {
|
|
|
22
22
|
toggleShortcut: "ctrl+shift+b",
|
|
23
23
|
transcriptMaxLines: 2000,
|
|
24
24
|
transcriptMaxBytes: 512 * 1024,
|
|
25
|
+
initScript: null,
|
|
25
26
|
} as const satisfies BashModeSettings;
|
|
26
27
|
export const SHORTCUT_KEYS: PowerlineShortcutKey[] = [
|
|
27
28
|
"stashHistory",
|
|
@@ -7,17 +7,27 @@ import type { SegmentContext, ColorScheme } from "../../config/types.ts";
|
|
|
7
7
|
import { getPreset } from "../../config/presets.ts";
|
|
8
8
|
import {
|
|
9
9
|
collectHiddenExtensionStatusKeys,
|
|
10
|
+
deriveAutoCustomItems,
|
|
10
11
|
mergeSegmentOptions,
|
|
11
12
|
} from "../../config/powerline-config.ts";
|
|
12
13
|
import { getDefaultColors } from "../../theme/theme.ts";
|
|
13
14
|
import { getGitStatus } from "../../git/status.ts";
|
|
14
15
|
import { getQueueContext } from "../queue/queue-context.ts";
|
|
15
16
|
import { getUsageTokenTotal } from "../../usage/ledger.ts";
|
|
17
|
+
import {
|
|
18
|
+
dayKey,
|
|
19
|
+
loadUsageFileFromDisk,
|
|
20
|
+
tokenTotal,
|
|
21
|
+
totalsForRange,
|
|
22
|
+
} from "../../usage/usage-store.ts";
|
|
23
|
+
import { parseTokenBudget } from "../../usage/token-budget.ts";
|
|
24
|
+
import { readSettings } from "../settings/settings-io.ts";
|
|
16
25
|
import {
|
|
17
26
|
CUSTOM_COMPACTION_STATUS_KEY,
|
|
18
27
|
EDITOR_STATUS_DEFER_MS,
|
|
19
28
|
} from "./constants.ts";
|
|
20
29
|
import { config, customCompactionEnabled } from "./state.ts";
|
|
30
|
+
import { HIDDEN_POWERLINE_STATUS_KEYS } from "./status-export.ts";
|
|
21
31
|
import type { RuntimeState } from "./types.ts";
|
|
22
32
|
|
|
23
33
|
/**
|
|
@@ -154,12 +164,26 @@ export function buildSegmentContext(
|
|
|
154
164
|
const gitStatus = getGitStatus(gitBranch, segmentOptions.git?.polling);
|
|
155
165
|
const extensionStatuses =
|
|
156
166
|
rt.footerDataRef?.getExtensionStatuses() ?? new Map();
|
|
167
|
+
const excludedStatusKeys = new Set<string>([
|
|
168
|
+
CUSTOM_COMPACTION_STATUS_KEY,
|
|
169
|
+
"stash",
|
|
170
|
+
...HIDDEN_POWERLINE_STATUS_KEYS,
|
|
171
|
+
]);
|
|
172
|
+
const effectiveCustomItems = deriveAutoCustomItems(
|
|
173
|
+
config.customItems,
|
|
174
|
+
extensionStatuses,
|
|
175
|
+
config.customItemsAuto,
|
|
176
|
+
excludedStatusKeys,
|
|
177
|
+
);
|
|
157
178
|
const customItemsById = new Map(
|
|
158
|
-
|
|
179
|
+
effectiveCustomItems.map((item) => [item.id, item]),
|
|
159
180
|
);
|
|
160
181
|
const hiddenExtensionStatusKeys = collectHiddenExtensionStatusKeys(
|
|
161
|
-
|
|
182
|
+
effectiveCustomItems,
|
|
162
183
|
);
|
|
184
|
+
for (const key of HIDDEN_POWERLINE_STATUS_KEYS) {
|
|
185
|
+
hiddenExtensionStatusKeys.add(key);
|
|
186
|
+
}
|
|
163
187
|
|
|
164
188
|
// Check if using OAuth subscription
|
|
165
189
|
const usingSubscription = ctx.model
|
|
@@ -201,8 +225,20 @@ export function buildSegmentContext(
|
|
|
201
225
|
extensionStatuses,
|
|
202
226
|
hiddenExtensionStatusKeys,
|
|
203
227
|
customItemsById,
|
|
228
|
+
effectiveCustomItems,
|
|
204
229
|
options: segmentOptions,
|
|
205
230
|
segmentLabels: new Map(Object.entries(config.segmentLabels)),
|
|
231
|
+
tokenBudget: (() => {
|
|
232
|
+
const dailyLimit = parseTokenBudget(
|
|
233
|
+
readSettings(ctx.cwd ?? process.cwd()).wishcraft,
|
|
234
|
+
).daily;
|
|
235
|
+
const now = Date.now();
|
|
236
|
+
const todayStart = Date.parse(`${dayKey(now)}T00:00:00`);
|
|
237
|
+
const dailyUsed = tokenTotal(
|
|
238
|
+
totalsForRange(loadUsageFileFromDisk(), todayStart, now + 1),
|
|
239
|
+
);
|
|
240
|
+
return { dailyLimit, dailyUsed };
|
|
241
|
+
})(),
|
|
206
242
|
theme,
|
|
207
243
|
colors,
|
|
208
244
|
};
|