@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 +5 -1
- package/bash-mode/editor.ts +2 -1
- package/bash-mode/history.ts +4 -3
- package/build-stamp.json +1 -1
- package/package.json +3 -2
- package/src/extension/core/debug-log.ts +9 -0
- package/src/extension/core/state.ts +3 -0
- package/src/extension/core/types.ts +2 -0
- package/src/extension/history/stash-history.ts +4 -3
- package/src/extension/session/session-lifecycle.ts +23 -7
- package/src/extension/settings/settings-io.ts +7 -6
- package/src/extension/shortcuts/shortcuts-config.ts +4 -3
- package/src/extension/ui/rpc-widgets.ts +185 -0
- package/src/extension/welcome/welcome-integration.ts +2 -1
- package/src/shell/cd-command.ts +3 -2
- package/src/theme/theme.ts +4 -3
- package/src/welcome/discover.ts +2 -1
- package/src/welcome/whats-new.ts +2 -1
- package/src/working-vibes/manager.ts +3 -2
- package/src/working-vibes/native-indicator.ts +3 -2
- package/src/working-vibes/provider.ts +4 -3
- package/src/working-vibes/storage.ts +7 -6
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
|
package/bash-mode/editor.ts
CHANGED
|
@@ -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
|
-
|
|
467
|
+
debugLog("[wishcraft] Failed to resolve bash ghost suggestion:", error);
|
|
467
468
|
});
|
|
468
469
|
}
|
|
469
470
|
}
|
package/bash-mode/history.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":"
|
|
1
|
+
{"source_sha":"c5a4ed154027696731b82238816c26fe1ce76945"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@groeponline/pi-wishcraft",
|
|
3
|
-
"version": "1.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
287
|
+
debugLog(
|
|
287
288
|
`[wishcraft] Shortcut conflict for ${key}: "${configured}" is already in use`,
|
|
288
289
|
);
|
|
289
290
|
continue;
|
|
290
291
|
}
|
|
291
292
|
|
|
292
|
-
|
|
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
|
-
|
|
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
|
-
|
|
313
|
+
debugLog("[wishcraft] Welcome overlay failed:", error);
|
|
313
314
|
});
|
|
314
315
|
}, 100);
|
|
315
316
|
}
|
package/src/shell/cd-command.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
178
|
+
debugLog("[wishcraft] Failed to report /cd error:", error);
|
|
178
179
|
}
|
|
179
180
|
}
|
|
180
181
|
}
|
package/src/theme/theme.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
210
|
+
debugLog(`[theme] Invalid color "${errorKey}". Fallback to text.`, e);
|
|
210
211
|
}
|
|
211
212
|
return theme.fg("text", text);
|
|
212
213
|
}
|
package/src/welcome/discover.ts
CHANGED
|
@@ -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
|
-
|
|
30
|
+
debugLog(`[powerline-welcome] ${scope}:`, error);
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
function scanContextFiles(homeDir: string, cwd: string): number {
|
package/src/welcome/whats-new.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
71
|
+
debugLog("[working-vibes] Generation aborted");
|
|
71
72
|
} else {
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
304
|
+
debugLog(
|
|
304
305
|
`[working-vibes] Failed to load vibe file ${filePath}:`,
|
|
305
306
|
error,
|
|
306
307
|
);
|