@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.
Files changed (70) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +83 -0
  3. package/ROADMAP.md +18 -20
  4. package/bash-mode/editor-ghost.ts +63 -0
  5. package/bash-mode/editor-history.ts +81 -0
  6. package/bash-mode/editor-input.ts +35 -0
  7. package/bash-mode/editor.ts +16 -134
  8. package/bash-mode/shell-session.ts +12 -15
  9. package/bash-mode/types.ts +2 -0
  10. package/docs/bash-mode.md +46 -0
  11. package/docs/commands.md +122 -0
  12. package/docs/configuration.md +307 -0
  13. package/docs/index.md +18 -0
  14. package/docs/segments.md +168 -0
  15. package/docs/skill-manager.md +7 -0
  16. package/docs/stash-and-shortcuts.md +64 -0
  17. package/docs/working-vibes.md +71 -0
  18. package/package.json +2 -1
  19. package/queue/store.ts +72 -3
  20. package/src/config/custom-items.ts +45 -0
  21. package/src/config/parse.ts +19 -3
  22. package/src/config/powerline-config.ts +5 -0
  23. package/src/config/primitives.ts +13 -1
  24. package/src/config/segment-options.ts +29 -24
  25. package/src/config/settings-patch.ts +23 -2
  26. package/src/config/types.ts +32 -17
  27. package/src/extension/commands/bash-mode-actions.ts +1 -0
  28. package/src/extension/commands/commands.ts +36 -18
  29. package/src/extension/commands/powerline-completions.ts +8 -0
  30. package/src/extension/commands/powerline-doctor.ts +247 -0
  31. package/src/extension/commands/powerline-export.ts +94 -0
  32. package/src/extension/commands/queue-commands.ts +29 -1
  33. package/src/extension/commands/vibe-command.ts +49 -1
  34. package/src/extension/core/constants.ts +1 -0
  35. package/src/extension/core/segment-context.ts +38 -2
  36. package/src/extension/core/state.ts +5 -1
  37. package/src/extension/core/status-export.ts +68 -0
  38. package/src/extension/core/types.ts +4 -0
  39. package/src/extension/hooks/repairs.ts +89 -16
  40. package/src/extension/session/activate.ts +1 -2
  41. package/src/extension/session/cost-alert.ts +36 -0
  42. package/src/extension/session/session-lifecycle.ts +82 -1
  43. package/src/extension/settings/settings-io.ts +23 -17
  44. package/src/extension/settings/wishcraft-config.ts +2 -1
  45. package/src/extension/shortcuts/shortcuts-config.ts +5 -0
  46. package/src/extension/ui/layout.ts +1 -1
  47. package/src/extension/ui/menu-items.ts +379 -0
  48. package/src/extension/ui/menu-views.ts +377 -191
  49. package/src/extension/ui/overlay-chrome.ts +163 -0
  50. package/src/extension/ui/powerline-menu-view.ts +2 -4
  51. package/src/extension/ui/token-overlays.ts +82 -0
  52. package/src/extension/welcome/welcome-integration.ts +5 -9
  53. package/src/segments/registry.ts +100 -39
  54. package/src/segments/system.ts +204 -64
  55. package/src/segments/usage.ts +10 -4
  56. package/src/usage/ledger.ts +6 -0
  57. package/src/usage/token-budget.ts +57 -0
  58. package/src/usage/tps-ring.ts +151 -0
  59. package/src/usage/usage-store.ts +351 -0
  60. package/src/welcome/banner.ts +2 -1
  61. package/src/welcome/index.ts +3 -1
  62. package/src/welcome/overlay.ts +2 -0
  63. package/src/welcome/renderer.ts +52 -10
  64. package/src/welcome/types.ts +1 -2
  65. package/src/welcome/whats-new.ts +156 -0
  66. package/src/welcome/widgets/index.ts +3 -0
  67. package/src/welcome/widgets/whats-new-widget.ts +19 -0
  68. package/src/working-vibes/generate.ts +144 -58
  69. package/src/working-vibes/index.ts +10 -2
  70. 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
- showOpenPortsList,
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 value",
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
- const current = process.env.POWERLINE_TPS || "not set";
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("repairs", {
227
- description: "Show tool-input repair stats (wishcraft harness layer)",
251
+ pi.registerCommand("usage", {
252
+ description: "Show session / today / week token usage overlay",
228
253
  handler: async (_args, ctx) => {
229
- const counts = getRepairCounts();
230
- if (counts.size === 0) {
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("Usage: /queue [send|retry|clear|target|alias]", "info");
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
- config.customItems.map((item) => [item.id, item]),
179
+ effectiveCustomItems.map((item) => [item.id, item]),
159
180
  );
160
181
  const hiddenExtensionStatusKeys = collectHiddenExtensionStatusKeys(
161
- config.customItems,
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
  };