@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.
@@ -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
- * Fire the configured `powerline.costAlert` warning at most once per session.
87
- * Reads the running cost from the (cached) token ledger so repeated calls are
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 { buildDeckSessionSnapshot } from "./session-snapshot.ts";
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
- const refreshSnapshot = () => buildDeckSessionSnapshot(rt, ctx);
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(refreshSnapshot().appearanceBase),
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(refreshSnapshot().appearanceBase)
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
- refreshSnapshot(),
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 = refreshSnapshot();
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", refreshSnapshot().ideas.length)) return;
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
- export function buildDeckSessionSnapshot(rt: RuntimeState, ctx: any): DeckSessionSnapshot {
15
- const theme = { fg: (_color: string, text: string) => text } as Theme;
16
- let segmentCtx;
17
- try {
18
- segmentCtx = buildSegmentContext(rt, ctx, theme);
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
- export interface DeckSessionSnapshot {
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";
@@ -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 === "success" ||
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
- * src/theme/tokens/mapping.ts
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, SemanticColor } from "../../config/types.ts";
9
- import type { WishcraftTokens } from "./types.ts";
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
  }