@groeponline/pi-wishcraft 1.2.0 → 1.3.1
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 +8 -0
- package/ROADMAP.md +142 -204
- package/docs/design/signal.md +51 -48
- package/package.json +1 -1
- package/src/config/appearance.ts +0 -1
- package/src/extension/core/segment-context.ts +20 -20
- package/src/extension/core/state.ts +2 -1
- package/src/extension/core/types.ts +4 -1
- package/src/extension/session/session-lifecycle.ts +9 -65
- package/src/extension/session/session-notifications.ts +91 -0
- package/src/extension/ui/deck/component.ts +46 -13
- package/src/extension/ui/deck/render.ts +4 -4
- package/src/extension/ui/deck/session-snapshot.ts +52 -34
- package/src/extension/ui/deck/types.ts +14 -7
- package/src/extension/ui/signal-layout.ts +0 -1
- package/src/signal/controller.ts +12 -0
- package/src/signal/integration.ts +7 -3
- package/src/theme/tokens/mapping.ts +11 -52
- package/src/theme/tokens/types.ts +3 -36
- package/src/usage/token-budget.ts +23 -0
|
@@ -16,26 +16,10 @@ import {
|
|
|
16
16
|
onVibeToolCall,
|
|
17
17
|
} from "../../working-vibes/index.ts";
|
|
18
18
|
import {
|
|
19
|
-
getSessionTotalCost,
|
|
20
19
|
getUsageTokenTotal,
|
|
21
20
|
isSessionAssistantMessage,
|
|
22
21
|
} from "../../usage/ledger.ts";
|
|
23
|
-
import {
|
|
24
|
-
formatCostAlertMessage,
|
|
25
|
-
shouldTriggerCostAlert,
|
|
26
|
-
} from "./cost-alert.ts";
|
|
27
|
-
import {
|
|
28
|
-
formatTokenBudgetWarning,
|
|
29
|
-
parseTokenBudget,
|
|
30
|
-
tokenBudgetLevel,
|
|
31
|
-
} from "../../usage/token-budget.ts";
|
|
32
|
-
import {
|
|
33
|
-
recordUsageEvent,
|
|
34
|
-
loadUsageFileFromDisk,
|
|
35
|
-
tokenTotal,
|
|
36
|
-
totalsForRange,
|
|
37
|
-
dayKey,
|
|
38
|
-
} from "../../usage/usage-store.ts";
|
|
22
|
+
import { recordUsageEvent } from "../../usage/usage-store.ts";
|
|
39
23
|
import {
|
|
40
24
|
detectCustomCompactionEnabled,
|
|
41
25
|
readSettings,
|
|
@@ -81,52 +65,10 @@ import {
|
|
|
81
65
|
dispatchSignalEvent,
|
|
82
66
|
settleSignal,
|
|
83
67
|
} from "../../signal/integration.ts";
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
* cheap; a UI-less or already-notified session short-circuits immediately.
|
|
89
|
-
*/
|
|
90
|
-
function maybeNotifyCostAlert(rt: RuntimeState, ctx: any): void {
|
|
91
|
-
if (!ctx?.hasUI || rt.costAlertNotified) return;
|
|
92
|
-
const threshold = config.costAlert;
|
|
93
|
-
const sessionEvents = rt.sessionBranchCache.get(ctx.sessionManager);
|
|
94
|
-
const totalCost = getSessionTotalCost(rt.tokenStatsCache.get(sessionEvents));
|
|
95
|
-
if (
|
|
96
|
-
!shouldTriggerCostAlert({
|
|
97
|
-
totalCost,
|
|
98
|
-
threshold,
|
|
99
|
-
alreadyNotified: rt.costAlertNotified,
|
|
100
|
-
})
|
|
101
|
-
) {
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
rt.costAlertNotified = true;
|
|
105
|
-
ctx.ui.notify(
|
|
106
|
-
formatCostAlertMessage(
|
|
107
|
-
totalCost,
|
|
108
|
-
threshold as number,
|
|
109
|
-
config.segmentOptions?.cost?.currency ?? "USD",
|
|
110
|
-
),
|
|
111
|
-
"warning",
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function maybeNotifyTokenBudget(rt: RuntimeState, ctx: any): void {
|
|
116
|
-
if (!ctx?.hasUI) return;
|
|
117
|
-
const daily = parseTokenBudget(readSettings(ctx.cwd ?? process.cwd()).wishcraft)
|
|
118
|
-
.daily;
|
|
119
|
-
if (!daily) return;
|
|
120
|
-
const now = Date.now();
|
|
121
|
-
const todayStart = Date.parse(`${dayKey(now)}T00:00:00`);
|
|
122
|
-
const used = tokenTotal(
|
|
123
|
-
totalsForRange(loadUsageFileFromDisk(), todayStart, now + 1),
|
|
124
|
-
);
|
|
125
|
-
const { level } = tokenBudgetLevel(used, daily);
|
|
126
|
-
if (level === 0 || level <= rt.tokenBudgetNotifiedLevel) return;
|
|
127
|
-
rt.tokenBudgetNotifiedLevel = level;
|
|
128
|
-
ctx.ui.notify(formatTokenBudgetWarning(used, daily, level), "warning");
|
|
129
|
-
}
|
|
68
|
+
import {
|
|
69
|
+
maybeNotifyCostAlert,
|
|
70
|
+
maybeNotifyTokenBudget,
|
|
71
|
+
} from "./session-notifications.ts";
|
|
130
72
|
|
|
131
73
|
// Helper to extract recent agent response text (skipping thinking blocks)
|
|
132
74
|
function getRecentAgentContext(ctx: any): string | undefined {
|
|
@@ -180,6 +122,7 @@ export function registerSessionLifecycle(
|
|
|
180
122
|
rt.liveAssistantUsage = null;
|
|
181
123
|
rt.costAlertNotified = false;
|
|
182
124
|
rt.tokenBudgetNotifiedLevel = 0;
|
|
125
|
+
rt.tokenBudgetSnapshot = { day: "", dailyLimit: null, dailyUsed: 0 };
|
|
183
126
|
rt.powerlineCompacting = false;
|
|
184
127
|
rt.deliverAfterRetrySettles = false;
|
|
185
128
|
rt.stashedEditorText = null;
|
|
@@ -232,7 +175,7 @@ export function registerSessionLifecycle(
|
|
|
232
175
|
} else {
|
|
233
176
|
dismissWelcome(rt, ctx);
|
|
234
177
|
}
|
|
235
|
-
maybeNotifyTokenBudget(rt, ctx);
|
|
178
|
+
maybeNotifyTokenBudget(rt, ctx, settings);
|
|
236
179
|
}
|
|
237
180
|
});
|
|
238
181
|
|
|
@@ -264,6 +207,7 @@ export function registerSessionLifecycle(
|
|
|
264
207
|
rt.getThinkingLevelFn = null;
|
|
265
208
|
rt.currentThinkingLevel = null;
|
|
266
209
|
rt.liveAssistantUsage = null;
|
|
210
|
+
rt.tokenBudgetSnapshot = { day: "", dailyLimit: null, dailyUsed: 0 };
|
|
267
211
|
rt.tuiRef = null;
|
|
268
212
|
rt.currentEditor = null;
|
|
269
213
|
resetLayoutCache(rt);
|
|
@@ -482,4 +426,4 @@ export function registerSessionLifecycle(
|
|
|
482
426
|
schedulePostCompactionDelivery(pi, rt, ctx);
|
|
483
427
|
}
|
|
484
428
|
});
|
|
485
|
-
}
|
|
429
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { getSessionTotalCost } from "../../usage/ledger.ts";
|
|
2
|
+
import {
|
|
3
|
+
formatTokenBudgetWarning,
|
|
4
|
+
parseTokenBudget,
|
|
5
|
+
tokenBudgetLevel,
|
|
6
|
+
} from "../../usage/token-budget.ts";
|
|
7
|
+
import {
|
|
8
|
+
dayKey,
|
|
9
|
+
loadUsageFileFromDisk,
|
|
10
|
+
tokenTotal,
|
|
11
|
+
totalsForRange,
|
|
12
|
+
} from "../../usage/usage-store.ts";
|
|
13
|
+
import { config } from "../core/state.ts";
|
|
14
|
+
import type { RuntimeState } from "../core/types.ts";
|
|
15
|
+
import { readSettings } from "../settings/settings-io.ts";
|
|
16
|
+
import {
|
|
17
|
+
formatCostAlertMessage,
|
|
18
|
+
shouldTriggerCostAlert,
|
|
19
|
+
} from "./cost-alert.ts";
|
|
20
|
+
|
|
21
|
+
/** Notify once when the configured per-session cost threshold is crossed. */
|
|
22
|
+
export function maybeNotifyCostAlert(rt: RuntimeState, ctx: any): void {
|
|
23
|
+
if (!ctx?.hasUI || rt.costAlertNotified) return;
|
|
24
|
+
const threshold = config.costAlert;
|
|
25
|
+
const sessionEvents = rt.sessionBranchCache.get(ctx.sessionManager);
|
|
26
|
+
const totalCost = getSessionTotalCost(rt.tokenStatsCache.get(sessionEvents));
|
|
27
|
+
if (
|
|
28
|
+
!shouldTriggerCostAlert({
|
|
29
|
+
totalCost,
|
|
30
|
+
threshold,
|
|
31
|
+
alreadyNotified: rt.costAlertNotified,
|
|
32
|
+
})
|
|
33
|
+
) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
rt.costAlertNotified = true;
|
|
38
|
+
ctx.ui.notify(
|
|
39
|
+
formatCostAlertMessage(
|
|
40
|
+
totalCost,
|
|
41
|
+
threshold as number,
|
|
42
|
+
config.segmentOptions?.cost?.currency ?? "USD",
|
|
43
|
+
),
|
|
44
|
+
"warning",
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Refresh disk-backed budget data at lifecycle boundaries, never during paint.
|
|
50
|
+
* A local-day rollover also resets the per-day warning latch.
|
|
51
|
+
*/
|
|
52
|
+
export function refreshTokenBudgetSnapshot(
|
|
53
|
+
rt: RuntimeState,
|
|
54
|
+
ctx: any,
|
|
55
|
+
settings?: ReturnType<typeof readSettings>,
|
|
56
|
+
): { daily: number | null; used: number; level: 0 | 80 | 100 } {
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const day = dayKey(now);
|
|
59
|
+
if (rt.tokenBudgetSnapshot.day !== day) {
|
|
60
|
+
rt.tokenBudgetNotifiedLevel = 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const resolvedSettings = settings ?? readSettings(ctx.cwd ?? process.cwd());
|
|
64
|
+
const daily = parseTokenBudget(resolvedSettings.wishcraft).daily;
|
|
65
|
+
if (!daily) {
|
|
66
|
+
rt.tokenBudgetSnapshot = { day, dailyLimit: null, dailyUsed: 0 };
|
|
67
|
+
return { daily: null, used: 0, level: 0 };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const todayStart = Date.parse(`${day}T00:00:00`);
|
|
71
|
+
const used = tokenTotal(
|
|
72
|
+
totalsForRange(loadUsageFileFromDisk(), todayStart, now + 1),
|
|
73
|
+
);
|
|
74
|
+
rt.tokenBudgetSnapshot = { day, dailyLimit: daily, dailyUsed: used };
|
|
75
|
+
const { level } = tokenBudgetLevel(used, daily);
|
|
76
|
+
return { daily, used, level };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Refresh the daily budget and emit its 80%/100% warning at most once per day. */
|
|
80
|
+
export function maybeNotifyTokenBudget(
|
|
81
|
+
rt: RuntimeState,
|
|
82
|
+
ctx: any,
|
|
83
|
+
settings?: ReturnType<typeof readSettings>,
|
|
84
|
+
): void {
|
|
85
|
+
if (!ctx?.hasUI) return;
|
|
86
|
+
const { daily, used, level } = refreshTokenBudgetSnapshot(rt, ctx, settings);
|
|
87
|
+
if (!daily || level === 0 || level <= rt.tokenBudgetNotifiedLevel) return;
|
|
88
|
+
|
|
89
|
+
rt.tokenBudgetNotifiedLevel = level;
|
|
90
|
+
ctx.ui.notify(formatTokenBudgetWarning(used, daily, level), "warning");
|
|
91
|
+
}
|
|
@@ -26,7 +26,10 @@ import {
|
|
|
26
26
|
import type { RuntimeState } from "../../core/types.ts";
|
|
27
27
|
import { appearanceDisplayName } from "../../../config/structural-presets.ts";
|
|
28
28
|
import { filterSkillRows, selectedGalleryMotion } from "./route-bodies.ts";
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
buildDeckSessionSnapshot,
|
|
31
|
+
buildDeckStaticSnapshot,
|
|
32
|
+
} from "./session-snapshot.ts";
|
|
30
33
|
import { deckRouteByJump, deckRouteIndex } from "./routes.ts";
|
|
31
34
|
import { filterDeckRoutes, renderDeckFrame } from "./render.ts";
|
|
32
35
|
import { DECK_ROUTE_DEFS } from "./routes.ts";
|
|
@@ -56,6 +59,7 @@ export function createDeckNavState(
|
|
|
56
59
|
assignEvent: "streaming",
|
|
57
60
|
skillCreate: false,
|
|
58
61
|
skillCreateName: "",
|
|
62
|
+
navMode: false,
|
|
59
63
|
};
|
|
60
64
|
}
|
|
61
65
|
|
|
@@ -66,14 +70,23 @@ export function createDeckComponent(
|
|
|
66
70
|
theme: import("@earendil-works/pi-coding-agent").Theme,
|
|
67
71
|
done: () => void,
|
|
68
72
|
) {
|
|
69
|
-
|
|
73
|
+
let staticSnapshot = buildDeckStaticSnapshot(ctx);
|
|
74
|
+
const liveSnapshot = () => buildDeckSessionSnapshot(rt, ctx, staticSnapshot);
|
|
75
|
+
const refreshStaticSnapshot = (): void => {
|
|
76
|
+
staticSnapshot = buildDeckStaticSnapshot(ctx);
|
|
77
|
+
};
|
|
78
|
+
|
|
70
79
|
let state = createDeckNavState(
|
|
71
80
|
initialRoute,
|
|
72
|
-
appearanceIndex(
|
|
81
|
+
appearanceIndex(liveSnapshot().appearanceBase),
|
|
73
82
|
);
|
|
74
83
|
let composer: ComposerDraft | null = null;
|
|
75
84
|
|
|
76
85
|
const setRoute = (route: DeckRoute): void => {
|
|
86
|
+
const routeChanged = route !== state.route;
|
|
87
|
+
if (routeChanged && (route === "skills" || route === "guardrails")) {
|
|
88
|
+
refreshStaticSnapshot();
|
|
89
|
+
}
|
|
77
90
|
const jumpingToAppearance = route === "appearance" && state.route !== "appearance";
|
|
78
91
|
state = {
|
|
79
92
|
...state,
|
|
@@ -83,7 +96,7 @@ export function createDeckComponent(
|
|
|
83
96
|
searchOpen: false,
|
|
84
97
|
searchQuery: "",
|
|
85
98
|
selectedAppearance: jumpingToAppearance
|
|
86
|
-
? appearanceIndex(
|
|
99
|
+
? appearanceIndex(liveSnapshot().appearanceBase)
|
|
87
100
|
: state.selectedAppearance,
|
|
88
101
|
};
|
|
89
102
|
composer = null;
|
|
@@ -100,7 +113,7 @@ export function createDeckComponent(
|
|
|
100
113
|
return renderDeckFrame(
|
|
101
114
|
theme,
|
|
102
115
|
width,
|
|
103
|
-
|
|
116
|
+
liveSnapshot(),
|
|
104
117
|
state,
|
|
105
118
|
rt.resolvedShortcuts,
|
|
106
119
|
composer,
|
|
@@ -143,7 +156,7 @@ export function createDeckComponent(
|
|
|
143
156
|
|
|
144
157
|
if (state.pendingJump === "g" && data.length === 1 && isOverlayPrintable(data)) {
|
|
145
158
|
const route = deckRouteByJump(data);
|
|
146
|
-
state = { ...state, pendingJump: null };
|
|
159
|
+
state = { ...state, pendingJump: null, navMode: false };
|
|
147
160
|
if (route) setRoute(route);
|
|
148
161
|
return;
|
|
149
162
|
}
|
|
@@ -155,8 +168,27 @@ export function createDeckComponent(
|
|
|
155
168
|
return;
|
|
156
169
|
}
|
|
157
170
|
|
|
171
|
+
// ←/tab returns focus to the NAVIGATION column in one press; from there
|
|
172
|
+
// ↑↓ moves the nav selection and any other key drops back into the list.
|
|
173
|
+
if (
|
|
174
|
+
!state.composerOpen &&
|
|
175
|
+
!state.skillCreate &&
|
|
176
|
+
(matchesKey(data, "left") || matchesKey(data, "tab"))
|
|
177
|
+
) {
|
|
178
|
+
state = { ...state, navMode: true };
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (state.navMode) {
|
|
182
|
+
if (matchesKey(data, "up") || matchesKey(data, "down")) {
|
|
183
|
+
// fall through to the nav handlers at the bottom
|
|
184
|
+
} else {
|
|
185
|
+
state = { ...state, navMode: false };
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
158
190
|
if (state.route === "appearance") {
|
|
159
|
-
if (handleList(data, "selectedAppearance", STRUCTURAL_PRESET_NAMES.length)) return;
|
|
191
|
+
if (!state.navMode && handleList(data, "selectedAppearance", STRUCTURAL_PRESET_NAMES.length)) return;
|
|
160
192
|
if (matchesKey(data, "enter")) {
|
|
161
193
|
const name = STRUCTURAL_PRESET_NAMES[state.selectedAppearance];
|
|
162
194
|
if (name) {
|
|
@@ -170,7 +202,7 @@ export function createDeckComponent(
|
|
|
170
202
|
|
|
171
203
|
if (state.route === "motion") {
|
|
172
204
|
const count = filterMotions(state.searchQuery).length;
|
|
173
|
-
if (handleList(data, "selectedMotion", count)) return;
|
|
205
|
+
if (!state.navMode && handleList(data, "selectedMotion", count)) return;
|
|
174
206
|
if (data === "t") {
|
|
175
207
|
state = { ...state, assignEvent: cycleAssignEvent(state.assignEvent) };
|
|
176
208
|
return;
|
|
@@ -209,6 +241,7 @@ export function createDeckComponent(
|
|
|
209
241
|
if (!name) return;
|
|
210
242
|
try {
|
|
211
243
|
const { filePath } = writeSkillFromTemplate(name, "standard");
|
|
244
|
+
refreshStaticSnapshot();
|
|
212
245
|
state = { ...state, skillCreate: false, skillCreateName: "" };
|
|
213
246
|
ctx.ui.notify(`Created ${name}. ${filePath}`, "info");
|
|
214
247
|
} catch (error) {
|
|
@@ -233,9 +266,9 @@ export function createDeckComponent(
|
|
|
233
266
|
state = { ...state, skillCreate: true, skillCreateName: "" };
|
|
234
267
|
return;
|
|
235
268
|
}
|
|
236
|
-
const snapshot =
|
|
269
|
+
const snapshot = liveSnapshot();
|
|
237
270
|
const rows = filterSkillRows(snapshot.skills, state.searchQuery);
|
|
238
|
-
if (handleList(data, "selectedSkill", rows.length)) return;
|
|
271
|
+
if (!state.navMode && handleList(data, "selectedSkill", rows.length)) return;
|
|
239
272
|
if (matchesKey(data, "enter")) {
|
|
240
273
|
const selected = rows[state.selectedSkill];
|
|
241
274
|
if (!selected) return;
|
|
@@ -253,7 +286,7 @@ export function createDeckComponent(
|
|
|
253
286
|
}
|
|
254
287
|
|
|
255
288
|
if (state.route === "ideas") {
|
|
256
|
-
if (handleList(data, "selectedIdea",
|
|
289
|
+
if (!state.navMode && handleList(data, "selectedIdea", liveSnapshot().ideas.length)) return;
|
|
257
290
|
}
|
|
258
291
|
|
|
259
292
|
if (matchesKey(data, "up")) {
|
|
@@ -298,7 +331,7 @@ export function createDeckComponent(
|
|
|
298
331
|
if (state.route !== "motion" && state.route !== "skills") {
|
|
299
332
|
const matches = filterDeckRoutes(next);
|
|
300
333
|
if (matches.length === 1) {
|
|
301
|
-
state = { ...state, searchOpen: false, searchQuery: "" };
|
|
334
|
+
state = { ...state, searchOpen: false, searchQuery: "", navMode: false };
|
|
302
335
|
setRoute(matches[0]!);
|
|
303
336
|
}
|
|
304
337
|
}
|
|
@@ -311,7 +344,7 @@ export function createDeckComponent(
|
|
|
311
344
|
}
|
|
312
345
|
const matches = filterDeckRoutes(state.searchQuery);
|
|
313
346
|
if (matches[0]) {
|
|
314
|
-
state = { ...state, searchOpen: false, searchQuery: "" };
|
|
347
|
+
state = { ...state, searchOpen: false, searchQuery: "", navMode: false };
|
|
315
348
|
setRoute(matches[0]);
|
|
316
349
|
}
|
|
317
350
|
}
|
|
@@ -31,13 +31,13 @@ export function deckFooter(state: DeckNavState): string {
|
|
|
31
31
|
if (state.skillCreate) return "type a name · enter create · esc cancel";
|
|
32
32
|
switch (state.route) {
|
|
33
33
|
case "appearance":
|
|
34
|
-
return "↑↓ select base · enter apply · / Search · g h Home · Esc Close";
|
|
34
|
+
return "↑↓ select base · enter apply · ←/tab nav · / Search · g h Home · Esc Close";
|
|
35
35
|
case "motion":
|
|
36
|
-
return "↑↓ motion · t event · e composer · enter apply · Esc Close";
|
|
36
|
+
return "↑↓ motion · t event · e composer · enter apply · ←/tab nav · Esc Close";
|
|
37
37
|
case "skills":
|
|
38
|
-
return "↑↓ skill · enter insert · n new · / filter · Esc Close";
|
|
38
|
+
return "↑↓ skill · enter insert · n new · / filter · ←/tab nav · Esc Close";
|
|
39
39
|
case "ideas":
|
|
40
|
-
return "↑↓ idea · / Search · g h Home · Esc Close";
|
|
40
|
+
return "↑↓ idea · / Search · ←/tab nav · g h Home · Esc Close";
|
|
41
41
|
default:
|
|
42
42
|
return "/ Search g h Home g s Signal g i Ideas ? Help Esc Close";
|
|
43
43
|
}
|
|
@@ -9,21 +9,13 @@ import { collectSkillDoctorInputs, diagnoseSkills } from "../../skills/skill-doc
|
|
|
9
9
|
import { parsePolicySettings } from "../../hooks/policy-config.ts";
|
|
10
10
|
import { readSettings } from "../../settings/settings-io.ts";
|
|
11
11
|
import { describePolicy } from "../../../motion/accessibility.ts";
|
|
12
|
-
import type { DeckSessionSnapshot } from "./types.ts";
|
|
12
|
+
import type { DeckSessionSnapshot, DeckStaticSnapshot } from "./types.ts";
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} catch {
|
|
20
|
-
segmentCtx = null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const queue = rt.queueStore.summarize(
|
|
24
|
-
getQueueContext(ctx),
|
|
25
|
-
rt.powerlineCompacting,
|
|
26
|
-
);
|
|
14
|
+
/**
|
|
15
|
+
* Build the expensive Deck data once per open/navigation refresh.
|
|
16
|
+
* This intentionally owns filesystem-backed skill discovery/doctor and settings reads.
|
|
17
|
+
*/
|
|
18
|
+
export function buildDeckStaticSnapshot(ctx: any): DeckStaticSnapshot {
|
|
27
19
|
const cwd = ctx.cwd ?? process.cwd();
|
|
28
20
|
const skills = loadSkillCatalog(cwd);
|
|
29
21
|
const doctorInputs = collectSkillDoctorInputs(cwd);
|
|
@@ -35,6 +27,51 @@ export function buildDeckSessionSnapshot(rt: RuntimeState, ctx: any): DeckSessio
|
|
|
35
27
|
const warnings = doctor.filter((row) => row.status !== "ok").length;
|
|
36
28
|
const settings = readSettings(cwd);
|
|
37
29
|
const policy = parsePolicySettings(settings.wishcraft);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
skillsTotal: skills.length,
|
|
33
|
+
skillsWarnings: warnings,
|
|
34
|
+
policyEnabled: policy.enabled,
|
|
35
|
+
policyRuleCount: policy.rules.length,
|
|
36
|
+
skills: skills.slice(0, 24).map((skill) => {
|
|
37
|
+
const row = doctor.find((entry) => entry.skill === skill.name);
|
|
38
|
+
const usage = doctorInputs.usage.get(skill.name);
|
|
39
|
+
return {
|
|
40
|
+
name: skill.name,
|
|
41
|
+
category: skill.category,
|
|
42
|
+
status: row?.status ?? (skill.warning ? "warn" : "ok"),
|
|
43
|
+
description: skill.description.slice(0, 72),
|
|
44
|
+
usage: usage?.count ?? 0,
|
|
45
|
+
};
|
|
46
|
+
}),
|
|
47
|
+
guardrailRules: policy.rules.slice(0, 8).map((rule) => ({
|
|
48
|
+
action: rule.action,
|
|
49
|
+
tool: rule.tool,
|
|
50
|
+
reason: rule.action === "deny" ? rule.reason : rule.context.slice(0, 48),
|
|
51
|
+
})),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Build only runtime-backed data during paint; no filesystem discovery here. */
|
|
56
|
+
export function buildDeckSessionSnapshot(
|
|
57
|
+
rt: RuntimeState,
|
|
58
|
+
ctx: any,
|
|
59
|
+
staticSnapshot: DeckStaticSnapshot = buildDeckStaticSnapshot(ctx),
|
|
60
|
+
): DeckSessionSnapshot {
|
|
61
|
+
const theme = { fg: (_color: string, text: string) => text } as Theme;
|
|
62
|
+
let segmentCtx;
|
|
63
|
+
try {
|
|
64
|
+
segmentCtx = buildSegmentContext(rt, ctx, theme, {
|
|
65
|
+
includeTokenBudget: false,
|
|
66
|
+
});
|
|
67
|
+
} catch {
|
|
68
|
+
segmentCtx = null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const queue = rt.queueStore.summarize(
|
|
72
|
+
getQueueContext(ctx),
|
|
73
|
+
rt.powerlineCompacting,
|
|
74
|
+
);
|
|
38
75
|
const appearance = resolveAppearanceMix(
|
|
39
76
|
effectiveAppearanceMix(config.appearance, config.preset),
|
|
40
77
|
);
|
|
@@ -56,6 +93,7 @@ export function buildDeckSessionSnapshot(rt: RuntimeState, ctx: any): DeckSessio
|
|
|
56
93
|
}
|
|
57
94
|
|
|
58
95
|
return {
|
|
96
|
+
...staticSnapshot,
|
|
59
97
|
modelLabel,
|
|
60
98
|
branchLabel,
|
|
61
99
|
contextPercent: Math.round(segmentCtx?.contextPercent ?? 0),
|
|
@@ -65,10 +103,6 @@ export function buildDeckSessionSnapshot(rt: RuntimeState, ctx: any): DeckSessio
|
|
|
65
103
|
signalMotion: rt.signal.motionId,
|
|
66
104
|
queueCount: queue.queueCount,
|
|
67
105
|
ideaCount: queue.ideaCount,
|
|
68
|
-
skillsTotal: skills.length,
|
|
69
|
-
skillsWarnings: warnings,
|
|
70
|
-
policyEnabled: policy.enabled,
|
|
71
|
-
policyRuleCount: policy.rules.length,
|
|
72
106
|
shellName: rt.shellSession?.state.shellName ?? null,
|
|
73
107
|
bashModeActive: rt.bashModeActive,
|
|
74
108
|
appearanceBase: appearance.base,
|
|
@@ -76,17 +110,6 @@ export function buildDeckSessionSnapshot(rt: RuntimeState, ctx: any): DeckSessio
|
|
|
76
110
|
nextIntent: queue.leadingText,
|
|
77
111
|
motionLevel: config.motionLevel,
|
|
78
112
|
policySummary: describePolicy(rt.motionPolicy),
|
|
79
|
-
skills: skills.slice(0, 24).map((skill) => {
|
|
80
|
-
const row = doctor.find((entry) => entry.skill === skill.name);
|
|
81
|
-
const usage = doctorInputs.usage.get(skill.name);
|
|
82
|
-
return {
|
|
83
|
-
name: skill.name,
|
|
84
|
-
category: skill.category,
|
|
85
|
-
status: row?.status ?? (skill.warning ? "warn" : "ok"),
|
|
86
|
-
description: skill.description.slice(0, 72),
|
|
87
|
-
usage: usage?.count ?? 0,
|
|
88
|
-
};
|
|
89
|
-
}),
|
|
90
113
|
ideas: rt.queueStore
|
|
91
114
|
.list()
|
|
92
115
|
.filter((item) => item.intent === "idea")
|
|
@@ -95,10 +118,5 @@ export function buildDeckSessionSnapshot(rt: RuntimeState, ctx: any): DeckSessio
|
|
|
95
118
|
text: item.text.slice(0, 64),
|
|
96
119
|
reviewStatus: item.reviewStatus ?? "idea",
|
|
97
120
|
})),
|
|
98
|
-
guardrailRules: policy.rules.slice(0, 8).map((rule) => ({
|
|
99
|
-
action: rule.action,
|
|
100
|
-
tool: rule.tool,
|
|
101
|
-
reason: rule.action === "deny" ? rule.reason : rule.context.slice(0, 48),
|
|
102
|
-
})),
|
|
103
121
|
};
|
|
104
122
|
}
|
|
@@ -28,7 +28,18 @@ export interface DeckRouteDef {
|
|
|
28
28
|
description: string;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
/** Expensive discovery/config data cached outside the render hot path. */
|
|
32
|
+
export interface DeckStaticSnapshot {
|
|
33
|
+
skillsTotal: number;
|
|
34
|
+
skillsWarnings: number;
|
|
35
|
+
policyEnabled: boolean;
|
|
36
|
+
policyRuleCount: number;
|
|
37
|
+
skills: DeckSkillRow[];
|
|
38
|
+
guardrailRules: DeckGuardrailRow[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Cheap session/runtime data that may be rebuilt for each paint. */
|
|
42
|
+
export interface DeckSessionSnapshot extends DeckStaticSnapshot {
|
|
32
43
|
modelLabel: string;
|
|
33
44
|
branchLabel: string;
|
|
34
45
|
contextPercent: number;
|
|
@@ -38,10 +49,6 @@ export interface DeckSessionSnapshot {
|
|
|
38
49
|
signalMotion: string;
|
|
39
50
|
queueCount: number;
|
|
40
51
|
ideaCount: number;
|
|
41
|
-
skillsTotal: number;
|
|
42
|
-
skillsWarnings: number;
|
|
43
|
-
policyEnabled: boolean;
|
|
44
|
-
policyRuleCount: number;
|
|
45
52
|
shellName: string | null;
|
|
46
53
|
bashModeActive: boolean;
|
|
47
54
|
appearanceBase: string;
|
|
@@ -49,9 +56,7 @@ export interface DeckSessionSnapshot {
|
|
|
49
56
|
nextIntent: string | null;
|
|
50
57
|
motionLevel: string;
|
|
51
58
|
policySummary: string;
|
|
52
|
-
skills: DeckSkillRow[];
|
|
53
59
|
ideas: DeckIdeaRow[];
|
|
54
|
-
guardrailRules: DeckGuardrailRow[];
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
export interface DeckSkillRow {
|
|
@@ -89,4 +94,6 @@ export interface DeckNavState {
|
|
|
89
94
|
assignEvent: import("../../../motion/types.ts").MotionEvent;
|
|
90
95
|
skillCreate: boolean;
|
|
91
96
|
skillCreateName: string;
|
|
97
|
+
/** True when ↑↓ moves the NAVIGATION column instead of the route list. */
|
|
98
|
+
navMode: boolean;
|
|
92
99
|
}
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* ---------------------------------------------------------------------------
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
10
9
|
import type { PresetDef, SegmentContext } from "../../config/types.ts";
|
|
11
10
|
import { renderSegmentWithWidth } from "./layout.ts";
|
|
12
11
|
import { getSeparator } from "../../theme/separators.ts";
|
package/src/signal/controller.ts
CHANGED
|
@@ -39,8 +39,12 @@ export function createSignalRuntime(now = Date.now()): SignalRuntime {
|
|
|
39
39
|
export interface SetSignalEventOptions {
|
|
40
40
|
activity?: string;
|
|
41
41
|
motionId?: string;
|
|
42
|
+
/** Motion to restore when a terminal one-shot settles back to semantic idle. */
|
|
43
|
+
settleMotionId?: string;
|
|
42
44
|
/** Finite events stop after this many frames. */
|
|
43
45
|
maxTicks?: number;
|
|
46
|
+
/** Terminal one-shots return to semantic idle when their final frame completes. */
|
|
47
|
+
settleOnDone?: boolean;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
export function setSignalEvent(
|
|
@@ -79,6 +83,14 @@ export function setSignalEvent(
|
|
|
79
83
|
onDone() {
|
|
80
84
|
runtime.release = null;
|
|
81
85
|
if (options.maxTicks !== undefined) runtime.active = false;
|
|
86
|
+
if (options.settleOnDone) {
|
|
87
|
+
runtime.event = "idle";
|
|
88
|
+
runtime.motionId = options.settleMotionId ?? defaultMotionFor("idle");
|
|
89
|
+
runtime.tick = 0;
|
|
90
|
+
runtime.startedAt = Date.now();
|
|
91
|
+
runtime.activity = "ready";
|
|
92
|
+
runtime.active = false;
|
|
93
|
+
}
|
|
82
94
|
},
|
|
83
95
|
});
|
|
84
96
|
}
|
|
@@ -17,7 +17,9 @@ export function dispatchSignalEvent(
|
|
|
17
17
|
setSignalEvent(rt.signal, rt.motionScheduler, rt.motionPolicy, event, {
|
|
18
18
|
activity,
|
|
19
19
|
motionId: resolved.motion[event] ?? resolved.signal.animation,
|
|
20
|
+
settleMotionId: resolved.motion.idle,
|
|
20
21
|
maxTicks: isFiniteEvent(event) ? 6 : undefined,
|
|
22
|
+
settleOnDone: isTerminalEvent(event),
|
|
21
23
|
});
|
|
22
24
|
rt.lastLayoutResult = null;
|
|
23
25
|
rt.tuiRef?.requestRender();
|
|
@@ -35,8 +37,10 @@ function isFiniteEvent(event: MotionEvent): boolean {
|
|
|
35
37
|
event === "idea.capture" ||
|
|
36
38
|
event === "skill.insert" ||
|
|
37
39
|
event === "policy.deny" ||
|
|
38
|
-
event
|
|
39
|
-
event === "warning" ||
|
|
40
|
-
event === "error"
|
|
40
|
+
isTerminalEvent(event)
|
|
41
41
|
);
|
|
42
42
|
}
|
|
43
|
+
|
|
44
|
+
function isTerminalEvent(event: MotionEvent): boolean {
|
|
45
|
+
return event === "success" || event === "warning" || event === "error";
|
|
46
|
+
}
|
|
@@ -1,62 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Default token mapping and legacy SemanticColor bridge for Wishcraft.
|
|
5
|
-
* ---------------------------------------------------------------------------
|
|
2
|
+
* Compatibility bridge for the canonical Wishcraft token mapping.
|
|
3
|
+
* New code should import from src/config/tokens.ts.
|
|
6
4
|
*/
|
|
7
5
|
|
|
8
|
-
import type { ColorScheme,
|
|
9
|
-
import
|
|
6
|
+
import type { ColorScheme, WishcraftTokens } from "../../config/types.ts";
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_TOKENS as CANONICAL_DEFAULT_TOKENS,
|
|
9
|
+
colorSchemeFromTokens,
|
|
10
|
+
resolveTokens,
|
|
11
|
+
} from "../../config/tokens.ts";
|
|
10
12
|
|
|
11
|
-
export const DEFAULT_TOKENS: WishcraftTokens =
|
|
12
|
-
surface: "#0f172a",
|
|
13
|
-
surfaceRaised: "#1e293b",
|
|
14
|
-
text: "#f8fafc",
|
|
15
|
-
textMuted: "dim",
|
|
16
|
-
primary: "#f59e0b",
|
|
17
|
-
secondary: "#00afaf",
|
|
18
|
-
accent: "#ea580c",
|
|
19
|
-
success: "success",
|
|
20
|
-
warning: "warning",
|
|
21
|
-
error: "error",
|
|
22
|
-
focus: "#38bdf8",
|
|
23
|
-
selection: "#334155",
|
|
24
|
-
motionDim: "dim",
|
|
25
|
-
motionHot: "#fbbf24",
|
|
26
|
-
motionTrail: "#78350f",
|
|
27
|
-
};
|
|
13
|
+
export const DEFAULT_TOKENS: WishcraftTokens = CANONICAL_DEFAULT_TOKENS;
|
|
28
14
|
|
|
29
|
-
/**
|
|
30
|
-
* Derives legacy SemanticColor configuration from modern WishcraftTokens.
|
|
31
|
-
*/
|
|
32
15
|
export function deriveColorSchemeFromTokens(tokens: WishcraftTokens): Required<ColorScheme> {
|
|
33
|
-
return
|
|
34
|
-
model: tokens.primary,
|
|
35
|
-
shellMode: tokens.accent,
|
|
36
|
-
path: tokens.secondary,
|
|
37
|
-
gitClean: tokens.success,
|
|
38
|
-
gitDirty: tokens.warning,
|
|
39
|
-
thinking: "thinkingOff",
|
|
40
|
-
thinkingMinimal: "thinkingMinimal",
|
|
41
|
-
thinkingLow: "thinkingLow",
|
|
42
|
-
thinkingMedium: "thinkingMedium",
|
|
43
|
-
context: tokens.textMuted,
|
|
44
|
-
contextWarn: tokens.warning,
|
|
45
|
-
contextError: tokens.error,
|
|
46
|
-
cost: tokens.text,
|
|
47
|
-
tokens: tokens.textMuted,
|
|
48
|
-
queue: tokens.accent,
|
|
49
|
-
separator: tokens.textMuted,
|
|
50
|
-
border: tokens.surfaceRaised,
|
|
51
|
-
};
|
|
16
|
+
return colorSchemeFromTokens(tokens);
|
|
52
17
|
}
|
|
53
18
|
|
|
54
|
-
/**
|
|
55
|
-
* Creates WishcraftTokens by merging overrides onto defaults.
|
|
56
|
-
*/
|
|
57
19
|
export function createTokens(overrides?: Partial<WishcraftTokens>): WishcraftTokens {
|
|
58
|
-
return
|
|
59
|
-
...DEFAULT_TOKENS,
|
|
60
|
-
...overrides,
|
|
61
|
-
};
|
|
20
|
+
return resolveTokens(overrides);
|
|
62
21
|
}
|