@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
@@ -41,7 +41,9 @@ export let config: PowerlineConfig = {
41
41
  invalidPlacement: null,
42
42
  welcome: true,
43
43
  stashSharpSShortcut: false,
44
- queue: { captureSigil: "#" },
44
+ costAlert: null,
45
+ customItemsAuto: false,
46
+ queue: { captureSigil: "#", retentionHours: 24 },
45
47
  segments: {},
46
48
  presets: {},
47
49
  segmentLabels: {},
@@ -109,6 +111,8 @@ export function createRuntimeState(
109
111
  stashedEditorText: null,
110
112
  stashedPromptHistory: readPersistedStashHistory(),
111
113
  currentEditor: null,
114
+ costAlertNotified: false,
115
+ tokenBudgetNotifiedLevel: 0,
112
116
  bashModeActive: false,
113
117
  bashTranscript: new BashTranscriptStore(bashModeSettings),
114
118
  bashCompletionEngine: new BashCompletionEngine(),
@@ -0,0 +1,68 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // ChefGroep status bridge. Powerline publishes its own state under a stable
3
+ // `powerline.*` key set via `ctx.ui.setStatus`, so ChefBar and other
4
+ // extensions can read it without depending on powerline internals. These keys
5
+ // are hidden from powerline's own `extension_statuses` segment.
6
+ // ═══════════════════════════════════════════════════════════════════════════
7
+
8
+ export const POWERLINE_STATUS_KEYS = {
9
+ preset: "powerline.preset",
10
+ tps: "powerline.tps",
11
+ ports: "powerline.ports",
12
+ } as const;
13
+
14
+ /** Keys never rendered in powerline's own bar (they exist for other extensions). */
15
+ export const HIDDEN_POWERLINE_STATUS_KEYS: ReadonlySet<string> = new Set(
16
+ Object.values(POWERLINE_STATUS_KEYS),
17
+ );
18
+
19
+ export interface PowerlineStatusSnapshot {
20
+ /** Active preset name. */
21
+ preset?: string;
22
+ /** `POWERLINE_TPS` override; `undefined` clears it (back to live rate). */
23
+ tps?: string | undefined;
24
+ /** open_ports count as text (`?` when a fleet host probe failed). */
25
+ ports?: string;
26
+ }
27
+
28
+ type StatusPublisherCtx = {
29
+ ui?: { setStatus?: (key: string, value: string | undefined) => void };
30
+ };
31
+
32
+ /**
33
+ * Pure: turn a partial snapshot into the exact `[statusKey, value]` writes to
34
+ * make. Only keys present in the snapshot are emitted (using `in` so an
35
+ * explicit `tps: undefined` still produces a clearing write).
36
+ */
37
+ export function buildPowerlineStatusExport(
38
+ snapshot: PowerlineStatusSnapshot,
39
+ ): Array<[string, string | undefined]> {
40
+ const entries: Array<[string, string | undefined]> = [];
41
+ if ("preset" in snapshot) {
42
+ entries.push([POWERLINE_STATUS_KEYS.preset, snapshot.preset]);
43
+ }
44
+ if ("tps" in snapshot) {
45
+ entries.push([POWERLINE_STATUS_KEYS.tps, snapshot.tps]);
46
+ }
47
+ if ("ports" in snapshot) {
48
+ entries.push([POWERLINE_STATUS_KEYS.ports, snapshot.ports]);
49
+ }
50
+ return entries;
51
+ }
52
+
53
+ /** Publish only the snapshot keys present, via `ctx.ui.setStatus`. */
54
+ export function publishPowerlineStatuses(
55
+ ctx: StatusPublisherCtx | null | undefined,
56
+ snapshot: PowerlineStatusSnapshot,
57
+ ): void {
58
+ const setStatus = ctx?.ui?.setStatus;
59
+ if (!setStatus) return;
60
+ for (const [key, value] of buildPowerlineStatusExport(snapshot)) {
61
+ setStatus(key, value);
62
+ }
63
+ }
64
+
65
+ /** Format a listening-port count for the status export (`?` when unknown). */
66
+ export function formatPortsStatusValue(count: number): string {
67
+ return count < 0 ? "?" : String(count);
68
+ }
@@ -62,6 +62,10 @@ export interface RuntimeState {
62
62
  stashedEditorText: string | null;
63
63
  stashedPromptHistory: string[];
64
64
  currentEditor: any;
65
+ /** True once the configured `costAlert` threshold has been notified this session. */
66
+ costAlertNotified: boolean;
67
+ /** Highest daily-budget warning already shown this session (0 / 80 / 100). */
68
+ tokenBudgetNotifiedLevel: 0 | 80 | 100;
65
69
  bashModeActive: boolean;
66
70
  bashTranscript: BashTranscriptStore;
67
71
  bashCompletionEngine: BashCompletionEngine;
@@ -2,20 +2,74 @@
2
2
  * repairs.ts
3
3
  * ---------------------------------------------------------------------------
4
4
  * Tool-input repairs vóór executie (pi's `tool_call` event heeft mutable
5
- * input). Alleen de reparaties die zonder schema-kennis universeel veilig
6
- * zijn; schema-afhankelijke repairs (bare-string-wrap, JSON-string-array,
7
- * placeholder-object) vereisen de validator-issue-lijst en blijven bewust
8
- * weg tot Pi die expose't.
5
+ * input). Custom/extension tools only. Order is fixed: json-parse before
6
+ * bare-string wrap so `'["a"]'` becomes `["a"]`, not `[['["a"]']]`.
9
7
  * ---------------------------------------------------------------------------
10
8
  */
11
9
 
12
10
  /** Degenerate markdown auto-link: link-tekst == url zonder protocol. */
13
11
  const AUTO_LINK_RE = /^\[([^\]\s]+)\]\((https?:\/\/|file:\/\/)?([^)\s]*)\)$/;
