@groeponline/pi-wishcraft 1.3.1 → 1.4.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 +2 -0
- package/ROADMAP.md +22 -127
- package/docs/design/vnext-release-plan.md +3 -1
- package/package.json +1 -1
- package/src/config/parse.ts +12 -6
- package/src/config/settings-registry.ts +118 -0
- package/src/config/structural-presets.ts +6 -1
- package/src/core/frontmatter.ts +5 -2
- package/src/extension/contrib/appearance.ts +91 -0
- package/src/extension/contrib/registry.ts +42 -0
- package/src/extension/contrib/types.ts +20 -0
- package/src/extension/core/state.ts +5 -4
- package/src/extension/hooks/hooks-config.ts +6 -1
- package/src/extension/hooks/index.ts +11 -5
- package/src/extension/session/read-hints.ts +6 -2
- package/src/extension/settings/wishcraft-config-items.ts +14 -110
- package/src/extension/settings/wishcraft-config.ts +19 -17
- package/src/extension/skills/inline-invocation.ts +35 -11
- package/src/extension/skills/skill-registry.ts +41 -2
- package/src/extension/ui/deck/routes.ts +17 -2
- package/src/motion/catalog.ts +14 -11
- package/src/motion/policy.ts +3 -2
- package/src/signal/render.ts +49 -6
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import { SETTING_DEFAULTS } from "../../config/settings-registry.ts";
|
|
14
15
|
import type { RuntimeState } from "../core/types.ts";
|
|
15
16
|
import {
|
|
16
17
|
getSettingsPath,
|
|
@@ -44,7 +45,7 @@ let hooksSettings: WishcraftHooksSettings = {};
|
|
|
44
45
|
let hooksEnabled = false;
|
|
45
46
|
let policyRules: PolicyRule[] = [];
|
|
46
47
|
let policyEnabled = false;
|
|
47
|
-
let repairsEnabled =
|
|
48
|
+
let repairsEnabled: boolean = SETTING_DEFAULTS["wishcraft.repairsEnabled"];
|
|
48
49
|
let pendingSessionContext: string | null = null;
|
|
49
50
|
|
|
50
51
|
function refreshSettings(cwd: string): void {
|
|
@@ -59,10 +60,15 @@ function refreshSettings(cwd: string): void {
|
|
|
59
60
|
const policy = parsePolicySettings(globalWishcraft);
|
|
60
61
|
policyRules = policy.rules;
|
|
61
62
|
policyEnabled = policy.enabled;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
(merged as Record<string, unknown>).repairsEnabled
|
|
63
|
+
if (!merged || typeof merged !== "object" || Array.isArray(merged)) {
|
|
64
|
+
repairsEnabled = SETTING_DEFAULTS["wishcraft.repairsEnabled"];
|
|
65
|
+
} else {
|
|
66
|
+
const configured = (merged as Record<string, unknown>).repairsEnabled;
|
|
67
|
+
repairsEnabled =
|
|
68
|
+
typeof configured === "boolean"
|
|
69
|
+
? configured
|
|
70
|
+
: SETTING_DEFAULTS["wishcraft.repairsEnabled"];
|
|
71
|
+
}
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
function hasAnyHook(hooks: WishcraftHooksSettings): boolean {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SETTING_DEFAULTS } from "../../config/settings-registry.ts";
|
|
1
2
|
import { readSettings } from "../settings/settings-io.ts";
|
|
2
3
|
|
|
3
4
|
export interface ReadToolInput {
|
|
@@ -43,9 +44,12 @@ export function readHintsEnabled(wishcraftSettings: unknown): boolean {
|
|
|
43
44
|
typeof wishcraftSettings !== "object" ||
|
|
44
45
|
Array.isArray(wishcraftSettings)
|
|
45
46
|
) {
|
|
46
|
-
return
|
|
47
|
+
return SETTING_DEFAULTS["wishcraft.readHints"];
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
+
const configured = (wishcraftSettings as Record<string, unknown>).readHints;
|
|
50
|
+
return typeof configured === "boolean"
|
|
51
|
+
? configured
|
|
52
|
+
: SETTING_DEFAULTS["wishcraft.readHints"];
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
/** True when core read output already carries an offset/range continuation line. */
|
|
@@ -1,119 +1,23 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/** Data-driven groups for `/wishcraft settings`, derived from the canonical registry. */
|
|
2
|
+
import {
|
|
3
|
+
SETTINGS_REGISTRY,
|
|
4
|
+
SETTING_GROUPS,
|
|
5
|
+
type SettingDefinition,
|
|
6
|
+
type SettingValue,
|
|
7
|
+
} from "../../config/settings-registry.ts";
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export type ConfigValue = boolean | string | number | null;
|
|
10
|
-
|
|
11
|
-
export interface ConfigItem {
|
|
12
|
-
/** Label in the list. */
|
|
13
|
-
label: string;
|
|
14
|
-
/** Path inside settings ("powerline.placement", "wishcraft.hooksEnabled", ...). */
|
|
15
|
-
path: string;
|
|
16
|
-
/** Edit kind. */
|
|
17
|
-
kind: "toggle" | "select" | "text" | "number";
|
|
18
|
-
/** For select: the choices. */
|
|
19
|
-
choices?: string[];
|
|
20
|
-
/** Hint under the group. */
|
|
21
|
-
hint?: string;
|
|
22
|
-
/** Detail explanation (optional). */
|
|
23
|
-
description?: string;
|
|
24
|
-
/** Effective value when the setting is absent (toggles only; defaults to false). */
|
|
25
|
-
default?: boolean;
|
|
26
|
-
}
|
|
9
|
+
export type ConfigValue = SettingValue | null;
|
|
10
|
+
export type ConfigItem = SettingDefinition;
|
|
27
11
|
|
|
28
12
|
export interface ConfigGroup {
|
|
29
13
|
title: string;
|
|
30
14
|
items: ConfigItem[];
|
|
31
15
|
}
|
|
32
16
|
|
|
33
|
-
|
|
34
|
-
"powerline",
|
|
35
|
-
"powerline-thin",
|
|
36
|
-
"slash",
|
|
37
|
-
"pipe",
|
|
38
|
-
"block",
|
|
39
|
-
"none",
|
|
40
|
-
"ascii",
|
|
41
|
-
"dot",
|
|
42
|
-
"chevron",
|
|
43
|
-
"star",
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
/** Build groups from current settings (values shown live). */
|
|
17
|
+
/** Build the operator groups from the canonical registry. */
|
|
47
18
|
export function buildConfigGroups(_settings: Record<string, unknown>): ConfigGroup[] {
|
|
48
|
-
return
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
{ label: "Preset", path: "powerline.preset", kind: "select", choices: ["default", "minimal", "compact", "full", "nerd", "ascii", "chef"] },
|
|
53
|
-
{
|
|
54
|
-
label: "Structural base",
|
|
55
|
-
path: "powerline.appearance.base",
|
|
56
|
-
kind: "select",
|
|
57
|
-
choices: [...STRUCTURAL_PRESET_NAMES],
|
|
58
|
-
hint: "colors and motion for Signal; layout preset stays separate",
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
label: "Motion level",
|
|
62
|
-
path: "powerline.motionLevel",
|
|
63
|
-
kind: "select",
|
|
64
|
-
choices: [...MOTION_LEVELS],
|
|
65
|
-
hint: "full · reduced · functional · off",
|
|
66
|
-
},
|
|
67
|
-
{ label: "Separator", path: "powerline.separator", kind: "select", choices: SEPARATORS },
|
|
68
|
-
{ label: "Placement", path: "powerline.placement", kind: "select", choices: ["above", "below"] },
|
|
69
|
-
{ label: "Path mode", path: "powerline.segmentOptions.path.mode", kind: "select", choices: ["basename", "abbreviated", "full"] },
|
|
70
|
-
{ label: "Path max length", path: "powerline.segmentOptions.path.maxLength", kind: "number", hint: "0 = unlimited" },
|
|
71
|
-
{ label: "Time format", path: "powerline.segmentOptions.time.format", kind: "select", choices: ["12h", "24h"] },
|
|
72
|
-
{ label: "Time seconds", path: "powerline.segmentOptions.time.showSeconds", kind: "toggle" },
|
|
73
|
-
{ label: "Git host icons", path: "powerline.segmentOptions.git.hostIcon", kind: "toggle" },
|
|
74
|
-
{ label: "Git ahead/behind", path: "powerline.segmentOptions.git.showAheadBehind", kind: "toggle" },
|
|
75
|
-
{ label: "Git latest commit", path: "powerline.segmentOptions.git.showCommit", kind: "toggle" },
|
|
76
|
-
{ label: "Context format", path: "powerline.segmentOptions.context.format", kind: "select", choices: ["full", "percent"] },
|
|
77
|
-
{ label: "Cache-read format", path: "powerline.segmentOptions.cache_read.format", kind: "select", choices: ["tokens", "percent", "both"] },
|
|
78
|
-
{ label: "Cost display", path: "powerline.segmentOptions.cost.subscriptionDisplay", kind: "select", choices: ["subscription", "reported-cost", "both"] },
|
|
79
|
-
{ label: "Currency", path: "powerline.segmentOptions.cost.currency", kind: "text" },
|
|
80
|
-
{ label: "Ports include UDP", path: "powerline.segmentOptions.openPorts.includeUdp", kind: "toggle" },
|
|
81
|
-
{ label: "TPS window (ms)", path: "powerline.segmentOptions.tps.windowMs", kind: "number", hint: "default 1000" },
|
|
82
|
-
{ label: "TPS mode", path: "powerline.segmentOptions.tps.mode", kind: "select", choices: ["both", "out", "in"] },
|
|
83
|
-
{ label: "TPS label", path: "powerline.segmentLabels.tps", kind: "text", hint: "empty = no label" },
|
|
84
|
-
],
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
title: "Welcome & vibes",
|
|
88
|
-
items: [
|
|
89
|
-
{ label: "Welcome overlay", path: "powerline.welcome", kind: "toggle", hint: "on = overlay at startup, off = no welcome" },
|
|
90
|
-
{ label: "Animate wishcraft lantern", path: "wishcraft.welcome.animateLantern", kind: "toggle", hint: "flicker on the lantern" },
|
|
91
|
-
],
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
title: "Skills",
|
|
95
|
-
items: [
|
|
96
|
-
{ label: "Inline expand /command and $skill", path: "wishcraft.inlineSkills", kind: "toggle", hint: "needs a restart to take effect" },
|
|
97
|
-
{ label: "Read hints", path: "wishcraft.readHints", kind: "toggle", default: true, hint: "off = no continuation hint after partial reads" },
|
|
98
|
-
],
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
title: "Hooks & repairs (harness)",
|
|
102
|
-
items: [
|
|
103
|
-
{ label: "Hooks enabled", path: "wishcraft.hooksEnabled", kind: "toggle", hint: "preToolUse / postToolUse / sessionStart command hooks" },
|
|
104
|
-
{ label: "Tool-input repairs", path: "wishcraft.repairsEnabled", kind: "toggle", hint: "null-for-optional, auto-link, json-array, path aliases" },
|
|
105
|
-
{ label: "Daily token budget", path: "wishcraft.tokenBudget.daily", kind: "number", hint: "colours the cost segment; never blocks. 0 = off" },
|
|
106
|
-
],
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
title: "Shortcuts",
|
|
110
|
-
items: [
|
|
111
|
-
{ label: "Menu", path: "powerlineShortcuts.menu", kind: "text", hint: "e.g. alt+p" },
|
|
112
|
-
{ label: "Info", path: "powerlineShortcuts.info", kind: "text" },
|
|
113
|
-
{ label: "Stash", path: "powerlineShortcuts.stashHistory", kind: "text" },
|
|
114
|
-
{ label: "Idea", path: "powerlineShortcuts.ideaCapture", kind: "text" },
|
|
115
|
-
{ label: "Queue", path: "powerlineShortcuts.queueOpen", kind: "text" },
|
|
116
|
-
],
|
|
117
|
-
},
|
|
118
|
-
];
|
|
19
|
+
return SETTING_GROUPS.map((group) => ({
|
|
20
|
+
title: group.title,
|
|
21
|
+
items: SETTINGS_REGISTRY.filter((item) => item.group === group.id),
|
|
22
|
+
}));
|
|
119
23
|
}
|
|
@@ -75,7 +75,9 @@ export function assignNestedConfigValue(
|
|
|
75
75
|
return true;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
/** Nested write (new object per level) + persist to settings.json.
|
|
78
|
+
/** Nested write (new object per level) + persist to settings.json.
|
|
79
|
+
* An empty string for a text field means "clear" — the key is removed from settings
|
|
80
|
+
* so the code falls through to the default. */
|
|
79
81
|
export function writeConfigPath(
|
|
80
82
|
cwd: string,
|
|
81
83
|
path: string,
|
|
@@ -112,20 +114,16 @@ function isPrintable(data: string): boolean {
|
|
|
112
114
|
|
|
113
115
|
/** Value shown for a config item given its stored value. */
|
|
114
116
|
export function displayValue(item: ConfigItem, value: ConfigValue): string {
|
|
115
|
-
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (value === null || value === undefined || value === "") return item.kind === "toggle" ? "off" : "—";
|
|
120
|
-
if (item.kind === "toggle") return value ? "on" : "off";
|
|
121
|
-
return String(value);
|
|
117
|
+
const effective = value ?? item.defaultValue ?? null;
|
|
118
|
+
if (effective === null || effective === "") return item.kind === "toggle" ? "off" : "—";
|
|
119
|
+
if (item.kind === "toggle") return effective === true ? "on" : "off";
|
|
120
|
+
return String(effective);
|
|
122
121
|
}
|
|
123
122
|
|
|
124
|
-
/** Next stored boolean after toggling `item` from its current
|
|
123
|
+
/** Next stored boolean after toggling `item` from its current effective value. */
|
|
125
124
|
export function nextToggleValue(item: ConfigItem, value: ConfigValue): boolean {
|
|
126
|
-
const effective =
|
|
127
|
-
|
|
128
|
-
return !effective;
|
|
125
|
+
const effective = value ?? item.defaultValue ?? false;
|
|
126
|
+
return effective !== true;
|
|
129
127
|
}
|
|
130
128
|
|
|
131
129
|
function coerce(item: ConfigItem, current: ConfigValue, next: string): ConfigValue {
|
|
@@ -190,9 +188,11 @@ export async function showWishcraftConfig(rt: RuntimeState, ctx: any): Promise<v
|
|
|
190
188
|
};
|
|
191
189
|
|
|
192
190
|
const cycleSelect = (item: ConfigItem, forward: boolean) => {
|
|
191
|
+
if (item.kind !== "select") return;
|
|
193
192
|
const cur = readConfigPath(settings, item.path);
|
|
194
|
-
const list = item.choices
|
|
195
|
-
const
|
|
193
|
+
const list = item.choices;
|
|
194
|
+
const effective = cur ?? item.defaultValue ?? list[0];
|
|
195
|
+
const idx = list.indexOf(String(effective));
|
|
196
196
|
const next = list[(idx + (forward ? 1 : list.length - 1) + list.length) % list.length]!;
|
|
197
197
|
const ok = writeConfigPath(cwd, item.path, next);
|
|
198
198
|
settings = readSettings(cwd);
|
|
@@ -208,14 +208,15 @@ export async function showWishcraftConfig(rt: RuntimeState, ctx: any): Promise<v
|
|
|
208
208
|
|
|
209
209
|
const toggle = (item: ConfigItem) => {
|
|
210
210
|
const cur = readConfigPath(settings, item.path);
|
|
211
|
-
const
|
|
211
|
+
const next = nextToggleValue(item, cur);
|
|
212
|
+
const ok = writeConfigPath(cwd, item.path, next);
|
|
212
213
|
settings = readSettings(cwd);
|
|
213
214
|
groups = buildConfigGroups(settings);
|
|
214
215
|
if (item.path.startsWith("powerline")) {
|
|
215
216
|
reloadPowerlineFromSettings(rt, settings);
|
|
216
217
|
}
|
|
217
218
|
ctx.ui.notify(
|
|
218
|
-
ok ? `${item.label}: ${
|
|
219
|
+
ok ? `${item.label}: ${next ? "on" : "off"} (saved)` : `${item.label} not saved`,
|
|
219
220
|
ok ? "info" : "warning",
|
|
220
221
|
);
|
|
221
222
|
};
|
|
@@ -282,7 +283,8 @@ export async function showWishcraftConfig(rt: RuntimeState, ctx: any): Promise<v
|
|
|
282
283
|
editing = false;
|
|
283
284
|
editBuffer = "";
|
|
284
285
|
} else if (matchesKey(data, "enter")) {
|
|
285
|
-
if (
|
|
286
|
+
// Save even if empty — allows clearing a field (e.g. remove a shortcut binding)
|
|
287
|
+
applyEdit(editBuffer.trim());
|
|
286
288
|
editing = false;
|
|
287
289
|
editBuffer = "";
|
|
288
290
|
} else if (matchesKey(data, "backspace")) {
|
|
@@ -17,6 +17,7 @@ import { logDiscoveryError } from "../../welcome/discover.ts";
|
|
|
17
17
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
18
|
import type { RuntimeState } from "../core/types.ts";
|
|
19
19
|
import {
|
|
20
|
+
getAvailableCommands,
|
|
20
21
|
getAvailableSkills,
|
|
21
22
|
invalidateSkillCache,
|
|
22
23
|
recordSkillUsage,
|
|
@@ -59,32 +60,51 @@ function isExcluded(index: number, ranges: Array<[number, number]>): boolean {
|
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
/**
|
|
62
|
-
* Expandeer
|
|
63
|
+
* Expandeer inline triggers — /command (trigger-based) en $skill (name-based)
|
|
64
|
+
* via gescheiden lookups.
|
|
65
|
+
*
|
|
66
|
+
* /command → expandeert ALLEEN als de skill `trigger: /command` heeft
|
|
67
|
+
* $skill → expandeert op basis van `name:` in frontmatter
|
|
68
|
+
*
|
|
69
|
+
* Skills zonder `trigger:` reageren niet op /-prefix, en skills met
|
|
70
|
+
* `trigger:` reageren op de exacte trigger-naam (zonder /-prefix).
|
|
63
71
|
*/
|
|
64
72
|
export function expandInlineTriggers(text: string): string {
|
|
65
73
|
const availableSkills = getAvailableSkills();
|
|
74
|
+
const availableCommands = getAvailableCommands();
|
|
66
75
|
|
|
67
|
-
// Pattern for both /command and $skill
|
|
68
|
-
const TRIGGER_REGEX = /(^|[\s(])(\/|\$)([a-zA-Z0-9_-]+)/g;
|
|
69
76
|
const excluded = findExcludedRanges(text);
|
|
70
77
|
|
|
78
|
+
// ── Match /command (trigger-based) ──
|
|
79
|
+
const SLASH_REGEX = /(^|[\s(])\/([a-zA-Z0-9_-]+)/g;
|
|
71
80
|
const matches: Array<{
|
|
72
81
|
start: number;
|
|
73
82
|
end: number;
|
|
74
83
|
full: string;
|
|
75
84
|
name: string;
|
|
76
85
|
}> = [];
|
|
77
|
-
let match: RegExpExecArray | null;
|
|
78
86
|
|
|
79
|
-
|
|
80
|
-
|
|
87
|
+
let match: RegExpExecArray | null;
|
|
88
|
+
while ((match = SLASH_REGEX.exec(text)) !== null) {
|
|
81
89
|
if (isExcluded(match.index, excluded)) continue;
|
|
90
|
+
const name = match[2]!;
|
|
91
|
+
// Alleen expanden als er een skill met matching trigger is
|
|
92
|
+
if (!availableCommands.has(name)) continue;
|
|
93
|
+
matches.push({
|
|
94
|
+
start: match.index + match[1]!.length,
|
|
95
|
+
end: match.index + match[0].length,
|
|
96
|
+
full: match[0],
|
|
97
|
+
name,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
82
100
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
101
|
+
// ── Match $skill (name-based) ──
|
|
102
|
+
const DOLLAR_REGEX = /(^|[\s(])\$([a-zA-Z0-9_-]+)/g;
|
|
103
|
+
while ((match = DOLLAR_REGEX.exec(text)) !== null) {
|
|
104
|
+
if (isExcluded(match.index, excluded)) continue;
|
|
105
|
+
const name = match[2]!;
|
|
106
|
+
// Alleen expanden als de skill-naam bekend is
|
|
86
107
|
if (!availableSkills.has(name)) continue;
|
|
87
|
-
|
|
88
108
|
matches.push({
|
|
89
109
|
start: match.index + match[1]!.length,
|
|
90
110
|
end: match.index + match[0].length,
|
|
@@ -107,6 +127,10 @@ export function expandInlineTriggers(text: string): string {
|
|
|
107
127
|
}
|
|
108
128
|
}
|
|
109
129
|
|
|
130
|
+
// /command lookups use command map, $skill lookups use skill map
|
|
131
|
+
const lookupMap = (m: { full: string }) =>
|
|
132
|
+
m.full.startsWith("/") ? availableCommands : availableSkills;
|
|
133
|
+
|
|
110
134
|
// Rebuild the text with expansions
|
|
111
135
|
let result = "";
|
|
112
136
|
let cursor = 0;
|
|
@@ -114,7 +138,7 @@ export function expandInlineTriggers(text: string): string {
|
|
|
114
138
|
for (const m of deduped) {
|
|
115
139
|
result += text.slice(cursor, m.start);
|
|
116
140
|
|
|
117
|
-
const filePath =
|
|
141
|
+
const filePath = lookupMap(m).get(m.name)!;
|
|
118
142
|
try {
|
|
119
143
|
const rawContent = readFileSync(filePath, "utf8");
|
|
120
144
|
const cleanContent = stripFrontmatter(rawContent);
|
|
@@ -37,6 +37,8 @@ export interface SkillEntry {
|
|
|
37
37
|
mtimeMs: number;
|
|
38
38
|
/** All frontmatter keys (raw, including unknown). */
|
|
39
39
|
frontmatterKeys: string[];
|
|
40
|
+
/** Frontmatter trigger value (e.g. /test, /showcase). */
|
|
41
|
+
trigger: string | null;
|
|
40
42
|
/** Diagnostic message (core diagnostics, or empty description). */
|
|
41
43
|
warning?: string;
|
|
42
44
|
}
|
|
@@ -82,6 +84,7 @@ function scanLooseExtraFiles(
|
|
|
82
84
|
lineCount: content.split("\n").length,
|
|
83
85
|
mtimeMs,
|
|
84
86
|
frontmatterKeys: parseFrontmatterKeys(content),
|
|
87
|
+
trigger: null,
|
|
85
88
|
warning: "no description — the model will not see this skill in the prompt",
|
|
86
89
|
});
|
|
87
90
|
knownPaths.add(filePath);
|
|
@@ -95,6 +98,7 @@ const CACHE_TTL_MS = 30_000;
|
|
|
95
98
|
let cachedAt = 0;
|
|
96
99
|
let cachedEntries: SkillEntry[] | null = null;
|
|
97
100
|
let cachedPathMap: Map<string, string> | null = null;
|
|
101
|
+
let cachedTriggerMap: Map<string, string> | null = null;
|
|
98
102
|
let cachedCwd: string | null = null;
|
|
99
103
|
let onCacheInvalidated: (() => void) | null = null;
|
|
100
104
|
|
|
@@ -110,6 +114,7 @@ export function invalidateSkillCache(): void {
|
|
|
110
114
|
cachedAt = 0;
|
|
111
115
|
cachedEntries = null;
|
|
112
116
|
cachedPathMap = null;
|
|
117
|
+
cachedTriggerMap = null;
|
|
113
118
|
cachedCwd = null;
|
|
114
119
|
onCacheInvalidated?.();
|
|
115
120
|
}
|
|
@@ -237,6 +242,7 @@ function buildRejectedSkillEntries(
|
|
|
237
242
|
lineCount,
|
|
238
243
|
mtimeMs,
|
|
239
244
|
frontmatterKeys: parseFrontmatterKeys(content),
|
|
245
|
+
trigger: null,
|
|
240
246
|
warning: message,
|
|
241
247
|
});
|
|
242
248
|
knownPaths.add(filePath);
|
|
@@ -283,6 +289,7 @@ export function loadSkillCatalog(cwd: string = process.cwd()): SkillEntry[] {
|
|
|
283
289
|
if (!warning && s.filePath && diagByPath.has(s.filePath)) {
|
|
284
290
|
warning = diagByPath.get(s.filePath);
|
|
285
291
|
}
|
|
292
|
+
const fm = content ? parseSkillFrontmatter(content) : { name: null, description: null, trigger: null };
|
|
286
293
|
return {
|
|
287
294
|
name: s.name,
|
|
288
295
|
description: s.description,
|
|
@@ -295,6 +302,7 @@ export function loadSkillCatalog(cwd: string = process.cwd()): SkillEntry[] {
|
|
|
295
302
|
lineCount,
|
|
296
303
|
mtimeMs,
|
|
297
304
|
frontmatterKeys: parseFrontmatterKeys(content),
|
|
305
|
+
trigger: fm.trigger ?? null,
|
|
298
306
|
warning,
|
|
299
307
|
};
|
|
300
308
|
});
|
|
@@ -305,6 +313,25 @@ export function loadSkillCatalog(cwd: string = process.cwd()): SkillEntry[] {
|
|
|
305
313
|
...entries.map((e) => e.filePath),
|
|
306
314
|
...loose.map((e) => e.filePath),
|
|
307
315
|
]);
|
|
316
|
+
|
|
317
|
+
// ── Name dedup: prefer project > global > prompts > extra ──
|
|
318
|
+
// When the same skill name appears in multiple roots (e.g. both
|
|
319
|
+
// ~/.pi/agent/skills/test/ and ./.pi/skills/test/), keep only the
|
|
320
|
+
// highest-priority entry so the catalog never shows doubles.
|
|
321
|
+
const NAME_PRIORITY: Record<SkillCategory, number> = {
|
|
322
|
+
project: 0,
|
|
323
|
+
global: 1,
|
|
324
|
+
prompts: 2,
|
|
325
|
+
extra: 3,
|
|
326
|
+
};
|
|
327
|
+
const seen = new Map<string, { prio: number; entry: SkillEntry }>();
|
|
328
|
+
for (const e of entries) {
|
|
329
|
+
const prio = NAME_PRIORITY[e.category] ?? 4;
|
|
330
|
+
const cur = seen.get(e.name);
|
|
331
|
+
if (!cur || prio < cur.prio) seen.set(e.name, { prio, entry: e });
|
|
332
|
+
}
|
|
333
|
+
const deduped = [...seen.values()].map((v) => v.entry);
|
|
334
|
+
|
|
308
335
|
const rejected = buildRejectedSkillEntries(
|
|
309
336
|
result.diagnostics,
|
|
310
337
|
catalogPaths,
|
|
@@ -314,20 +341,32 @@ export function loadSkillCatalog(cwd: string = process.cwd()): SkillEntry[] {
|
|
|
314
341
|
cachedAt = now;
|
|
315
342
|
cachedCwd = cwd;
|
|
316
343
|
cachedEntries = [
|
|
317
|
-
...
|
|
344
|
+
...deduped.filter((e) => !looseNames.has(e.name)),
|
|
318
345
|
...loose,
|
|
319
346
|
...rejected,
|
|
320
347
|
].sort((a, b) => a.name.localeCompare(b.name));
|
|
321
348
|
cachedPathMap = new Map(cachedEntries.map((e) => [e.name, e.filePath] as const));
|
|
349
|
+
cachedTriggerMap = new Map(
|
|
350
|
+
cachedEntries
|
|
351
|
+
.filter((e) => e.trigger !== null)
|
|
352
|
+
.map((e) => [e.trigger!, e.filePath] as const),
|
|
353
|
+
);
|
|
322
354
|
return cachedEntries;
|
|
323
355
|
}
|
|
324
356
|
|
|
325
|
-
/** Compat: name → file path (for inline-invocation). */
|
|
357
|
+
/** Compat: name → file path (for inline-invocation $skill). */
|
|
326
358
|
export function getAvailableSkills(): Map<string, string> {
|
|
327
359
|
loadSkillCatalog();
|
|
328
360
|
return cachedPathMap ?? new Map();
|
|
329
361
|
}
|
|
330
362
|
|
|
363
|
+
/** Compat: trigger → file path (for inline-invocation /command).
|
|
364
|
+
* Only skills with a `trigger: /command` frontmatter field are returned. */
|
|
365
|
+
export function getAvailableCommands(): Map<string, string> {
|
|
366
|
+
loadSkillCatalog();
|
|
367
|
+
return cachedTriggerMap ?? new Map();
|
|
368
|
+
}
|
|
369
|
+
|
|
331
370
|
// ---------------------------------------------------------------------------
|
|
332
371
|
// Usage-ledger (~/.pi/agent/skill-usage.json)
|
|
333
372
|
// ---------------------------------------------------------------------------
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { DeckRoute, DeckRouteDef } from "./types.ts";
|
|
2
2
|
import { DECK_ROUTES } from "./types.ts";
|
|
3
|
+
import { getContributedDeckRoutes } from "../../contrib/registry.ts";
|
|
3
4
|
|
|
4
|
-
export const
|
|
5
|
+
export const BUILTIN_DECK_ROUTE_DEFS: readonly DeckRouteDef[] = [
|
|
5
6
|
{ id: "home", label: "Home", jumpKey: "h", description: "Session overview and next intent" },
|
|
6
7
|
{ id: "signal", label: "Signal", jumpKey: "s", description: "Lane layout and live activity" },
|
|
7
8
|
{ id: "skills", label: "Skills", jumpKey: "k", description: "Catalog and health diagnostics" },
|
|
@@ -15,8 +16,22 @@ export const DECK_ROUTE_DEFS: readonly DeckRouteDef[] = [
|
|
|
15
16
|
{ id: "diagnostics", label: "Diagnostics", jumpKey: "d", description: "Environment and capability checks" },
|
|
16
17
|
];
|
|
17
18
|
|
|
19
|
+
export const DECK_ROUTE_DEFS: readonly DeckRouteDef[] = BUILTIN_DECK_ROUTE_DEFS;
|
|
20
|
+
|
|
21
|
+
export function getAllDeckRouteDefs(): readonly DeckRouteDef[] {
|
|
22
|
+
const contributed = getContributedDeckRoutes().map((r) => ({
|
|
23
|
+
id: r.id as DeckRoute,
|
|
24
|
+
label: r.label,
|
|
25
|
+
jumpKey: r.jumpKey ?? "",
|
|
26
|
+
description: r.description ?? "",
|
|
27
|
+
}));
|
|
28
|
+
// ponytail: no dedup beyond registry — built-ins win, contributed appended
|
|
29
|
+
return [...BUILTIN_DECK_ROUTE_DEFS, ...contributed];
|
|
30
|
+
}
|
|
31
|
+
|
|
18
32
|
export function isDeckRoute(value: string): value is DeckRoute {
|
|
19
|
-
|
|
33
|
+
if ((DECK_ROUTES as readonly string[]).includes(value)) return true;
|
|
34
|
+
return getContributedDeckRoutes().some((r) => r.id === value);
|
|
20
35
|
}
|
|
21
36
|
|
|
22
37
|
export function parseDeckRouteArg(args: string | undefined): DeckRoute {
|
package/src/motion/catalog.ts
CHANGED
|
@@ -20,7 +20,8 @@ const CORE_MOTIONS: readonly MotionDef[] = [
|
|
|
20
20
|
colorRole: "motionHot",
|
|
21
21
|
fallbackGlyph: "*",
|
|
22
22
|
loop: "while-active",
|
|
23
|
-
|
|
23
|
+
// ponytail: vivid — longer trail + faster pulse, travels the full Signal rail
|
|
24
|
+
generator: { geometry: "ember", trail: 6, direction: "forward", intervalMs: 62, ease: "pulse" },
|
|
24
25
|
description: "Lanternwake signature. A warm ember travels the connected segments, only while work happens.",
|
|
25
26
|
},
|
|
26
27
|
{
|
|
@@ -29,10 +30,11 @@ const CORE_MOTIONS: readonly MotionDef[] = [
|
|
|
29
30
|
category: "wishcraft",
|
|
30
31
|
kind: "frames",
|
|
31
32
|
channels: ["workingGlyph", "ambient"],
|
|
32
|
-
colorRole: "
|
|
33
|
+
colorRole: "motionHot",
|
|
33
34
|
fallbackGlyph: "o",
|
|
34
35
|
loop: "ambient",
|
|
35
|
-
|
|
36
|
+
// ponytail: vivid ambient — higher contrast, tighter loop so idle actually breathes
|
|
37
|
+
frames: ["◌", "◎", "◈", "⬡", "◈", "◎"],
|
|
36
38
|
description: "Soft phase drift for minimal presets.",
|
|
37
39
|
},
|
|
38
40
|
{
|
|
@@ -56,7 +58,7 @@ const CORE_MOTIONS: readonly MotionDef[] = [
|
|
|
56
58
|
colorRole: "motionHot",
|
|
57
59
|
fallbackGlyph: "#",
|
|
58
60
|
loop: "while-active",
|
|
59
|
-
generator: { geometry: "heat", trail:
|
|
61
|
+
generator: { geometry: "heat", trail: 7, direction: "forward", intervalMs: 55, ease: "linear" },
|
|
60
62
|
description: "Heat propagates block by block toward the active tool.",
|
|
61
63
|
},
|
|
62
64
|
{
|
|
@@ -125,10 +127,10 @@ const CORE_MOTIONS: readonly MotionDef[] = [
|
|
|
125
127
|
category: "matrix",
|
|
126
128
|
kind: "generator",
|
|
127
129
|
channels: ["workingGlyph", "signal", "ambient"],
|
|
128
|
-
colorRole: "
|
|
130
|
+
colorRole: "motionHot",
|
|
129
131
|
fallbackGlyph: "O",
|
|
130
132
|
loop: "ambient",
|
|
131
|
-
generator: { geometry: "orbit", radius:
|
|
133
|
+
generator: { geometry: "orbit", radius: 3, trail: 5, intervalMs: 150, ease: "breathe" },
|
|
132
134
|
description: "Orbit and phase motion, slow enough for night work.",
|
|
133
135
|
},
|
|
134
136
|
{
|
|
@@ -276,13 +278,14 @@ export const CHANNEL_MATRIX: Record<MotionEvent, readonly MotionChannel[]> = {
|
|
|
276
278
|
};
|
|
277
279
|
|
|
278
280
|
/** Documented cadence bands per channel. */
|
|
281
|
+
// ponytail: tightened for vivid motion — signal/glyph sweep at ~60-80ms, ambient breathes at ~180ms
|
|
279
282
|
export const CADENCE_MS: Record<MotionChannel, { min: number; max: number }> = {
|
|
280
|
-
workingGlyph: { min:
|
|
281
|
-
signal: { min:
|
|
283
|
+
workingGlyph: { min: 55, max: 85 },
|
|
284
|
+
signal: { min: 55, max: 85 },
|
|
282
285
|
deckTransient: { min: 50, max: 100 },
|
|
283
|
-
panelIndicator: { min:
|
|
284
|
-
borderEmphasis: { min:
|
|
285
|
-
ambient: { min:
|
|
286
|
+
panelIndicator: { min: 100, max: 200 },
|
|
287
|
+
borderEmphasis: { min: 180, max: 350 },
|
|
288
|
+
ambient: { min: 180, max: 400 },
|
|
286
289
|
};
|
|
287
290
|
|
|
288
291
|
/** Gallery previews are allowed to run hotter than production channels. */
|
package/src/motion/policy.ts
CHANGED
|
@@ -80,9 +80,10 @@ export function targetFps(
|
|
|
80
80
|
? allowed.filter((channel) => consumers.includes(channel))
|
|
81
81
|
: allowed;
|
|
82
82
|
if (live.length === 0) return 0;
|
|
83
|
-
|
|
83
|
+
// ponytail: vivid idle breathes visibly; active sweeps at 16 FPS
|
|
84
|
+
if (live.length === 1 && live[0] === "ambient") return 8;
|
|
84
85
|
if (effectiveLevel(policy) === "reduced") return 3;
|
|
85
|
-
return
|
|
86
|
+
return 16;
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
/** Color transitions are suppressed without color; glyph motion may stay. */
|