@groeponline/pi-wishcraft 1.23.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 +12 -0
- package/RELEASE.md +37 -8
- package/bash-mode/completion.ts +2 -0
- 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/config/tokens.ts +21 -3
- package/src/config/types.ts +4 -1
- 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/deck/component.ts +11 -0
- package/src/extension/ui/rpc-widgets.ts +185 -0
- package/src/extension/welcome/welcome-integration.ts +2 -1
- package/src/git/status.ts +4 -0
- package/src/segments/system.ts +7 -1
- package/src/shell/cd-command.ts +3 -2
- package/src/theme/theme.ts +10 -3
- package/src/usage/ledger.ts +5 -0
- 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,18 @@
|
|
|
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
|
+
|
|
10
|
+
## [1.24.0] - 2026-11-12
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Git probes no longer race interactive git.** Every read-only `git` spawn (`src/git/status.ts`, `bash-mode/completion.ts`) now runs with `GIT_OPTIONAL_LOCKS=0` and `windowsHide: true`, so status polling can't refresh `.git/index` under a real checkout and orphaned `index.lock` files stop bricking the repo.
|
|
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).
|
|
15
|
+
- **Token totals prefer the provider-reported `totalTokens`.** The four-field sum undercounted providers that report reasoning/other tokens only in the total.
|
|
16
|
+
|
|
5
17
|
## [1.23.0] - 2026-11-08
|
|
6
18
|
|
|
7
19
|
### Fixed
|
package/RELEASE.md
CHANGED
|
@@ -113,11 +113,34 @@ Measured from this repository on 2026-11-08:
|
|
|
113
113
|
| `npm whoami` with `secrets.NPM_TOKEN` | `chefgroeponline` — valid |
|
|
114
114
|
| `ACTIONS_ID_TOKEN_REQUEST_URL` in a job with `id-token: write` | present |
|
|
115
115
|
| `POST /-/npm/v1/oidc/token/exchange/package/@groeponline%2fpi-wishcraft` | **401** |
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
| `npm publish` with that token (v1.23.0, run 42464125440) | **EOTP** — one-time password required |
|
|
117
|
+
|
|
118
|
+
**The token authenticates but cannot write.** It is a *Publish* token on an
|
|
119
|
+
account with 2FA enabled, so npm demands an OTP for `npm publish` and a CI
|
|
120
|
+
runner can never answer one.
|
|
121
|
+
|
|
122
|
+
**Resolved 2026-11-09 03:58 UTC.** The org `NPM_TOKEN` secret was replaced with a
|
|
123
|
+
granular token that has *Bypass 2FA*; run 42559311821 (dispatch on `v1.23.0`)
|
|
124
|
+
then published `1.23.0` and created its GitHub Release. No workflow change was
|
|
125
|
+
needed — `NODE_AUTH_TOKEN` still publishes. `v1.18.0`–`v1.22.0` stay unpublished:
|
|
126
|
+
their tag refs carry unpinned `uses: actions/...@v7` and the org policy rejects
|
|
127
|
+
them, so they cannot be replayed without rewriting tags. `1.23.0` supersedes them.
|
|
128
|
+
|
|
129
|
+
The two routes that were available, for the record:
|
|
130
|
+
|
|
131
|
+
- **Token route (unblocks v1.23.0 as tagged):** Access Tokens → Generate New
|
|
132
|
+
Token → **Granular**, read+write on `@groeponline` (or a classic
|
|
133
|
+
**Automation** token), with **Bypass 2FA** enabled; store it as the org
|
|
134
|
+
`NPM_TOKEN` secret. Then replay the tag:
|
|
135
|
+
`gh workflow run release.yml --ref v1.23.0` — that same job also creates the
|
|
136
|
+
missing GitHub Release.
|
|
137
|
+
- **OIDC route (no credential at all):** configure the trusted publisher below
|
|
138
|
+
and drop the `NODE_AUTH_TOKEN` env from the `publish` job. A tag cut *before*
|
|
139
|
+
that change still carries the token env and cannot be replayed, so this route
|
|
140
|
+
publishes the next version and leaves the hole.
|
|
141
|
+
|
|
142
|
+
Switching to trusted publishing is a one-time npmjs.com change, after which the
|
|
143
|
+
`NODE_AUTH_TOKEN` env comes out of the job:
|
|
121
144
|
|
|
122
145
|
1. Sign in as the maintainer of `@groeponline/pi-wishcraft` (`chefgroeponline
|
|
123
146
|
<joep@chefgroep.online>` — `npm view @groeponline/pi-wishcraft maintainers`).
|
|
@@ -149,9 +172,15 @@ Rules that follow from the current (token) configuration:
|
|
|
149
172
|
|
|
150
173
|
For a scoped package npm answers a PUT with **404, not 403**, when the caller
|
|
151
174
|
may not publish. That means the credential is missing, revoked, read-only or
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
175
|
+
does not own the scope. It is not "package not found", and it is not a
|
|
176
|
+
retryable transient. Two neighbours are distinct and have their own fix:
|
|
177
|
+
|
|
178
|
+
- `ENEEDAUTH` — npm had no credential at all: `NODE_AUTH_TOKEN` was empty and
|
|
179
|
+
the OIDC exchange refused.
|
|
180
|
+
- `EOTP` — the credential authenticates but the account requires a one-time
|
|
181
|
+
password for writes, which CI cannot supply. This is what v1.23.0 hit. The
|
|
182
|
+
token needs 2FA bypass (granular token with *Bypass 2FA*, or a classic
|
|
183
|
+
Automation token); retrying with the same token only burns another run.
|
|
155
184
|
|
|
156
185
|
`scripts/npm-publish.sh` prints that diagnosis instead of the raw 404. Recovery:
|
|
157
186
|
|
package/bash-mode/completion.ts
CHANGED
|
@@ -246,6 +246,8 @@ function runGit(args: string[], cwd: string): string[] {
|
|
|
246
246
|
cwd,
|
|
247
247
|
encoding: "utf8",
|
|
248
248
|
stdio: ["ignore", "pipe", "ignore"],
|
|
249
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
|
|
250
|
+
windowsHide: true,
|
|
249
251
|
});
|
|
250
252
|
if (result.status !== 0 || !result.stdout) return [];
|
|
251
253
|
return result.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
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": "*",
|
package/src/config/tokens.ts
CHANGED
|
@@ -29,11 +29,20 @@ export type { TokenRole, WishcraftTokens };
|
|
|
29
29
|
/**
|
|
30
30
|
* Which token each legacy semantic color reads from.
|
|
31
31
|
*
|
|
32
|
-
* `thinking*` is deliberately absent: those
|
|
32
|
+
* `thinking*` is deliberately absent: those seven are Pi's own thinking-level
|
|
33
33
|
* colors, not part of the Wishcraft palette, so they pass through untouched.
|
|
34
34
|
*/
|
|
35
35
|
export const SEMANTIC_TOKEN_ROLE: Record<
|
|
36
|
-
Exclude<
|
|
36
|
+
Exclude<
|
|
37
|
+
SemanticColor,
|
|
38
|
+
| "thinking"
|
|
39
|
+
| "thinkingMinimal"
|
|
40
|
+
| "thinkingLow"
|
|
41
|
+
| "thinkingMedium"
|
|
42
|
+
| "thinkingHigh"
|
|
43
|
+
| "thinkingXhigh"
|
|
44
|
+
| "thinkingMax"
|
|
45
|
+
>,
|
|
37
46
|
TokenRole
|
|
38
47
|
> = {
|
|
39
48
|
model: "primary",
|
|
@@ -54,12 +63,21 @@ export const SEMANTIC_TOKEN_ROLE: Record<
|
|
|
54
63
|
/** Pi thinking-level colors, passed through rather than tokenised. */
|
|
55
64
|
export const THINKING_COLORS: Pick<
|
|
56
65
|
Required<ColorScheme>,
|
|
57
|
-
|
|
66
|
+
| "thinking"
|
|
67
|
+
| "thinkingMinimal"
|
|
68
|
+
| "thinkingLow"
|
|
69
|
+
| "thinkingMedium"
|
|
70
|
+
| "thinkingHigh"
|
|
71
|
+
| "thinkingXhigh"
|
|
72
|
+
| "thinkingMax"
|
|
58
73
|
> = {
|
|
59
74
|
thinking: "thinkingOff",
|
|
60
75
|
thinkingMinimal: "thinkingMinimal",
|
|
61
76
|
thinkingLow: "thinkingLow",
|
|
62
77
|
thinkingMedium: "thinkingMedium",
|
|
78
|
+
thinkingHigh: "rainbow",
|
|
79
|
+
thinkingXhigh: "rainbow",
|
|
80
|
+
thinkingMax: "rainbow",
|
|
63
81
|
};
|
|
64
82
|
|
|
65
83
|
/**
|
package/src/config/types.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { MotionEvent } from "../motion/types.ts";
|
|
|
3
3
|
import type { CostCurrencyCode } from "../usage/rates.ts";
|
|
4
4
|
|
|
5
5
|
// Theme color - either a pi theme color name or a custom hex color
|
|
6
|
-
export type ColorValue = ThemeColor | `#${string}
|
|
6
|
+
export type ColorValue = ThemeColor | `#${string}` | "rainbow";
|
|
7
7
|
export type ThemeLike = Pick<Theme, "fg">;
|
|
8
8
|
|
|
9
9
|
// Semantic color names for segments
|
|
@@ -17,6 +17,9 @@ export type SemanticColor =
|
|
|
17
17
|
| "thinkingMinimal"
|
|
18
18
|
| "thinkingLow"
|
|
19
19
|
| "thinkingMedium"
|
|
20
|
+
| "thinkingHigh"
|
|
21
|
+
| "thinkingXhigh"
|
|
22
|
+
| "thinkingMax"
|
|
20
23
|
| "context"
|
|
21
24
|
| "contextWarn"
|
|
22
25
|
| "contextError"
|
|
@@ -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
|
}
|
|
@@ -370,6 +370,16 @@ export function createDeckComponent(
|
|
|
370
370
|
if (state.route === "ideas") {
|
|
371
371
|
const ideas = filteredIdeas(liveSnapshot(), state);
|
|
372
372
|
if (!state.navMode && handleList(data, "selectedIdea", ideas.length)) return;
|
|
373
|
+
// Nav ↑↓ must reach the route-column handlers below, like every other
|
|
374
|
+
// list route; the body below used to swallow them (trailing return),
|
|
375
|
+
// stranding the cursor on Ideas forever.
|
|
376
|
+
if (matchesKey(data, "up") || matchesKey(data, "down")) {
|
|
377
|
+
if (state.navMode || ideas.length === 0) {
|
|
378
|
+
// fall through to the nav handlers at the bottom
|
|
379
|
+
} else {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
} else {
|
|
373
383
|
const cursor = Math.min(state.selectedIdea, Math.max(0, ideas.length - 1));
|
|
374
384
|
const idea = ideas[cursor];
|
|
375
385
|
if (!idea?.id) return;
|
|
@@ -404,6 +414,7 @@ export function createDeckComponent(
|
|
|
404
414
|
return;
|
|
405
415
|
}
|
|
406
416
|
return;
|
|
417
|
+
}
|
|
407
418
|
}
|
|
408
419
|
|
|
409
420
|
if (state.route === "ports") {
|
|
@@ -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/git/status.ts
CHANGED
|
@@ -94,6 +94,10 @@ function runGit(args: string[], timeoutMs = 200): Promise<string | null> {
|
|
|
94
94
|
return new Promise((resolve) => {
|
|
95
95
|
const proc = spawn("git", args, {
|
|
96
96
|
stdio: ["ignore", "pipe", "pipe"],
|
|
97
|
+
// Read-only probes must not refresh .git/index — a stray index.lock
|
|
98
|
+
// there races interactive git and bricks the repo (upstream #156/#180).
|
|
99
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
|
|
100
|
+
windowsHide: true,
|
|
97
101
|
});
|
|
98
102
|
|
|
99
103
|
let stdout = "";
|
package/src/segments/system.ts
CHANGED
|
@@ -32,7 +32,13 @@ export const thinkingSegment: StatusLineSegment = {
|
|
|
32
32
|
const content = `think:${label}`;
|
|
33
33
|
|
|
34
34
|
if (level === "high" || level === "xhigh" || level === "max") {
|
|
35
|
-
|
|
35
|
+
const highKey =
|
|
36
|
+
level === "high"
|
|
37
|
+
? "thinkingHigh"
|
|
38
|
+
: level === "xhigh"
|
|
39
|
+
? "thinkingXhigh"
|
|
40
|
+
: "thinkingMax";
|
|
41
|
+
return { content: color(ctx, highKey, content), visible: true };
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
if (level === "minimal") {
|
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;
|
|
@@ -25,6 +26,9 @@ const DEFAULT_COLORS: Required<ColorScheme> = {
|
|
|
25
26
|
thinkingMinimal: "thinkingMinimal",
|
|
26
27
|
thinkingLow: "thinkingLow",
|
|
27
28
|
thinkingMedium: "thinkingMedium",
|
|
29
|
+
thinkingHigh: "rainbow",
|
|
30
|
+
thinkingXhigh: "rainbow",
|
|
31
|
+
thinkingMax: "rainbow",
|
|
28
32
|
context: "dim",
|
|
29
33
|
contextWarn: "warning",
|
|
30
34
|
contextError: "error",
|
|
@@ -131,11 +135,11 @@ export function loadThemeConfig(): PowerlineThemeConfig {
|
|
|
131
135
|
}
|
|
132
136
|
} catch (e: unknown) {
|
|
133
137
|
if (e instanceof SyntaxError) {
|
|
134
|
-
|
|
138
|
+
debugLog(`[theme] SyntaxError in ${path}:`, e.message);
|
|
135
139
|
} else if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
136
140
|
// File doesn't exist despite existsSync, ignore
|
|
137
141
|
} else {
|
|
138
|
-
|
|
142
|
+
debugLog(`[theme] Unexpected error reading ${path}:`, e);
|
|
139
143
|
}
|
|
140
144
|
}
|
|
141
145
|
}
|
|
@@ -189,6 +193,9 @@ export function applyColor(
|
|
|
189
193
|
color: ColorValue,
|
|
190
194
|
text: string,
|
|
191
195
|
): string {
|
|
196
|
+
if (color === "rainbow") {
|
|
197
|
+
return rainbow(text);
|
|
198
|
+
}
|
|
192
199
|
if (typeof color === "string" && /^#[0-9a-fA-F]{6}$/.test(color)) {
|
|
193
200
|
return `${hexToRgbAnsi(color)}${text}\x1b[0m`;
|
|
194
201
|
}
|
|
@@ -200,7 +207,7 @@ export function applyColor(
|
|
|
200
207
|
if (!loggedErrors.has(errorKey)) {
|
|
201
208
|
loggedErrors.add(errorKey);
|
|
202
209
|
if (loggedErrors.size > 100) loggedErrors.clear();
|
|
203
|
-
|
|
210
|
+
debugLog(`[theme] Invalid color "${errorKey}". Fallback to text.`, e);
|
|
204
211
|
}
|
|
205
212
|
return theme.fg("text", text);
|
|
206
213
|
}
|
package/src/usage/ledger.ts
CHANGED
|
@@ -36,6 +36,11 @@ export function isSessionAssistantMessage(value: unknown): value is AssistantMes
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export function getUsageTokenTotal(usage: SessionAssistantUsage): number {
|
|
39
|
+
// Prefer the provider-reported total: it can cover token kinds the four
|
|
40
|
+
// fields miss (e.g. reasoning on providers that don't itemize).
|
|
41
|
+
if (typeof usage.totalTokens === "number" && usage.totalTokens > 0) {
|
|
42
|
+
return usage.totalTokens;
|
|
43
|
+
}
|
|
39
44
|
return usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
|
|
40
45
|
}
|
|
41
46
|
|
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
|
);
|