@groeponline/pi-wishcraft 1.24.0 → 1.25.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 CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.25.0] - 2026-11-12
6
+
7
+ ### Added
8
+ - **The extension now activates cleanly under `pi --mode rpc`, with a host integration test to keep it that way.** `ctx.hasUI` is `true` in rpc mode, so every TUI-only call used to pass its gate and then die silently — or worse: `ctx.ui.custom()` returns `undefined` in rpc, and the welcome overlay's `.catch` on that `undefined` threw an uncaught TypeError on every rpc `session_start`. The session bootstrap now branches on `ctx.mode`: under `tui` nothing changes; under `rpc` the six powerline widgets are exported as `string[]` widget lines (the only form that crosses the rpc UI subprotocol), refreshed on the same scheduler cadence as a TUI repaint, and the welcome overlay degrades to a single `notify`. `tests/pi-host-rpc.test.ts` boots the real pi host over JSONL — LF-framed, both pipes drained — and asserts every slash command registers, the `powerline-*` widgets and statuses arrive as `extension_ui_request` records, and not a single non-JSON line ever reaches stdout. Diagnostics moved from `console.debug` (stdout — protocol corruption) to a `debugLog` helper on stderr.
9
+
5
10
  ## [1.24.0] - 2026-11-12
6
11
 
7
12
  ### Fixed
@@ -9,7 +14,6 @@
9
14
  - **Think levels high/xhigh/max got their rainbow back.** The thinking segment collapsed all three into the plain `thinking` color; it now uses dedicated `thinkingHigh`/`thinkingXhigh`/`thinkingMax` semantic colors (default `rainbow`, overridable per preset — the `rainbow` ColorValue itself is new).
10
15
  - **Token totals prefer the provider-reported `totalTokens`.** The four-field sum undercounted providers that report reasoning/other tokens only in the total.
11
16
 
12
-
13
17
  ## [1.23.0] - 2026-11-08
14
18
 
15
19
  ### Fixed
@@ -30,6 +30,7 @@ import type {
30
30
  BashModeEditorOptions,
31
31
  GhostSuggestion,
32
32
  } from "./types.ts";
33
+ import { debugLog } from "../src/extension/core/debug-log.ts";
33
34
 
34
35
  const BRACKETED_PASTE_START = "\x1b[200~";
35
36
  const BRACKETED_PASTE_END = "\x1b[201~";
@@ -463,7 +464,7 @@ export class BashModeEditor extends CustomEditor {
463
464
  })
464
465
  .catch((error) => {
465
466
  if (controller.signal.aborted || (error instanceof Error && error.message === "aborted")) return;
466
- console.debug("[wishcraft] Failed to resolve bash ghost suggestion:", error);
467
+ debugLog("[wishcraft] Failed to resolve bash ghost suggestion:", error);
467
468
  });
468
469
  }
469
470
  }
@@ -7,6 +7,7 @@ import {
7
7
  } from "node:fs";
8
8
  import { dirname, join } from "node:path";
9
9
  import { getAgentPath, getHomeDir } from "../src/paths/agent-dirs.ts";
10
+ import { debugLog } from "../src/extension/core/debug-log.ts";
10
11
 