14
12
 
13
+ const CORE_TOOLS = new Set([
14
+ "bash",
15
+ "read",
16
+ "edit",
17
+ "write",
18
+ "grep",
19
+ "find",
20
+ "ls",
21
+ ]);
22
+
23
+ const PATH_ALIASES = ["filePath", "absolutePath", "target_file", "file_path"];
24
+
25
+ const ARRAY_KEYS = new Set([
26
+ "files",
27
+ "paths",
28
+ "globs",
29
+ "args",
30
+ "items",
31
+ "patterns",
32
+ "include",
33
+ "exclude",
34
+ "queries",
35
+ "urls",
36
+ "commands",
37
+ ]);
38
+
15
39
  function isDegenerateAutoLink(linkText: string, url: string): boolean {
16
40
  return linkText === url || linkText === decodeURI(url);
17
41
  }
18
42
 
43
+ function isArrayKey(key: string): boolean {
44
+ if (ARRAY_KEYS.has(key)) return true;
45
+ return /_(files|paths|globs|args|items|patterns)$/.test(key);
46
+ }
47
+
48
+ function looksLikePath(text: string): boolean {
49
+ if (/\s/.test(text)) return false;
50
+ return text.includes("/") || text.includes(".") || /^[a-zA-Z0-9_-]+$/.test(text);
51
+ }
52
+
53
+ function parseJsonArray(value: string): unknown[] | null {
54
+ const trimmed = value.trim();
55
+ if (!trimmed.startsWith("[")) return null;
56
+ try {
57
+ const parsed = JSON.parse(trimmed);
58
+ return Array.isArray(parsed) ? parsed : null;
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ function isEmptyObject(value: unknown): boolean {
65
+ return (
66
+ typeof value === "object" &&
67
+ value !== null &&
68
+ !Array.isArray(value) &&
69
+ Object.keys(value as Record<string, unknown>).length === 0
70
+ );
71
+ }
72
+
19
73
  export interface RepairResult {
20
74
  toolName: string;
21
75
  repairs: string[];
@@ -31,39 +85,58 @@ export function repairToolInput(
31
85
  input: Record<string, unknown>,
32
86
  ): RepairResult {
33
87
  const repairs: string[] = [];
34
- const isBuiltin = ["bash", "read", "edit", "write", "grep", "find", "ls"].includes(
35
- toolName,
36
- );
37
- if (isBuiltin || !input || typeof input !== "object") {
88
+ if (CORE_TOOLS.has(toolName) || !input || typeof input !== "object") {
38
89
  return { toolName, repairs };
39
90
  }
40
91
 
92
+ if (typeof input.path !== "string" || !input.path) {
93
+ for (const alias of PATH_ALIASES) {
94
+ const value = input[alias];
95
+ if (typeof value === "string" && looksLikePath(value)) {
96
+ input.path = value;
97
+ delete input[alias];
98
+ repairs.push(`path-alias:${alias}`);
99
+ break;
100
+ }
101
+ }
102
+ }
103
+
41
104
  for (const key of Object.keys(input)) {
42
105
  const value = input[key];
43
- // null-for-optional: model stuurde null, schema wil afwezig
44
106
  if (value === null) {
45
107
  delete input[key];
46
108
  repairs.push(`null-for-optional:${key}`);
47
109
  continue;
48
110
  }
49
- // markdown auto-link op pad-achtige velden: [x.md](http://x.md) → x.md
50
111
  if (typeof value === "string") {
51
112
  const m = AUTO_LINK_RE.exec(value);
52
113
  if (m && m[1] && isDegenerateAutoLink(m[1], m[3] ?? "")) {
53
114
  input[key] = m[1];
54
115
  repairs.push(`auto-link-unwrap:${key}`);
55
116
  }
117
+ const current = input[key];
118
+ if (typeof current === "string") {
119
+ const parsed = parseJsonArray(current);
120
+ if (parsed) {
121
+ input[key] = parsed;
122
+ repairs.push(`json-string-array:${key}`);
123
+ }
124
+ }
125
+ }
126
+ const afterParse = input[key];
127
+ if (isArrayKey(key) && isEmptyObject(afterParse)) {
128
+ input[key] = [];
129
+ repairs.push(`empty-object-placeholder:${key}`);
130
+ continue;
131
+ }
132
+ if (isArrayKey(key) && typeof afterParse === "string") {
133
+ input[key] = [afterParse];
134
+ repairs.push(`bare-string-wrap:${key}`);
56
135
  }
57
136
  }
58
137
  return { toolName, repairs };
59
138
  }
60
139
 
61
- function looksLikePath(text: string): boolean {
62
- if (/\s/.test(text)) return false;
63
- return text.includes("/") || text.includes(".") || /^[a-zA-Z0-9_-]+$/.test(text);
64
- }
65
- void looksLikePath;
66
-
67
140
  /** Repair-teller per (tool, repair) — zichtbaar via /repairs. */
68
141
  const repairCounts = new Map<string, number>();
69
142
 
@@ -7,7 +7,6 @@ import { readSettings } from "../settings/settings-io.ts";
7
7
  import { registerSessionLifecycle } from "./session-lifecycle.ts";
8
8
  import { registerCommands } from "../commands/commands.ts";
9
9
  import { setupInlineInvocation } from "../skills/inline-invocation.ts";
10
- import { setupHooks } from "../hooks/index.ts";
11
10
  import {
12
11
  config,
13
12
  createRuntimeState,
@@ -22,9 +21,9 @@ export default function powerlineFooter(pi: ExtensionAPI) {
22
21
  registerCustomPresets(config.presets);
23
22
 
24
23
  const rt = createRuntimeState(startupSettings);
24
+ rt.queueStore.setSentRetentionMs(config.queue.retentionHours * 60 * 60 * 1000);
25
25
 
26
26
  registerSessionLifecycle(pi, rt);
27
27
  registerCommands(pi, rt);
28
28
  setupInlineInvocation(pi, rt);
29
- setupHooks(pi, rt, process.cwd());
30
29
  }
@@ -0,0 +1,36 @@
1
+ import type { CostCurrencyCode } from "../../usage/rates.ts";
2
+ import { formatUsdCost } from "../../usage/rates.ts";
3
+
4
+ /**
5
+ * Decide whether the once-per-session cost alert should fire. Pure and
6
+ * side-effect free so it can be unit-tested without a running session.
7
+ */
8
+ export function shouldTriggerCostAlert(options: {
9
+ totalCost: number;
10
+ threshold: number | null | undefined;
11
+ alreadyNotified: boolean;
12
+ }): boolean {
13
+ const { totalCost, threshold, alreadyNotified } = options;
14
+ if (alreadyNotified) return false;
15
+ if (
16
+ typeof threshold !== "number" ||
17
+ !Number.isFinite(threshold) ||
18
+ threshold <= 0
19
+ ) {
20
+ return false;
21
+ }
22
+ return totalCost >= threshold;
23
+ }
24
+
25
+ /** Format the one-line warning shown when the session crosses the threshold. */
26
+ export function formatCostAlertMessage(
27
+ totalCostUsd: number,
28
+ thresholdUsd: number,
29
+ currency: CostCurrencyCode = "USD",
30
+ ): string {
31
+ const total =
32
+ formatUsdCost(totalCostUsd, currency) ?? `$${totalCostUsd.toFixed(2)}`;
33
+ const threshold =
34
+ formatUsdCost(thresholdUsd, currency) ?? `$${thresholdUsd.toFixed(2)}`;
35
+ return `Session cost reached ${total} (alert threshold ${threshold})`;
36
+ }
@@ -15,10 +15,26 @@ import {
15
15
  onVibeToolCall,
16
16
  } from "../../working-vibes/index.ts";
17
17
  import {
18
+ getSessionTotalCost,
18
19
  getUsageTokenTotal,
19
- hasSessionAssistantUsage,
20
20
  isSessionAssistantMessage,
21
21
  } from "../../usage/ledger.ts";
22
+ import {
23
+ formatCostAlertMessage,
24
+ shouldTriggerCostAlert,
25
+ } from "./cost-alert.ts";
26
+ import {
27
+ formatTokenBudgetWarning,
28
+ parseTokenBudget,
29
+ tokenBudgetLevel,
30
+ } from "../../usage/token-budget.ts";
31
+ import {
32
+ recordUsageEvent,
33
+ loadUsageFileFromDisk,
34
+ tokenTotal,
35
+ totalsForRange,
36
+ dayKey,
37
+ } from "../../usage/usage-store.ts";
22
38
  import {
23
39
  detectCustomCompactionEnabled,
24
40
  readSettings,
@@ -59,6 +75,52 @@ import {
59
75
  scheduleDismissWelcome,
60
76
  } from "../welcome/welcome-control.ts";
61
77
 
78
+ /**
79
+ * Fire the configured `powerline.costAlert` warning at most once per session.
80
+ * Reads the running cost from the (cached) token ledger so repeated calls are
81
+ * cheap; a UI-less or already-notified session short-circuits immediately.
82
+ */
83
+ function maybeNotifyCostAlert(rt: RuntimeState, ctx: any): void {
84
+ if (!ctx?.hasUI || rt.costAlertNotified) return;
85
+ const threshold = config.costAlert;
86
+ const sessionEvents = rt.sessionBranchCache.get(ctx.sessionManager);
87
+ const totalCost = getSessionTotalCost(rt.tokenStatsCache.get(sessionEvents));
88
+ if (
89
+ !shouldTriggerCostAlert({
90
+ totalCost,
91
+ threshold,
92
+ alreadyNotified: rt.costAlertNotified,
93
+ })
94
+ ) {
95
+ return;
96
+ }
97
+ rt.costAlertNotified = true;
98
+ ctx.ui.notify(
99
+ formatCostAlertMessage(
100
+ totalCost,
101
+ threshold as number,
102
+ config.segmentOptions?.cost?.currency ?? "USD",
103
+ ),
104
+ "warning",
105
+ );
106
+ }
107
+
108
+ function maybeNotifyTokenBudget(rt: RuntimeState, ctx: any): void {
109
+ if (!ctx?.hasUI) return;
110
+ const daily = parseTokenBudget(readSettings(ctx.cwd ?? process.cwd()).wishcraft)
111
+ .daily;
112
+ if (!daily) return;
113
+ const now = Date.now();
114
+ const todayStart = Date.parse(`${dayKey(now)}T00:00:00`);
115
+ const used = tokenTotal(
116
+ totalsForRange(loadUsageFileFromDisk(), todayStart, now + 1),
117
+ );
118
+ const { level } = tokenBudgetLevel(used, daily);
119
+ if (level === 0 || level <= rt.tokenBudgetNotifiedLevel) return;
120
+ rt.tokenBudgetNotifiedLevel = level;
121
+ ctx.ui.notify(formatTokenBudgetWarning(used, daily, level), "warning");
122
+ }
123
+
62
124
  // Helper to extract recent agent response text (skipping thinking blocks)
63
125
  function getRecentAgentContext(ctx: any): string | undefined {
64
126
  const sessionEvents = ctx.sessionManager?.getBranch?.() ?? [];
@@ -107,6 +169,8 @@ export function registerSessionLifecycle(
107
169
  rt.lastUserPrompt = "";
108
170
  rt.isStreaming = false;
109
171
  rt.liveAssistantUsage = null;
172
+ rt.costAlertNotified = false;
173
+ rt.tokenBudgetNotifiedLevel = 0;
110
174
  rt.powerlineCompacting = false;
111
175
  rt.deliverAfterRetrySettles = false;
112
176
  rt.stashedEditorText = null;
@@ -116,6 +180,9 @@ export function registerSessionLifecycle(
116
180
  rt.bashModeSettings = parseBashModeSettings(settings, rt.resolvedShortcuts);
117
181
  rt.showLastPrompt = settings.showLastPrompt !== false;
118
182
  setConfig(parsePowerlineConfig(settings.powerline, PRESET_NAMES));
183
+ rt.queueStore.setSentRetentionMs(
184
+ config.queue.retentionHours * 60 * 60 * 1000,
185
+ );
119
186
  registerCustomSegments(config.segments);
120
187
  registerCustomPresets(config.presets);
121
188
  warnInvalidSegmentSettings(ctx);
@@ -154,6 +221,7 @@ export function registerSessionLifecycle(
154
221
  } else {
155
222
  dismissWelcome(rt, ctx);
156
223
  }
224
+ maybeNotifyTokenBudget(rt, ctx);
157
225
  }
158
226
  });
159
227
 
@@ -271,9 +339,21 @@ export function registerSessionLifecycle(
271
339
  rt.liveAssistantUsage = null;
272
340
  } else if (getUsageTokenTotal(event.message.usage) > 0) {
273
341
  rt.liveAssistantUsage = event.message.usage;
342
+ const usage = event.message.usage;
343
+ recordUsageEvent({
344
+ at: Date.now(),
345
+ model: ctx.model?.id ?? ctx.model?.name,
346
+ input: usage.input,
347
+ output: usage.output,
348
+ cacheRead: usage.cacheRead,
349
+ cacheWrite: usage.cacheWrite,
350
+ cost: usage.cost.total,
351
+ });
274
352
  }
275
353
  }
276
354
  requestImmediateStatusRender(rt, { deferDuringTyping: false });
355
+ maybeNotifyCostAlert(rt, ctx);
356
+ maybeNotifyTokenBudget(rt, ctx);
277
357
  });
278
358
 
279
359
  pi.on("turn_end", async (_event, ctx) => {
@@ -361,6 +441,7 @@ export function registerSessionLifecycle(
361
441
  );
362
442
  }
363
443
  }
444
+ maybeNotifyCostAlert(rt, ctx);
364
445
  }
365
446
  } catch (error) {
366
447
  if (!isStaleExtensionContextError(error)) throw error;
@@ -1,9 +1,13 @@
1
1
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
2
2
  import { join, dirname } from "node:path";
3
3
 
4
- import type { StatusLinePreset } from "../../config/types.ts";
4
+ import type {
5
+ CustomPresetConfig,
6
+ StatusLinePreset,
7
+ } from "../../config/types.ts";
5
8
  import type { PowerlineConfig } from "../../config/powerline-config.ts";
6
9
  import {
10
+ nextPowerlineSettingWithCustomPreset,
7
11
  nextPowerlineSettingWithOptions,
8
12
  nextPowerlineSettingWithPreset,
9
13
  } from "../../config/powerline-config.ts";
@@ -182,21 +186,6 @@ export function writePowerlineSetting(
182
186
  return writeSettingKey(cwd, "powerline", update);
183
187
  }
184
188
 
185
- export function writePowerlineDisabledSegmentSetting(
186
- disabledSegments: string[],
187
- cwd: string = process.cwd(),
188
- ): boolean {
189
- return writePowerlineSetting(cwd, (existing) => {
190
- // Shorthand string (bv. "chef") is een preset-naam: bewaar 'm.
191
- const base = isRecord(existing)
192
- ? existing
193
- : typeof existing === "string"
194
- ? { preset: existing }
195
- : {};
196
- return { ...base, disabledSegments };
197
- });
198
- }
199
-
200
189
  export function writePowerlinePresetSetting(
201
190
  preset: StatusLinePreset,
202
191
  cwd: string = process.cwd(),
@@ -206,10 +195,27 @@ export function writePowerlinePresetSetting(
206
195
  );
207
196
  }
208
197
 
198
+ export function writePowerlineCustomPresetSetting(
199
+ cwd: string,
200
+ name: string,
201
+ presetDef: CustomPresetConfig,
202
+ ): boolean {
203
+ return writePowerlineSetting(cwd, (existingPowerlineSetting) =>
204
+ nextPowerlineSettingWithCustomPreset(
205
+ existingPowerlineSetting,
206
+ name,
207
+ presetDef,
208
+ ),
209
+ );
210
+ }
211
+
209
212
  export function writePowerlineOptionSetting(
210
213
  cwd: string,
211
214
  updates: Partial<
212
- Pick<PowerlineConfig, "welcome" | "stashSharpSShortcut" | "placement">
215
+ Pick<
216
+ PowerlineConfig,
217
+ "welcome" | "stashSharpSShortcut" | "placement" | "disabledSegments"
218
+ >
213
219
  >,
214
220
  currentPreset: StatusLinePreset,
215
221
  ): boolean {
@@ -145,7 +145,8 @@ export function buildConfigGroups(settings: Record<string, unknown>): ConfigGrou
145
145
  title: "Hooks & repairs (harness-laag)",
146
146
  items: [
147
147
  { label: "Hooks ingeschakeld", path: "wishcraft.hooksEnabled", kind: "toggle", hint: "commandcode-achtige preToolUse/postToolUse/sessionStart hooks" },
148
- { label: "Tool-input repairs", path: "wishcraft.repairsEnabled", kind: "toggle", hint: "null-for-optional, auto-link unwrap op custom tools" },
148
+ { label: "Tool-input repairs", path: "wishcraft.repairsEnabled", kind: "toggle", hint: "null-for-optional, auto-link, json-array, path aliases" },
149
+ { label: "Dagelijks tokenbudget", path: "wishcraft.tokenBudget.daily", kind: "number", hint: "kleurt cost-segment; blokkeert nooit. 0 = uit" },
149
150
  ],
150
151
  },
151
152
  {
@@ -348,10 +348,15 @@ export function parseBashModeSettings(
348
348
  Number.isFinite(raw.transcriptMaxBytes)
349
349
  ? Math.max(16 * 1024, Math.floor(raw.transcriptMaxBytes))
350
350
  : DEFAULT_BASH_MODE_SETTINGS.transcriptMaxBytes;
351
+ const initScript =
352
+ typeof raw.initScript === "string" && raw.initScript.trim()
353
+ ? raw.initScript
354
+ : null;
351
355
 
352
356
  return {
353
357
  toggleShortcut,
354
358
  transcriptMaxLines,
355
359
  transcriptMaxBytes,
360
+ initScript,
356
361
  };
357
362
  }
@@ -61,7 +61,7 @@ export function computeResponsiveLayout(
61
61
  // Get all segments: primary first, then secondary
62
62
  const mergedSegments = mergeSegmentsWithCustomItems(
63
63
  presetDef,
64
- config.customItems,
64
+ ctx.effectiveCustomItems,
65
65
  {
66
66
  layout: config.layout,
67
67
  disabledSegments: config.disabledSegments,