11
12
  interface PersistedHistoryEntry {
12
13
  command: string;
@@ -108,7 +109,7 @@ export function readProjectHistory(cwd: string): PersistedHistoryEntry[] {
108
109
  } catch (error) {
109
110
  // Project history is a best-effort cache. If it is unreadable or malformed,
110
111
  // bash mode should keep working instead of failing command entry entirely.
111
- console.debug(
112
+ debugLog(
112
113
  `[wishcraft] Failed to read bash project history from ${filePath}:`,
113
114
  error,
114
115
  );
@@ -150,7 +151,7 @@ export function appendProjectHistory(
150
151
  });
151
152
  } catch (error) {
152
153
  // History persistence should never block a successful shell command from completing.
153
- console.debug(
154
+ debugLog(
154
155
  `[wishcraft] Failed to persist bash project history to ${filePath}:`,
155
156
  error,
156
157
  );
@@ -233,7 +234,7 @@ export function readGlobalShellHistory(shellPath: string): string[] {
233
234
  if (stat && (errorCode === "EACCES" || errorCode === "EPERM")) {
234
235
  globalHistoryCache.set(cacheKey, { ...stat, entries: [] });
235
236
  }
236
- console.debug(
237
+ debugLog(
237
238
  `[wishcraft] Failed to read global shell history for ${shellName}:`,
238
239
  error,
239
240
  );
package/build-stamp.json CHANGED
@@ -1 +1 @@
1
- {"source_sha":"1e85fa41738909a1dd3d098ad8ef0986ae76a298"}
1
+ {"source_sha":"c5a4ed154027696731b82238816c26fe1ce76945"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groeponline/pi-wishcraft",
3
- "version": "1.24.0",
3
+ "version": "1.25.0",
4
4
  "description": "Pi coding agent extension: operator cockpit with a live powerline status bar, idea queue, skill search, sticky Bash, hooks, policy guardrails, and TUI session UX.",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.4.2",
@@ -73,7 +73,8 @@
73
73
  "audit": "bun scripts/audit-gate.mjs",
74
74
  "preview": "bun scripts/preview.mjs",
75
75
  "verify:pi-package": "bun scripts/verify-pi-package-contract.mjs",
76
- "hooks:install": "git config core.hooksPath .githooks"
76
+ "hooks:install": "git config core.hooksPath .githooks",
77
+ "test:integration": "node --experimental-strip-types --test tests/pi-host-rpc.test.ts"
77
78
  },
78
79
  "peerDependencies": {
79
80
  "@earendil-works/pi-ai": "*",
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Diagnostics go to stderr, never stdout. In `pi --mode rpc` stdout is the
3
+ * JSONL protocol channel — a `console.debug` line there corrupts framing for
4
+ * the host. Use this instead of `console.debug` anywhere the extension can
5
+ * run under an rpc host.
6
+ */
7
+ export function debugLog(message: string, ...args: unknown[]): void {
8
+ console.error(message, ...args);
9
+ }
@@ -105,6 +105,7 @@ export function createRuntimeState(
105
105
  liveAssistantUsage: null,
106
106
  isStreaming: false,
107
107
  tuiRef: null,
108
+ rpcWidgetRefresh: null,
108
109
  restoreFooterStatusRepaintHook: null,
109
110
  stashShortcutInputUnsubscribe: null,
110
111
  dismissWelcomeOverlay: null,
@@ -165,6 +166,7 @@ export function createRuntimeState(
165
166
  }
166
167
 
167
168
  rt.tuiRef?.requestRender();
169
+ rt.rpcWidgetRefresh?.();
168
170
  }, STATUS_RENDER_DEBOUNCE_MS);
169
171
 
170
172
  rt.motionScheduler = new MotionScheduler({
@@ -172,6 +174,7 @@ export function createRuntimeState(
172
174
  // Motion only changes the Signal rail; segment data remains cached.
173
175
  rt.lastLayoutResult = null;
174
176
  rt.tuiRef?.requestRender();
177
+ rt.rpcWidgetRefresh?.();
175
178
  },
176
179
  });
177
180
 
@@ -55,6 +55,8 @@ export interface RuntimeState {
55
55
  liveAssistantUsage: SessionAssistantUsage | null;
56
56
  isStreaming: boolean;
57
57
  tuiRef: any;
58
+ /** Pushes fresh widget string[] over the rpc UI subprotocol; null under tui. */
59
+ rpcWidgetRefresh: (() => void) | null;
58
60
  restoreFooterStatusRepaintHook: (() => void) | null;
59
61
  stashShortcutInputUnsubscribe: (() => void) | null;
60
62
  dismissWelcomeOverlay: (() => void) | null;
@@ -7,6 +7,7 @@ import { getAgentPath } from "../../paths/agent-dirs.ts";
7
7
  import { isRecord } from "../settings/settings-io.ts";
8
8
  import { hasNonWhitespaceText } from "./prompt-history.ts";
9
9
  import { STASH_HISTORY_LIMIT } from "../core/constants.ts";
10
+ import { debugLog } from "../core/debug-log.ts";
10
11
 
11
12
  export function getStashHistoryPath(): string {
12
13
  return getAgentPath("powerline-footer", "stash-history.json");
@@ -50,7 +51,7 @@ export function readPersistedStashHistory(): string[] {
50
51
 
51
52
  const parsed = JSON.parse(readFileSync(stashHistoryPath, "utf-8"));
52
53
  if (!isRecord(parsed)) {
53
- console.debug(
54
+ debugLog(
54
55
  `[wishcraft] Ignoring invalid stash history at ${stashHistoryPath}`,
55
56
  );
56
57
  return [];
@@ -58,7 +59,7 @@ export function readPersistedStashHistory(): string[] {
58
59
 
59
60
  return normalizeStashHistoryEntries(parsed.history);
60
61
  } catch (error) {
61
- console.debug(
62
+ debugLog(
62
63
  `[wishcraft] Failed to read stash history from ${stashHistoryPath}:`,
63
64
  error,
64
65
  );
@@ -77,7 +78,7 @@ export function persistStashHistory(history: string[]): void {
77
78
  mkdirSync(dirname(stashHistoryPath), { recursive: true });
78
79
  writeFileSync(stashHistoryPath, JSON.stringify(payload, null, 2) + "\n");
79
80
  } catch (error) {
80
- console.debug(
81
+ debugLog(
81
82
  `[wishcraft] Failed to persist stash history to ${stashHistoryPath}:`,
82
83
  error,
83
84
  );
@@ -44,6 +44,7 @@ import {
44
44
  finishFailedCompaction,
45
45
  } from "../queue/queue-integration.ts";
46
46
  import { setupCustomEditor } from "../ui/custom-editor.ts";
47
+ import { disposeRpcWidgets, installRpcWidgets } from "../ui/rpc-widgets.ts";
47
48
  import {
48
49
  setupWelcomeHeader,
49
50
  setupWelcomeOverlay,
@@ -173,15 +174,28 @@ export function registerSessionLifecycle(
173
174
  initVibeManager(ctx);
174
175
 
175
176
  if (rt.enabled && ctx.hasUI) {
176
- setupCustomEditor(pi, rt, ctx);
177
- if (shouldShowStartupWelcome(event.reason, config.welcome)) {
178
- if (settings.quietStartup === true) {
179
- setupWelcomeHeader(rt, ctx);
180
- } else {
181
- setupWelcomeOverlay(rt, ctx);
177
+ if (ctx.mode === "rpc") {
178
+ // In rpc mode component factories, custom overlays, the editor and
179
+ // footer/header are all no-ops; widgets only cross the wire as
180
+ // string[] lines. `ctx.hasUI` is still true, so gate on the mode.
181
+ installRpcWidgets(rt, ctx);
182
+ if (shouldShowStartupWelcome(event.reason, config.welcome)) {
183
+ ctx.ui.notify(
184
+ `Wishcraft cockpit active (preset: ${config.preset})`,
185
+ "info",
186
+ );
182
187
  }
183
188
  } else {
184
- dismissWelcome(rt, ctx);
189
+ setupCustomEditor(pi, rt, ctx);
190
+ if (shouldShowStartupWelcome(event.reason, config.welcome)) {
191
+ if (settings.quietStartup === true) {
192
+ setupWelcomeHeader(rt, ctx);
193
+ } else {
194
+ setupWelcomeOverlay(rt, ctx);
195
+ }
196
+ } else {
197
+ dismissWelcome(rt, ctx);
198
+ }
185
199
  }
186
200
  maybeNotifyTokenBudget(rt, ctx, settings);
187
201
  }
@@ -217,6 +231,8 @@ export function registerSessionLifecycle(
217
231
  rt.liveAssistantUsage = null;
218
232
  rt.tokenBudgetSnapshot = { day: "", dailyLimit: null, dailyUsed: 0 };
219
233
  rt.tuiRef = null;
234
+ rt.rpcWidgetRefresh = null;
235
+ disposeRpcWidgets();
220
236
  rt.currentEditor = null;
221
237
  resetLayoutCache(rt);
222
238
  });
@@ -12,6 +12,7 @@ import {
12
12
  nextPowerlineSettingWithPreset,
13
13
  } from "../../config/powerline-config.ts";
14
14
  import { getAgentPath } from "../../paths/agent-dirs.ts";
15
+ import { debugLog } from "../core/debug-log.ts";
15
16
 
16
17
  export function isRecord(value: unknown): value is Record<string, unknown> {
17
18
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -60,7 +61,7 @@ export function readSettingsFile(
60
61
 
61
62
  const parsed = JSON.parse(readFileSync(settingsPath, "utf-8"));
62
63
  if (!isRecord(parsed)) {
63
- console.debug(
64
+ debugLog(
64
65
  `[wishcraft] Ignoring non-object settings at ${settingsPath}`,
65
66
  );
66
67
  return {};
@@ -70,7 +71,7 @@ export function readSettingsFile(
70
71
  } catch (error) {
71
72
  // Settings are user-edited input. Log and keep the extension running with defaults
72
73
  // instead of crashing the UI during startup.
73
- console.debug(
74
+ debugLog(
74
75
  `[wishcraft] Failed to read settings from ${settingsPath}:`,
75
76
  error,
76
77
  );
@@ -88,7 +89,7 @@ export function readWritableSettingsFile(
88
89
  try {
89
90
  const parsed = JSON.parse(readFileSync(settingsPath, "utf-8"));
90
91
  if (!isRecord(parsed)) {
91
- console.debug(
92
+ debugLog(
92
93
  `[wishcraft] Refusing to write settings to non-object file at ${settingsPath}`,
93
94
  );
94
95
  return null;
@@ -98,7 +99,7 @@ export function readWritableSettingsFile(
98
99
  } catch (error) {
99
100
  // Do not overwrite malformed user settings with partial data. Surface the failure
100
101
  // through the command handler so the user can fix the file intentionally.
101
- console.debug(
102
+ debugLog(
102
103
  `[wishcraft] Failed to parse settings at ${settingsPath}:`,
103
104
  error,
104
105
  );
@@ -115,7 +116,7 @@ export function readCompactionPolicyEnabled(
115
116
  if (!isRecord(parsed) || typeof parsed.enabled !== "boolean") return false;
116
117
  return parsed.enabled;
117
118
  } catch (error) {
118
- console.debug(
119
+ debugLog(
119
120
  `[wishcraft] Failed to read compaction policy from ${configPath}:`,
120
121
  error,
121
122
  );
@@ -171,7 +172,7 @@ export function writeSettingKey(
171
172
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
172
173
  return true;
173
174
  } catch (error) {
174
- console.debug(
175
+ debugLog(
175
176
  `[wishcraft] Failed to persist setting '${key}' to ${settingsPath}:`,
176
177
  error,
177
178
  );
@@ -17,6 +17,7 @@ import type {
17
17
  PowerlineShortcuts,
18
18
  ShortcutBinding,
19
19
  } from "../core/types.ts";
20
+ import { debugLog } from "../core/debug-log.ts";
20
21
 
21
22
  const APP_RESERVED_SHORTCUTS = [
22
23
  "escape",
@@ -283,13 +284,13 @@ export function resolveShortcutConfig(
283
284
 
284
285
  const replacement = findShortcutReplacement(key, used);
285
286
  if (!replacement) {
286
- console.debug(
287
+ debugLog(
287
288
  `[wishcraft] Shortcut conflict for ${key}: "${configured}" is already in use`,
288
289
  );
289
290
  continue;
290
291
  }
291
292
 
292
- console.debug(
293
+ debugLog(
293
294
  `[wishcraft] Shortcut conflict for ${key}: "${configured}" replaced with "${replacement}"`,
294
295
  );
295
296
 
@@ -334,7 +335,7 @@ export function parseBashModeSettings(
334
335
  : fallbackToggleShortcut;
335
336
 
336
337
  if (configuredToggleShortcut && toggleShortcut !== configuredToggleShortcut) {
337
- console.debug(
338
+ debugLog(
338
339
  `[wishcraft] Bash mode shortcut conflict: "${configuredToggleShortcut}" replaced with "${toggleShortcut ?? "disabled"}"`,
339
340
  );
340
341
  }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * RPC surface for the powerline. In `pi --mode rpc` `ctx.ui.setWidget`
3
+ * ignores component factories — only the `string[]` form crosses the JSONL
4
+ * wire as `extension_ui_request` records. This adapter renders the same line
5
+ * producers the TUI widgets use and pushes the results as widget lines,
6
+ * refreshed on the same cadence as a TUI repaint.
7
+ *
8
+ * The TUI footer also hands the extension a ReadonlyFooterDataProvider —
9
+ * git branch, extension statuses, branch-change events. In rpc mode the
10
+ * host never builds a footer, so `rt.footerDataRef` stays null and every
11
+ * consumer would render empty. We therefore install our own provider: the
12
+ * branch comes from the shared git-status cache, statuses are mirrored
13
+ * through a wrapped `ctx.ui.setStatus`, and branch changes bridge onto the
14
+ * git-update bus like the footer does.
15
+ */
16
+
17
+ import type { Theme } from "@earendil-works/pi-coding-agent";
18
+ import type { ReadonlyFooterDataProvider } from "@earendil-works/pi-coding-agent";
19
+
20
+ import { getCurrentBranch, subscribeGitUpdates } from "../../git/status.ts";
21
+ import { subscribePortsUpdates } from "../../segments/ports.ts";
22
+ import { subscribeCustomSegmentUpdates } from "../../segments/custom.ts";
23
+ import { debugLog } from "../core/debug-log.ts";
24
+ import { requestStatusRender } from "../core/segment-context.ts";
25
+ import { config } from "../core/state.ts";
26
+ import type { RuntimeState } from "../core/types.ts";
27
+ import {
28
+ renderBashTranscriptLines,
29
+ renderLastPromptLines,
30
+ renderPowerlinePrimaryLines,
31
+ renderPowerlineQueuePreviewLines,
32
+ renderPowerlineSecondaryLines,
33
+ renderPowerlineStatusLines,
34
+ } from "./status-line-renderers.ts";
35
+
36
+ /** rpc hosts have no tty; pick a stable line width for the exported lines. */
37
+ const RPC_WIDGET_WIDTH = 80;
38
+
39
+ interface RpcUi {
40
+ theme: Theme;
41
+ setWidget(
42
+ key: string,
43
+ lines: string[],
44
+ options?: { placement?: "aboveEditor" | "belowEditor" },
45
+ ): void;
46
+ setStatus(key: string, text: string | undefined): void;
47
+ }
48
+
49
+ interface RpcCtx {
50
+ ui: RpcUi;
51
+ }
52
+
53
+ type WidgetRenderer = (rt: RuntimeState, width: number, theme: Theme) => string[];
54
+
55
+ function rpcWidgets(): Readonly<
56
+ {
57
+ key: string;
58
+ placement: "aboveEditor" | "belowEditor";
59
+ render: WidgetRenderer;
60
+ }[]
61
+ > {
62
+ return [
63
+ {
64
+ key: "powerline-status",
65
+ placement: "aboveEditor",
66
+ render: (rt, width) => renderPowerlineStatusLines(rt, width),
67
+ },
68
+ {
69
+ key: "powerline-top",
70
+ placement: config.placement === "below" ? "belowEditor" : "aboveEditor",
71
+ render: (rt, width, theme) => renderPowerlinePrimaryLines(rt, width, theme),
72
+ },
73
+ {
74
+ key: "powerline-secondary",
75
+ placement: "belowEditor",
76
+ render: (rt, width, theme) =>
77
+ renderPowerlineSecondaryLines(rt, width, theme),
78
+ },
79
+ {
80
+ key: "powerline-bash-transcript",
81
+ placement: "belowEditor",
82
+ render: (rt, width, theme) =>
83
+ renderBashTranscriptLines(rt, width, theme),
84
+ },
85
+ {
86
+ key: "powerline-queue-preview",
87
+ placement: "belowEditor",
88
+ render: (rt, width, theme) =>
89
+ renderPowerlineQueuePreviewLines(rt, width, theme),
90
+ },
91
+ {
92
+ key: "powerline-last-prompt",
93
+ placement: "belowEditor",
94
+ render: (rt, width) => renderLastPromptLines(rt, width),
95
+ },
96
+ ];
97
+ }
98
+
99
+ function widgetWidth(): number {
100
+ const columns = process.stdout.columns;
101
+ return typeof columns === "number" && columns > 0 ? columns : RPC_WIDGET_WIDTH;
102
+ }
103
+
104
+ let disposeSessionResources: (() => void) | null = null;
105
+
106
+ /**
107
+ * Register every powerline widget in its `string[]` form and keep the lines
108
+ * current via `rt.rpcWidgetRefresh`, driven by the status render scheduler
109
+ * plus the same async-segment subscriptions the TUI footer installs.
110
+ */
111
+ export function installRpcWidgets(rt: RuntimeState, ctx: RpcCtx): void {
112
+ disposeSessionResources?.();
113
+
114
+ // Mirror setStatus so segments/status lines can read extension statuses in
115
+ // rpc — the host keeps them on its own (unreachable) footer provider.
116
+ const statusMirror = new Map<string, string>();
117
+ const originalSetStatus = ctx.ui.setStatus.bind(ctx.ui);
118
+ ctx.ui.setStatus = (key, text) => {
119
+ const previous = statusMirror.get(key);
120
+ if (text === undefined) {
121
+ statusMirror.delete(key);
122
+ } else {
123
+ statusMirror.set(key, text);
124
+ }
125
+ originalSetStatus(key, text);
126
+ // The host keeps statuses on a footer it never builds in rpc mode. A change
127
+ // here must wake the same render scheduler the TUI footer would have.
128
+ if (previous !== text) requestStatusRender(rt);
129
+ };
130
+ const restoreSetStatus = () => {
131
+ ctx.ui.setStatus = originalSetStatus;
132
+ };
133
+
134
+ const provider: ReadonlyFooterDataProvider = {
135
+ getGitBranch: () => getCurrentBranch(null),
136
+ getExtensionStatuses: () => statusMirror,
137
+ getAvailableProviderCount: () => 0,
138
+ onBranchChange: (callback) => subscribeGitUpdates(callback),
139
+ };
140
+ rt.footerDataRef = provider;
141
+
142
+ const unsubs = [
143
+ subscribeGitUpdates(() => requestStatusRender(rt)),
144
+ subscribePortsUpdates(() => requestStatusRender(rt)),
145
+ subscribeCustomSegmentUpdates(() => requestStatusRender(rt)),
146
+ ];
147
+
148
+ const theme = ctx.ui.theme;
149
+ const push = () => {
150
+ if (rt.currentCtx == null) return;
151
+ const width = widgetWidth();
152
+ for (const widget of rpcWidgets()) {
153
+ try {
154
+ ctx.ui.setWidget(widget.key, widget.render(rt, width, theme), {
155
+ placement: widget.placement,
156
+ });
157
+ } catch (error) {
158
+ debugLog(`[wishcraft] rpc widget ${widget.key} failed:`, error);
159
+ }
160
+ }
161
+ };
162
+
163
+ rt.rpcWidgetRefresh = () => {
164
+ // Invalidate the composed line only — wiping the session caches per tick
165
+ // would rebuild git/token stats on every refresh.
166
+ rt.lastLayoutResult = null;
167
+ rt.layoutDirty = true;
168
+ push();
169
+ };
170
+
171
+ disposeSessionResources = () => {
172
+ for (const unsub of unsubs) unsub();
173
+ restoreSetStatus();
174
+ rt.rpcWidgetRefresh = null;
175
+ if (rt.footerDataRef === provider) rt.footerDataRef = null;
176
+ disposeSessionResources = null;
177
+ };
178
+
179
+ push();
180
+ }
181
+
182
+ /** Drop the rpc provider, subscriptions and the status mirror. */
183
+ export function disposeRpcWidgets(): void {
184
+ disposeSessionResources?.();
185
+ }
@@ -15,6 +15,7 @@ import { isRecord, readSettings } from "../settings/settings-io.ts";
15
15
  import type { RuntimeState } from "../core/types.ts";
16
16
  import { getQueueContext } from "../queue/queue-context.ts";
17
17
  import { pickNextReviewIdea } from "../queue/idea-review.ts";
18
+ import { debugLog } from "../core/debug-log.ts";
18
19
 
19
20
  /**
20
21
  * Three next steps for an operator who has never run Wishcraft before.
@@ -309,7 +310,7 @@ export function setupWelcomeOverlay(rt: RuntimeState, ctx: any) {
309
310
  },
310
311
  )
311
312
  .catch((error: unknown) => {
312
- console.debug("[wishcraft] Welcome overlay failed:", error);
313
+ debugLog("[wishcraft] Welcome overlay failed:", error);
313
314
  });
314
315
  }, 100);
315
316
  }
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
3
3
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
4
4
  import { SessionManager, type ExtensionAPI, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
5
5
  import type { AutocompleteItem } from "@earendil-works/pi-tui";
6
+ import { debugLog } from "../extension/core/debug-log.ts";
6
7
 
7
8
  function stripMatchingQuotes(value: string): string {
8
9
  const trimmed = value.trim();
@@ -160,7 +161,7 @@ export async function runCdCommand(args: string, ctx: ExtensionCommandContext):
160
161
  nextCtx.ui.notify(`Changed directory to ${nextCtx.cwd}`, "info");
161
162
  nextCtx.ui.setTitle(`pi - ${basename(nextCtx.cwd) || nextCtx.cwd}`);
162
163
  } catch (error) {
163
- console.debug("[wishcraft] Failed to update process cwd after /cd:", error);
164
+ debugLog("[wishcraft] Failed to update process cwd after /cd:", error);
164
165
  }
165
166
  },
166
167
  });
@@ -174,7 +175,7 @@ export async function runCdCommand(args: string, ctx: ExtensionCommandContext):
174
175
  try {
175
176
  ctx.ui.notify(`Failed to change directory: ${message}`, "error");
176
177
  } catch {
177
- console.debug("[wishcraft] Failed to report /cd error:", error);
178
+ debugLog("[wishcraft] Failed to report /cd error:", error);
178
179
  }
179
180
  }
180
181
  }
@@ -9,6 +9,7 @@ import type {
9
9
  SemanticColor,
10
10
  ThemeLike,
11
11
  } from "../config/types.ts";
12
+ import { debugLog } from "../extension/core/debug-log.ts";
12
13
 
13
14
  export interface PowerlineThemeConfig {
14
15
  colors?: unknown;
@@ -134,11 +135,11 @@ export function loadThemeConfig(): PowerlineThemeConfig {
134
135
  }
135
136
  } catch (e: unknown) {
136
137
  if (e instanceof SyntaxError) {
137
- console.debug(`[theme] SyntaxError in ${path}:`, e.message);
138
+ debugLog(`[theme] SyntaxError in ${path}:`, e.message);
138
139
  } else if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
139
140
  // File doesn't exist despite existsSync, ignore
140
141
  } else {
141
- console.debug(`[theme] Unexpected error reading ${path}:`, e);
142
+ debugLog(`[theme] Unexpected error reading ${path}:`, e);
142
143
  }
143
144
  }
144
145
  }
@@ -206,7 +207,7 @@ export function applyColor(
206
207
  if (!loggedErrors.has(errorKey)) {
207
208
  loggedErrors.add(errorKey);
208
209
  if (loggedErrors.size > 100) loggedErrors.clear();
209
- console.debug(`[theme] Invalid color "${errorKey}". Fallback to text.`, e);
210
+ debugLog(`[theme] Invalid color "${errorKey}". Fallback to text.`, e);
210
211
  }
211
212
  return theme.fg("text", text);
212
213
  }
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
2
  import { join, basename } from "node:path";
3
3
  import { getAgentPath, getHomeDir } from "../paths/agent-dirs.ts";
4
4
  import type { LoadedCounts } from "./types.ts";
5
+ import { debugLog } from "../extension/core/debug-log.ts";
5
6
 
6
7
  const loggedDiscoveryErrors = new Set<string>();
7
8
 
@@ -26,7 +27,7 @@ export function logDiscoveryError(scope: string, error: unknown): void {
26
27
  loggedDiscoveryErrors.clear();
27
28
  }
28
29
 
29
- console.debug(`[powerline-welcome] ${scope}:`, error);
30
+ debugLog(`[powerline-welcome] ${scope}:`, error);
30
31
  }
31
32
 
32
33
  function scanContextFiles(homeDir: string, cwd: string): number {
@@ -13,6 +13,7 @@ import {
13
13
  import { dirname, join } from "node:path";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import { getAgentPath } from "../paths/agent-dirs.ts";
16
+ import { debugLog } from "../extension/core/debug-log.ts";
16
17
 
17
18
  const MAX_WHATSNEW_LINES = 8;
18
19
  const WHATS_NEW_STATE_FILE = "whats-new.json";
@@ -159,7 +160,7 @@ function saveLastSeenVersion(version: string): void {
159
160
  "utf8",
160
161
  );
161
162
  } catch (error) {
162
- console.debug("[wishcraft] Failed to persist whats-new state:", error);
163
+ debugLog("[wishcraft] Failed to persist whats-new state:", error);
163
164
  }
164
165
  }
165
166
 
@@ -16,6 +16,7 @@ import {
16
16
  type VibeMode,
17
17
  } from "./storage.ts";
18
18
  import { generateVibe } from "./provider.ts";
19
+ import { debugLog } from "../extension/core/debug-log.ts";
19
20
 
20
21
  function trackRecentVibe(vibe: string): void {
21
22
  // Don't track fallback messages
@@ -67,9 +68,9 @@ async function generateAndUpdate(
67
68
  } catch (error) {
68
69
  // AbortError is expected on timeout/cancel - don't log as error
69
70
  if (error instanceof Error && error.name === "AbortError") {
70
- console.debug("[working-vibes] Generation aborted");
71
+ debugLog("[working-vibes] Generation aborted");
71
72
  } else {
72
- console.debug("[working-vibes] Generation failed:", error);
73
+ debugLog("[working-vibes] Generation failed:", error);
73
74
  }
74
75
  // Fallback already showing, no action needed
75
76
  }
@@ -10,6 +10,7 @@ import { hasNerdFonts } from "../theme/icons.ts";
10
10
  import { nativeWorkingIndicator } from "./frames.ts";
11
11
  import type { NativeIndicatorSpec } from "./frames.ts";
12
12
  import { vibeState } from "./storage.ts";
13
+ import { debugLog } from "../extension/core/debug-log.ts";
13
14
 
14
15
  type NativeIndicatorUi = {
15
16
  setWorkingIndicator?: (options?: { frames?: string[]; intervalMs?: number }) => void;
@@ -58,7 +59,7 @@ export function applyNativeWorkingIndicator(
58
59
  };
59
60
  ui.setWorkingIndicator({ frames: spec.frames.map(paint), intervalMs: spec.intervalMs });
60
61
  } catch (error) {
61
- console.debug("[working-vibes] native indicator failed:", error);
62
+ debugLog("[working-vibes] native indicator failed:", error);
62
63
  }
63
64
  }
64
65
 
@@ -66,6 +67,6 @@ export function resetNativeWorkingIndicator(ui: NativeIndicatorUi | undefined):
66
67
  try {
67
68
  ui?.setWorkingIndicator?.();
68
69
  } catch (error) {
69
- console.debug("[working-vibes] native indicator reset failed:", error);
70
+ debugLog("[working-vibes] native indicator reset failed:", error);
70
71
  }
71
72
  }
@@ -11,6 +11,7 @@ import type {
11
11
  import { normalizeContext } from "@earendil-works/pi-ai";
12
12
  import { requirePeerSymbol } from "../extension/core/peer-guard.ts";
13
13
  import { VIBE_SYSTEM_PROMPT, vibeState } from "./storage.ts";
14
+ import { debugLog } from "../extension/core/debug-log.ts";
14
15
 
15
16
  // ═══════════════════════════════════════════════════════════════════════════
16
17
  // Types
@@ -136,7 +137,7 @@ export async function generateVibe(
136
137
  // Resolve model from registry
137
138
  const model = vibeState.extensionCtx.modelRegistry.find(provider, modelId);
138
139
  if (!model) {
139
- console.debug(
140
+ debugLog(
140
141
  `[working-vibes] Model not found: ${vibeState.config.modelSpec}`,
141
142
  );
142
143
  return `${vibeState.config.fallback}...`;
@@ -146,7 +147,7 @@ export async function generateVibe(
146
147
  const auth =
147
148
  await vibeState.extensionCtx.modelRegistry.getApiKeyAndHeaders(model);
148
149
  if (!auth.ok) {
149
- console.debug(`[working-vibes] Auth failed for ${provider}: ${auth.error}`);
150
+ debugLog(`[working-vibes] Auth failed for ${provider}: ${auth.error}`);
150
151
  return `${vibeState.config.fallback}...`;
151
152
  }
152
153
 
@@ -164,7 +165,7 @@ export async function generateVibe(
164
165
  response.stopReason === "error" &&
165
166
  response.errorMessage
166
167
  ) {
167
- console.debug(
168
+ debugLog(
168
169
  `[working-vibes] Vibe generation failed for ${vibeState.config.modelSpec}: ${response.errorMessage}`,
169
170
  );
170
171
  }
@@ -10,6 +10,7 @@ import { getAgentPath } from "../paths/agent-dirs.ts";
10
10
 
11
11
  export type VibeMode = "generate" | "file";
12
12
  import { workingIndicatorStyles, type WorkingIndicatorStyle } from "./frames.ts";
13
+ import { debugLog } from "../extension/core/debug-log.ts";
13
14
  export type { WorkingIndicatorStyle };
14
15
 
15
16
  // ═══════════════════════════════════════════════════════════════════════════
@@ -103,7 +104,7 @@ function readSettingsForLoad(): Record<string, unknown> {
103
104
 
104
105
  const parsed = JSON.parse(readFileSync(settingsPath, "utf-8"));
105
106
  if (!isRecord(parsed)) {
106
- console.debug(
107
+ debugLog(
107
108
  `[working-vibes] Ignoring non-object settings at ${settingsPath}`,
108
109
  );
109
110
  return {};
@@ -111,7 +112,7 @@ function readSettingsForLoad(): Record<string, unknown> {
111
112
 
112
113
  return parsed;
113
114
  } catch (error) {
114
- console.debug(
115
+ debugLog(
115
116
  `[working-vibes] Failed to load settings from ${settingsPath}:`,
116
117
  error,
117
118
  );
@@ -129,7 +130,7 @@ function readSettingsForWrite(scope: string): Record<string, unknown> | null {
129
130
  try {
130
131
  const parsed = JSON.parse(readFileSync(settingsPath, "utf-8"));
131
132
  if (!isRecord(parsed)) {
132
- console.debug(
133
+ debugLog(
133
134
  `[working-vibes] Refusing to write ${scope}: settings at ${settingsPath} is not an object`,
134
135
  );
135
136
  return null;
@@ -137,7 +138,7 @@ function readSettingsForWrite(scope: string): Record<string, unknown> | null {
137
138
 
138
139
  return parsed;
139
140
  } catch (error) {
140
- console.debug(
141
+ debugLog(
141
142
  `[working-vibes] Failed to parse settings while writing ${scope} at ${settingsPath}:`,
142
143
  error,
143
144
  );
@@ -156,7 +157,7 @@ function persistSettings(
156
157
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
157
158
  return true;
158
159
  } catch (error) {
159
- console.debug(
160
+ debugLog(
160
161
  `[working-vibes] Failed to persist ${scope} to ${settingsPath}:`,
161
162
  error,
162
163
  );
@@ -300,7 +301,7 @@ export function loadVibesFromFile(theme: string): string[] {
300
301
  .map((line) => line.trim())
301
302
  .filter((line) => line.length > 0 && line.endsWith("..."));
302
303
  } catch (error) {
303
- console.debug(
304
+ debugLog(
304
305
  `[working-vibes] Failed to load vibe file ${filePath}:`,
305
306
  error,
306
307
  );