@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
package/CHANGELOG.md
CHANGED
package/ROADMAP.md
CHANGED
|
@@ -1,146 +1,41 @@
|
|
|
1
1
|
# Roadmap — pi-wishcraft
|
|
2
2
|
|
|
3
|
-
Baseline: **v1.
|
|
4
|
-
|
|
5
|
-
Pi is the engine. Wishcraft is the operator experience layer: Signal, Deck, skills, ideas, shell UX, hooks, repairs and policy without forking Pi core.
|
|
6
|
-
|
|
7
|
-
Historical 0.x/1.0/vNext campaign detail belongs in `CHANGELOG.md` and `docs/design/`. This file describes only the current product contract and work that is still open.
|
|
8
|
-
|
|
9
|
-
## Product boundary
|
|
10
|
-
|
|
11
|
-
Wishcraft may extend the Pi experience, but it does not become:
|
|
12
|
-
|
|
13
|
-
- a Pi fork or second agent engine;
|
|
14
|
-
- a fleet/orchestration control plane;
|
|
15
|
-
- a SaaS integrations bundle;
|
|
16
|
-
- a second plugin runtime competing with Pi extensions;
|
|
17
|
-
- a telemetry collector by default.
|
|
18
|
-
|
|
19
|
-
External systems should connect through small contribution/integration contracts. Vendor-specific control planes belong outside Wishcraft.
|
|
3
|
+
Baseline: **v1.3.1** (`e79737f`, tag `v1.3.1` 2026-08-25, npm `1.3.1`) — `origin/main` @ `0044a87` (vivid). Historical vNext PR0-8 detail is archived in `CHANGELOG.md` and `docs/design/vnext-overview.md`; this file is the current contract.
|
|
20
4
|
|
|
21
5
|
## Release line
|
|
22
6
|
|
|
23
|
-
| Release | Theme | State | Done means |
|
|
24
|
-
| --- | --- | --- | --- |
|
|
25
|
-
| **1.2** | Operator Layer | shipped | Deck,
|
|
26
|
-
| **1.3** | Hardening |
|
|
27
|
-
| **1.4** | Extension Contract |
|
|
28
|
-
| **1.5** | Craft Ecosystem |
|
|
29
|
-
| **2.0** | Stable
|
|
30
|
-
|
|
31
|
-
## 1.3 — Hardening
|
|
32
|
-
|
|
33
|
-
No feature dump. This release exists to make the v1.2 surface trustworthy.
|
|
34
|
-
|
|
35
|
-
### P0 — release integrity
|
|
36
|
-
|
|
37
|
-
- One reusable `Verify` contract for PR verification and release gating.
|
|
38
|
-
- Verify includes whitespace, typecheck, unit tests, circular dependency check and Pi package contract.
|
|
39
|
-
- Release jobs depend on Verify and re-check the synchronized `origin/main` tree before tagging.
|
|
40
|
-
- `main` should require the Verify check in repository rules before merge.
|
|
41
|
-
|
|
42
|
-
### P0 — render/runtime performance
|
|
43
|
-
|
|
44
|
-
- Deck paint must not run filesystem-backed skill discovery, skill doctor or settings parsing.
|
|
45
|
-
- Expensive Deck data is cached as static snapshot state and refreshed only on open, relevant navigation or mutation.
|
|
46
|
-
- Live model/Git/context/queue/Signal state remains cheap to repaint.
|
|
47
|
-
- Idle motion stays 0 FPS.
|
|
48
|
-
|
|
49
|
-
### P0 — appearance consistency
|
|
50
|
-
|
|
51
|
-
- `src/config/types.ts` + `src/config/tokens.ts` are the canonical token contract.
|
|
52
|
-
- `src/theme/tokens/*` is compatibility-only; it may re-export but may not define a second palette or semantic mapping.
|
|
53
|
-
- One structural base must paint the same semantic roles regardless of the configuration path used to select it.
|
|
54
|
-
|
|
55
|
-
### P0 — Signal contract
|
|
56
|
-
|
|
57
|
-
- Signal is the universal status renderer for legacy and structural presets.
|
|
58
|
-
- Legacy presets keep their existing segment/layout/color contract unless a structural appearance layer is explicitly selected.
|
|
59
|
-
- Terminal one-shots (`success`, `warning`, `error`) settle to `idle/ready` after their finite burst.
|
|
60
|
-
- Reduced/off/screen-reader modes communicate state without requiring animation.
|
|
61
|
-
|
|
62
|
-
### P1 — verification matrix
|
|
63
|
-
|
|
64
|
-
Add/keep regression coverage for:
|
|
65
|
-
|
|
66
|
-
- widths around 40 / 80 / 120+ columns;
|
|
67
|
-
- ASCII, Nerd Font and `NO_COLOR` rendering;
|
|
68
|
-
- full / reduced / functional / off motion;
|
|
69
|
-
- Deck render hot-path invariants;
|
|
70
|
-
- legacy preset color compatibility;
|
|
71
|
-
- release-gate dependency order;
|
|
72
|
-
- Linux and macOS system-segment parsing.
|
|
73
|
-
|
|
74
|
-
## 1.4 — Extension Contract
|
|
75
|
-
|
|
76
|
-
The next architecture step is **composability**, not more built-in routes.
|
|
77
|
-
|
|
78
|
-
### Typed settings registry
|
|
79
|
-
|
|
80
|
-
One registry should drive:
|
|
81
|
-
|
|
82
|
-
```text
|
|
83
|
-
settings definition
|
|
84
|
-
│
|
|
85
|
-
├─ parsing + validation
|
|
86
|
-
├─ defaults + migrations
|
|
87
|
-
├─ /wishcraft settings
|
|
88
|
-
├─ Pi settings contribution (where supported)
|
|
89
|
-
└─ generated documentation/examples
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
This removes duplicate knowledge about settings and enables a real zero-config first run.
|
|
7
|
+
| Release | Theme | State | Done means | Pin |
|
|
8
|
+
| --- | --- | --- | --- | --- |
|
|
9
|
+
| **1.2** | Operator Layer | shipped | Deck, Signal 3-lane, 10 bases, gallery/composer, workbench, a11y | tag `v1.2.0` `65e62fd` (784faaa merge) |
|
|
10
|
+
| **1.3** | Hardening | shipped | Verify contract, 0 FPS idle, token SSOT, renderer/lifecycle, regression | `f352b38`→`ecb2d5e` v1.3.0, `e79737f` v1.3.1, RC `v1.3.2-cea7e3e` pending |
|
|
11
|
+
| **1.4** | Extension Contract | **current** | 1.4a-d below; no new runtime | `0bfa5a2` registry on main, `62d6156` contrib on `feat/contribution-api`, `0044a87` vivid on main |
|
|
12
|
+
| **1.5** | Craft Ecosystem | next | skill install via Pi-native dist + recipes, built on 1.4 contract | starts after 1.4c Signal wiring green |
|
|
13
|
+
| **2.0** | Stable Platform | target | compat policy, migrations, perf budgets, stable contracts, provenance | after 1.4a-d + 1.3-P1 |
|
|
93
14
|
|
|
94
|
-
|
|
15
|
+
## 1.4 — Extension Contract (current, split)
|
|
95
16
|
|
|
96
|
-
|
|
17
|
+
**1.4a — Settings registry** — *shipped* `0bfa5a2` `feat(config): typed settings registry` on `origin/main`. `src/config/settings-registry.ts` (28 defs, `SETTING_DEFAULTS`, `get/validate` O(1) maps, `SETTING_GROUPS`). `src/extension/settings/wishcraft-config-items.ts` now renders from registry (124 lines removed). Tests `tests/settings-registry.test.ts` + `wishcraft-config.test.ts` green (557). Still TODO: `contributeSettings()` Pi-contribution + `defaults/migrations` + docgen from registry.
|
|
97
18
|
|
|
98
|
-
|
|
99
|
-
registerDeckRoute()
|
|
100
|
-
registerSignalSource()
|
|
101
|
-
registerMotion()
|
|
102
|
-
registerAppearanceContribution()
|
|
103
|
-
registerRecipeOrAction()
|
|
104
|
-
contributeSettings()
|
|
105
|
-
```
|
|
19
|
+
**1.4b — Contribution API** — *pending* `62d6156` `feat(contract): typed contribution API` on `origin/feat/contribution-api` (not yet on `origin/main`). `src/extension/contrib/{types,registry}.ts` ships `registerDeckRoute`/`registerSignalSource` (validate `id /^[a-z0-9][a-z0-9-_]*$/`, dedup, `false` never throw). Deck wired via `src/extension/ui/deck/routes.ts` `getAllDeckRouteDefs()`/`isDeckRoute()`. Still TODO: `registerMotion()`, `registerAppearanceContribution()`, `registerRecipeOrAction()` as separate slices (one PR per contract).
|
|
106
20
|
|
|
107
|
-
|
|
21
|
+
**1.4c — Signal wiring** — *next* after 1.4b merge. `src/signal/render.ts` must call `getContributedSignalSources()` with per-source fault isolation (`try/catch` → `null`), versioned capability scope. Done when both Deck and Signal contributions render without taking down the bar (`tests/contrib-registry.test.ts` + `signal.test.ts` + fault test).
|
|
108
22
|
|
|
109
|
-
-
|
|
110
|
-
- Contributions are data/callback contracts, not arbitrary nested plugin loaders.
|
|
111
|
-
- Public contracts are versioned and capability-scoped.
|
|
112
|
-
- A failing contribution cannot take down Signal or the Deck.
|
|
23
|
+
**1.4d — Preset composer** — Decouple `PresetDef` `tokens/chrome/signal/motion/deck/welcome/glyphs` from `src/config/structural-presets.ts` to allow `contributedAppearance` overrides per preset with validation. Done when `tests/structural-presets.test.ts` + `tokens.test.ts` cover contributed palette/motion.
|
|
113
24
|
|
|
114
|
-
|
|
25
|
+
**Gate for 1.4 done:** `npm run typecheck && npm test && npx madge --circular src` green, `isDeckRoute` + Signal sources both isolated, registry `contributeSettings` tested, no new runtime.
|
|
115
26
|
|
|
116
|
-
|
|
27
|
+
## 1.3 — Hardening (shipped, pending tags)
|
|
117
28
|
|
|
118
|
-
-
|
|
119
|
-
- curated recipes/actions that compose existing Wishcraft/Pi capabilities;
|
|
120
|
-
- package discovery surfaced through Pi-native package distribution rather than a parallel Wishcraft marketplace;
|
|
121
|
-
- export/import of appearance and motion recipes with validation.
|
|
29
|
+
P0 Verify: `whitespace + tsc --noEmit + npm test + madge --circular + scripts/verify-pi-package-contract.mjs`. Release depends on Verify and re-checks `origin/main` before tag. `main` requires `Verify` (repo rules). Promote flow is **official**: `release-candidate/v*` + `promote-release-candidate.yml` (`65143ae`) → tag → npm. P1 matrix still open: widths 40/80/120+, ASCII/Nerd/`NO_COLOR`, `full/reduced/functional/off`, Deck hot-path (no fs in paint), legacy colors, gate order, Linux/macOS segments. Pending RCs: `v1.3.2-cea7e3e` (`ff68fff`), `v1.4.0-0bfa5a2` (`529bc10`), `v1.5.0-0044a87` — collapse into single `1.4.0` promote after 1.4c.
|
|
122
30
|
|
|
123
|
-
##
|
|
31
|
+
## 1.5 — Craft Ecosystem (next)
|
|
124
32
|
|
|
125
|
-
|
|
33
|
+
Starts after 1.4c wiring green: skill install from Pi-native dist (no parallel marketplace), curated recipes. One paragraph; detail deferred.
|
|
126
34
|
|
|
127
|
-
|
|
35
|
+
## 2.0 — Stable Platform (target)
|
|
128
36
|
|
|
129
|
-
-
|
|
130
|
-
- settings migrations with round-trip tests;
|
|
131
|
-
- measured render/performance budgets;
|
|
132
|
-
- stable contribution API with fault isolation;
|
|
133
|
-
- release provenance and required repository checks;
|
|
134
|
-
- docs generated from canonical contracts where practical;
|
|
135
|
-
- no known duplicate sources of truth for tokens, settings or release verification.
|
|
37
|
+
Needs: compat/deprecation policy, migrations with round-trip tests, perf budgets, stable contribution API with fault isolation, provenance via required `Verify`, docs from canonical contracts, no duplicate SSOTs.
|
|
136
38
|
|
|
137
|
-
## Always-on
|
|
39
|
+
## Always-on
|
|
138
40
|
|
|
139
|
-
|
|
140
|
-
- No synchronous discovery work in paint/render loops.
|
|
141
|
-
- No background animation when nothing consumes it.
|
|
142
|
-
- Core Pi tools are not silently rewritten by Wishcraft repairs.
|
|
143
|
-
- Policies and hooks remain explicitly disableable.
|
|
144
|
-
- Security-sensitive paths fail closed.
|
|
145
|
-
- Package/release verification runs before npm publication.
|
|
146
|
-
- New features must fit the operator-layer boundary above.
|
|
41
|
+
English UI, no sync discovery in paint, no background animation when idle (0 FPS, ambient 8 FPS max vivid), no silent Pi core rewrites, policies/hooks disableable, fail closed, Verify before npm, operator-layer boundary.
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
# Wishcraft vNext — Stacked PR Release Plan
|
|
1
|
+
# Wishcraft vNext — Stacked PR Release Plan — ARCHIVED (all 8 landed)
|
|
2
|
+
|
|
3
|
+
> **Archived 2026-08-26:** All 8 PRs landed via `784faaa feat: merge Wishcraft vNext operator layer` → `65e62fd v1.2.0` (plus hardening `f352b38`/`ecb2d5e`/`e79737f` and vivid `0044a87`). Historical detail only; active plan is `ROADMAP.md` 1.4a-d. Do not use this as future todo.
|
|
2
4
|
|
|
3
5
|
## Goal
|
|
4
6
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@groeponline/pi-wishcraft",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Operator cockpit for Pi: live powerline status, searchable skills, idea queue, sticky Bash, hooks, policy controls, and session UX.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
package/src/config/parse.ts
CHANGED
|
@@ -29,6 +29,7 @@ import type {
|
|
|
29
29
|
} from "./types.ts";
|
|
30
30
|
import { isStructuralPresetName } from "./structural-presets.ts";
|
|
31
31
|
import { isMotionLevel, type MotionLevel } from "../motion/accessibility.ts";
|
|
32
|
+
import { SETTING_DEFAULTS } from "./settings-registry.ts";
|
|
32
33
|
|
|
33
34
|
export interface PowerlineConfig {
|
|
34
35
|
preset: StatusLinePreset;
|
|
@@ -65,7 +66,7 @@ export function parsePowerlineConfig(
|
|
|
65
66
|
presets: readonly StatusLinePreset[],
|
|
66
67
|
): PowerlineConfig {
|
|
67
68
|
const defaultConfig: PowerlineConfig = {
|
|
68
|
-
preset: "
|
|
69
|
+
preset: SETTING_DEFAULTS["powerline.preset"],
|
|
69
70
|
customItems: [],
|
|
70
71
|
disabledSegments: [],
|
|
71
72
|
invalidDisabledSegments: [],
|
|
@@ -73,9 +74,9 @@ export function parsePowerlineConfig(
|
|
|
73
74
|
invalidLayoutSegments: [],
|
|
74
75
|
separator: null,
|
|
75
76
|
segmentOptions: {},
|
|
76
|
-
placement: "
|
|
77
|
+
placement: SETTING_DEFAULTS["powerline.placement"],
|
|
77
78
|
invalidPlacement: null,
|
|
78
|
-
welcome:
|
|
79
|
+
welcome: SETTING_DEFAULTS["powerline.welcome"],
|
|
79
80
|
stashSharpSShortcut: false,
|
|
80
81
|
costAlert: null,
|
|
81
82
|
customItemsAuto: false,
|
|
@@ -84,7 +85,7 @@ export function parsePowerlineConfig(
|
|
|
84
85
|
presets: {},
|
|
85
86
|
segmentLabels: {},
|
|
86
87
|
appearance: {},
|
|
87
|
-
motionLevel: "
|
|
88
|
+
motionLevel: SETTING_DEFAULTS["powerline.motionLevel"],
|
|
88
89
|
};
|
|
89
90
|
|
|
90
91
|
const directPreset = normalizePreset(value, presets);
|
|
@@ -138,7 +139,10 @@ export function parsePowerlineConfig(
|
|
|
138
139
|
segmentOptions: normalizeSegmentOptions(value),
|
|
139
140
|
placement,
|
|
140
141
|
invalidPlacement,
|
|
141
|
-
welcome:
|
|
142
|
+
welcome:
|
|
143
|
+
typeof value.welcome === "boolean"
|
|
144
|
+
? value.welcome
|
|
145
|
+
: SETTING_DEFAULTS["powerline.welcome"],
|
|
142
146
|
stashSharpSShortcut: value.stashSharpSShortcut === true,
|
|
143
147
|
costAlert: normalizeCostAlert(value.costAlert),
|
|
144
148
|
customItemsAuto:
|
|
@@ -149,7 +153,9 @@ export function parsePowerlineConfig(
|
|
|
149
153
|
presets: customPresetDefs,
|
|
150
154
|
segmentLabels: normalizeSegmentLabels(value.segmentLabels),
|
|
151
155
|
appearance: normalizeAppearance(value.appearance),
|
|
152
|
-
motionLevel: isMotionLevel(value.motionLevel)
|
|
156
|
+
motionLevel: isMotionLevel(value.motionLevel)
|
|
157
|
+
? value.motionLevel
|
|
158
|
+
: SETTING_DEFAULTS["powerline.motionLevel"],
|
|
153
159
|
};
|
|
154
160
|
}
|
|
155
161
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical metadata for settings exposed by Wishcraft.
|
|
3
|
+
*
|
|
4
|
+
* Keep defaults, edit kinds, choices, grouping and operator copy here. Runtime
|
|
5
|
+
* parsers may consume the same defaults without depending on the TUI layer.
|
|
6
|
+
*/
|
|
7
|
+
import { MOTION_LEVELS } from "../motion/accessibility.ts";
|
|
8
|
+
import { STRUCTURAL_PRESET_NAMES } from "./types.ts";
|
|
9
|
+
|
|
10
|
+
export type SettingValue = boolean | string | number;
|
|
11
|
+
export type SettingKind = "toggle" | "select" | "text" | "number";
|
|
12
|
+
export type SettingGroupId = "status" | "vibes" | "skills" | "harness" | "shortcuts";
|
|
13
|
+
|
|
14
|
+
interface SettingDefinitionBase {
|
|
15
|
+
id: string;
|
|
16
|
+
path: string;
|
|
17
|
+
group: SettingGroupId;
|
|
18
|
+
label: string;
|
|
19
|
+
hint?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
restartRequired?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type SettingDefinition =
|
|
25
|
+
| (SettingDefinitionBase & { kind: "toggle"; defaultValue?: boolean })
|
|
26
|
+
| (SettingDefinitionBase & { kind: "select"; choices: readonly string[]; defaultValue?: string })
|
|
27
|
+
| (SettingDefinitionBase & { kind: "text"; defaultValue?: string })
|
|
28
|
+
| (SettingDefinitionBase & { kind: "number"; defaultValue?: number });
|
|
29
|
+
|
|
30
|
+
export interface SettingGroupDefinition {
|
|
31
|
+
id: SettingGroupId;
|
|
32
|
+
title: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const SETTING_DEFAULTS = {
|
|
36
|
+
"powerline.preset": "default",
|
|
37
|
+
"powerline.placement": "above",
|
|
38
|
+
"powerline.welcome": true,
|
|
39
|
+
"powerline.motionLevel": "full",
|
|
40
|
+
"powerline.segmentOptions.tps.windowMs": 1000,
|
|
41
|
+
"wishcraft.readHints": true,
|
|
42
|
+
"wishcraft.hooksEnabled": true,
|
|
43
|
+
"wishcraft.repairsEnabled": true,
|
|
44
|
+
} as const satisfies Record<string, SettingValue>;
|
|
45
|
+
|
|
46
|
+
export const SETTING_GROUPS = [
|
|
47
|
+
{ id: "status", title: "Status bar" },
|
|
48
|
+
{ id: "vibes", title: "Welcome & vibes" },
|
|
49
|
+
{ id: "skills", title: "Skills" },
|
|
50
|
+
{ id: "harness", title: "Hooks & repairs (harness)" },
|
|
51
|
+
{ id: "shortcuts", title: "Shortcuts" },
|
|
52
|
+
] as const satisfies readonly SettingGroupDefinition[];
|
|
53
|
+
|
|
54
|
+
const SEPARATORS = [
|
|
55
|
+
"powerline", "powerline-thin", "slash", "pipe", "block", "none",
|
|
56
|
+
"ascii", "dot", "chevron", "star",
|
|
57
|
+
] as const;
|
|
58
|
+
const STATUS_PRESETS = ["default", "minimal", "compact", "full", "nerd", "ascii", "chef"] as const;
|
|
59
|
+
|
|
60
|
+
export const SETTINGS_REGISTRY = [
|
|
61
|
+
{ id: "status.preset", path: "powerline.preset", group: "status", label: "Preset", kind: "select", choices: STATUS_PRESETS, defaultValue: SETTING_DEFAULTS["powerline.preset"] },
|
|
62
|
+
{ id: "appearance.base", path: "powerline.appearance.base", group: "status", label: "Structural base", kind: "select", choices: STRUCTURAL_PRESET_NAMES, hint: "colors and motion for Signal; layout preset stays separate" },
|
|
63
|
+
{ id: "motion.level", path: "powerline.motionLevel", group: "status", label: "Motion level", kind: "select", choices: MOTION_LEVELS, defaultValue: SETTING_DEFAULTS["powerline.motionLevel"], hint: "full · reduced · functional · off" },
|
|
64
|
+
{ id: "status.separator", path: "powerline.separator", group: "status", label: "Separator", kind: "select", choices: SEPARATORS },
|
|
65
|
+
{ id: "status.placement", path: "powerline.placement", group: "status", label: "Placement", kind: "select", choices: ["above", "below"], defaultValue: SETTING_DEFAULTS["powerline.placement"] },
|
|
66
|
+
{ id: "status.path.mode", path: "powerline.segmentOptions.path.mode", group: "status", label: "Path mode", kind: "select", choices: ["basename", "abbreviated", "full"] },
|
|
67
|
+
{ id: "status.path.maxLength", path: "powerline.segmentOptions.path.maxLength", group: "status", label: "Path max length", kind: "number", hint: "0 = unlimited" },
|
|
68
|
+
{ id: "status.time.format", path: "powerline.segmentOptions.time.format", group: "status", label: "Time format", kind: "select", choices: ["12h", "24h"] },
|
|
69
|
+
{ id: "status.time.seconds", path: "powerline.segmentOptions.time.showSeconds", group: "status", label: "Time seconds", kind: "toggle" },
|
|
70
|
+
{ id: "status.git.hostIcons", path: "powerline.segmentOptions.git.hostIcon", group: "status", label: "Git host icons", kind: "toggle" },
|
|
71
|
+
{ id: "status.git.aheadBehind", path: "powerline.segmentOptions.git.showAheadBehind", group: "status", label: "Git ahead/behind", kind: "toggle" },
|
|
72
|
+
{ id: "status.git.latestCommit", path: "powerline.segmentOptions.git.showCommit", group: "status", label: "Git latest commit", kind: "toggle" },
|
|
73
|
+
{ id: "status.context.format", path: "powerline.segmentOptions.context.format", group: "status", label: "Context format", kind: "select", choices: ["full", "percent"] },
|
|
74
|
+
{ id: "status.cacheRead.format", path: "powerline.segmentOptions.cache_read.format", group: "status", label: "Cache-read format", kind: "select", choices: ["tokens", "percent", "both"] },
|
|
75
|
+
{ id: "status.cost.display", path: "powerline.segmentOptions.cost.subscriptionDisplay", group: "status", label: "Cost display", kind: "select", choices: ["subscription", "reported-cost", "both"] },
|
|
76
|
+
{ id: "status.cost.currency", path: "powerline.segmentOptions.cost.currency", group: "status", label: "Currency", kind: "text" },
|
|
77
|
+
{ id: "status.ports.udp", path: "powerline.segmentOptions.openPorts.includeUdp", group: "status", label: "Ports include UDP", kind: "toggle" },
|
|
78
|
+
{ id: "status.tps.windowMs", path: "powerline.segmentOptions.tps.windowMs", group: "status", label: "TPS window (ms)", kind: "number", defaultValue: SETTING_DEFAULTS["powerline.segmentOptions.tps.windowMs"], hint: "default 1000" },
|
|
79
|
+
{ id: "status.tps.mode", path: "powerline.segmentOptions.tps.mode", group: "status", label: "TPS mode", kind: "select", choices: ["both", "out", "in"] },
|
|
80
|
+
{ id: "status.tps.label", path: "powerline.segmentLabels.tps", group: "status", label: "TPS label", kind: "text", hint: "empty = no label" },
|
|
81
|
+
{ id: "welcome.enabled", path: "powerline.welcome", group: "vibes", label: "Welcome overlay", kind: "toggle", defaultValue: SETTING_DEFAULTS["powerline.welcome"], hint: "on = overlay at startup, off = no welcome" },
|
|
82
|
+
{ id: "welcome.lanternMotion", path: "wishcraft.welcome.animateLantern", group: "vibes", label: "Animate wishcraft lantern", kind: "toggle", hint: "flicker on the lantern" },
|
|
83
|
+
{ id: "skills.inline", path: "wishcraft.inlineSkills", group: "skills", label: "Inline expand /command and $skill", kind: "toggle", hint: "needs a restart to take effect", restartRequired: true },
|
|
84
|
+
{ id: "skills.readHints", path: "wishcraft.readHints", group: "skills", label: "Read hints", kind: "toggle", defaultValue: SETTING_DEFAULTS["wishcraft.readHints"], hint: "off = no continuation hint after partial reads" },
|
|
85
|
+
{ id: "harness.hooks", path: "wishcraft.hooksEnabled", group: "harness", label: "Hooks enabled", kind: "toggle", defaultValue: SETTING_DEFAULTS["wishcraft.hooksEnabled"], hint: "gate for configured preToolUse / postToolUse / sessionStart hooks" },
|
|
86
|
+
{ id: "harness.repairs", path: "wishcraft.repairsEnabled", group: "harness", label: "Tool-input repairs", kind: "toggle", defaultValue: SETTING_DEFAULTS["wishcraft.repairsEnabled"], hint: "null-for-optional, auto-link, json-array, path aliases" },
|
|
87
|
+
{ id: "budget.dailyTokens", path: "wishcraft.tokenBudget.daily", group: "harness", label: "Daily token budget", kind: "number", hint: "colours the cost segment; never blocks. 0 = off" },
|
|
88
|
+
{ id: "shortcut.menu", path: "powerlineShortcuts.menu", group: "shortcuts", label: "Menu", kind: "text", hint: "e.g. alt+p" },
|
|
89
|
+
{ id: "shortcut.info", path: "powerlineShortcuts.info", group: "shortcuts", label: "Info", kind: "text" },
|
|
90
|
+
{ id: "shortcut.stash", path: "powerlineShortcuts.stashHistory", group: "shortcuts", label: "Stash", kind: "text" },
|
|
91
|
+
{ id: "shortcut.idea", path: "powerlineShortcuts.ideaCapture", group: "shortcuts", label: "Idea", kind: "text" },
|
|
92
|
+
{ id: "shortcut.queue", path: "powerlineShortcuts.queueOpen", group: "shortcuts", label: "Queue", kind: "text" },
|
|
93
|
+
] as const satisfies readonly SettingDefinition[];
|
|
94
|
+
|
|
95
|
+
const SETTINGS_BY_PATH = new Map<string, SettingDefinition>(SETTINGS_REGISTRY.map((item) => [item.path, item]));
|
|
96
|
+
const SETTINGS_BY_ID = new Map<string, SettingDefinition>(SETTINGS_REGISTRY.map((item) => [item.id, item]));
|
|
97
|
+
|
|
98
|
+
export function getSettingDefinition(pathOrId: string): SettingDefinition | undefined {
|
|
99
|
+
return SETTINGS_BY_PATH.get(pathOrId) ?? SETTINGS_BY_ID.get(pathOrId);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function effectiveSettingValue(
|
|
103
|
+
definition: SettingDefinition,
|
|
104
|
+
storedValue: unknown,
|
|
105
|
+
): SettingValue | null {
|
|
106
|
+
if (validateSettingValue(definition, storedValue)) return storedValue;
|
|
107
|
+
return definition.defaultValue ?? null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function validateSettingValue(
|
|
111
|
+
definition: SettingDefinition,
|
|
112
|
+
value: unknown,
|
|
113
|
+
): value is SettingValue {
|
|
114
|
+
if (definition.kind === "toggle") return typeof value === "boolean";
|
|
115
|
+
if (definition.kind === "number") return typeof value === "number" && Number.isFinite(value);
|
|
116
|
+
if (definition.kind === "text") return typeof value === "string";
|
|
117
|
+
return typeof value === "string" && definition.choices.includes(value);
|
|
118
|
+
}
|
|
@@ -23,6 +23,7 @@ import { STRUCTURAL_PRESET_NAMES } from "./types.ts";
|
|
|
23
23
|
export { STRUCTURAL_PRESET_NAMES };
|
|
24
24
|
import type { MotionEvent } from "../motion/types.ts";
|
|
25
25
|
import { defaultMotionFor } from "../motion/catalog.ts";
|
|
26
|
+
import { resolveAppearance } from "../extension/contrib/appearance.ts";
|
|
26
27
|
|
|
27
28
|
function tokens(partial: Partial<WishcraftTokens>): Partial<WishcraftTokens> {
|
|
28
29
|
return partial;
|
|
@@ -513,7 +514,11 @@ export function isStructuralPresetName(name: string): name is StructuralPresetNa
|
|
|
513
514
|
}
|
|
514
515
|
|
|
515
516
|
export function getStructuralPreset(name: StructuralPresetName): StructuralPresetDef {
|
|
516
|
-
return STRUCTURAL_PRESETS[name];
|
|
517
|
+
return resolveAppearance(STRUCTURAL_PRESETS[name]);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export function getEffectivePreset(name: StructuralPresetName): StructuralPresetDef {
|
|
521
|
+
return getStructuralPreset(name);
|
|
517
522
|
}
|
|
518
523
|
|
|
519
524
|
/** Display name for a stored base, or the raw value when it is not structural. */
|
package/src/core/frontmatter.ts
CHANGED
|
@@ -42,14 +42,15 @@ export function stripFrontmatter(content: string): string {
|
|
|
42
42
|
export interface SkillFrontmatter {
|
|
43
43
|
name: string | null;
|
|
44
44
|
description: string | null;
|
|
45
|
+
trigger: string | null;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
|
-
* Parse minimal YAML frontmatter (name/description) from a skill file.
|
|
49
|
+
* Parse minimal YAML frontmatter (name/description/trigger) from a skill file.
|
|
49
50
|
* Returns nulls when the keys are absent or there is no frontmatter block.
|
|
50
51
|
*/
|
|
51
52
|
export function parseSkillFrontmatter(content: string): SkillFrontmatter {
|
|
52
|
-
const fm: SkillFrontmatter = { name: null, description: null };
|
|
53
|
+
const fm: SkillFrontmatter = { name: null, description: null, trigger: null };
|
|
53
54
|
const lines = content.split("\n");
|
|
54
55
|
|
|
55
56
|
if (lines[0]?.trim() !== "---") {
|
|
@@ -76,6 +77,8 @@ export function parseSkillFrontmatter(content: string): SkillFrontmatter {
|
|
|
76
77
|
fm.name = value || null;
|
|
77
78
|
} else if (key === "description" && fm.description === null) {
|
|
78
79
|
fm.description = value || null;
|
|
80
|
+
} else if (key === "trigger" && fm.trigger === null) {
|
|
81
|
+
fm.trigger = value ? value.replace(/^\//, "") : null;
|
|
79
82
|
}
|
|
80
83
|
}
|
|
81
84
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { StructuralPresetDef, StructuralPresetName } from "../../config/types.ts";
|
|
2
|
+
|
|
3
|
+
// ponytail: appearance contributions are data patches, not loaders. One preset may have many patches; last wins per field.
|
|
4
|
+
|
|
5
|
+
type SignalPatch = Omit<Partial<StructuralPresetDef["signal"]>, "separators" | "caps"> & {
|
|
6
|
+
separators?: Partial<StructuralPresetDef["signal"]["separators"]>;
|
|
7
|
+
caps?: Partial<StructuralPresetDef["signal"]["caps"]>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type ChromePatch = Omit<Partial<StructuralPresetDef["chrome"]>, "corners" | "dividers"> & {
|
|
11
|
+
corners?: Partial<StructuralPresetDef["chrome"]["corners"]>;
|
|
12
|
+
dividers?: Partial<StructuralPresetDef["chrome"]["dividers"]>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type AppearancePresetPatch = Omit<
|
|
16
|
+
Partial<StructuralPresetDef>,
|
|
17
|
+
"name" | "tokens" | "signal" | "chrome" | "glyphs"
|
|
18
|
+
> & {
|
|
19
|
+
tokens?: StructuralPresetDef["tokens"];
|
|
20
|
+
signal?: SignalPatch;
|
|
21
|
+
chrome?: ChromePatch;
|
|
22
|
+
glyphs?: Partial<StructuralPresetDef["glyphs"]>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const patches = new Map<StructuralPresetName, AppearancePresetPatch[]>();
|
|
26
|
+
|
|
27
|
+
/** Register an appearance-only patch for a structural preset. Invalid input is rejected without throwing. */
|
|
28
|
+
export function registerAppearanceContribution(
|
|
29
|
+
preset: StructuralPresetName,
|
|
30
|
+
patch: AppearancePresetPatch,
|
|
31
|
+
): boolean {
|
|
32
|
+
if (!preset || typeof patch !== "object" || patch === null) return false;
|
|
33
|
+
if (Array.isArray(patch) || "name" in patch) return false;
|
|
34
|
+
const list = patches.get(preset) ?? [];
|
|
35
|
+
list.push({ ...patch });
|
|
36
|
+
patches.set(preset, list);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Return registered patches for a preset in application order. */
|
|
41
|
+
export function getAppearancePatches(
|
|
42
|
+
preset: StructuralPresetName,
|
|
43
|
+
): readonly AppearancePresetPatch[] {
|
|
44
|
+
return patches.get(preset) ?? [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Clear all registered appearance contributions. Primarily used for isolation in tests and reloads. */
|
|
48
|
+
export function clearAppearanceContributions(): void {
|
|
49
|
+
patches.clear();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Resolve a structural preset with all contributed patches while preserving nested defaults and preset identity. */
|
|
53
|
+
export function resolveAppearance(base: StructuralPresetDef): StructuralPresetDef {
|
|
54
|
+
const list = patches.get(base.name as StructuralPresetName);
|
|
55
|
+
if (!list || list.length === 0) return base;
|
|
56
|
+
let out: StructuralPresetDef = { ...base };
|
|
57
|
+
for (const patch of list) {
|
|
58
|
+
out = {
|
|
59
|
+
...out,
|
|
60
|
+
...patch,
|
|
61
|
+
name: base.name,
|
|
62
|
+
tokens: patch.tokens ? { ...out.tokens, ...patch.tokens } : out.tokens,
|
|
63
|
+
signal: patch.signal
|
|
64
|
+
? {
|
|
65
|
+
...out.signal,
|
|
66
|
+
...patch.signal,
|
|
67
|
+
separators: patch.signal.separators
|
|
68
|
+
? { ...out.signal.separators, ...patch.signal.separators }
|
|
69
|
+
: out.signal.separators,
|
|
70
|
+
caps: patch.signal.caps
|
|
71
|
+
? { ...out.signal.caps, ...patch.signal.caps }
|
|
72
|
+
: out.signal.caps,
|
|
73
|
+
}
|
|
74
|
+
: out.signal,
|
|
75
|
+
chrome: patch.chrome
|
|
76
|
+
? {
|
|
77
|
+
...out.chrome,
|
|
78
|
+
...patch.chrome,
|
|
79
|
+
corners: patch.chrome.corners
|
|
80
|
+
? { ...out.chrome.corners, ...patch.chrome.corners }
|
|
81
|
+
: out.chrome.corners,
|
|
82
|
+
dividers: patch.chrome.dividers
|
|
83
|
+
? { ...out.chrome.dividers, ...patch.chrome.dividers }
|
|
84
|
+
: out.chrome.dividers,
|
|
85
|
+
}
|
|
86
|
+
: out.chrome,
|
|
87
|
+
glyphs: patch.glyphs ? { ...out.glyphs, ...patch.glyphs } : out.glyphs,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ContributedDeckRoute, ContributedSignalSource } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
// ponytail: single in-memory registry, no new runtime — data/callback contracts only.
|
|
4
|
+
// A failing contribution never takes down Signal or Deck — callers must isolate.
|
|
5
|
+
|
|
6
|
+
const deckRoutes = new Map<string, ContributedDeckRoute>();
|
|
7
|
+
const signalSources = new Map<string, ContributedSignalSource>();
|
|
8
|
+
|
|
9
|
+
function isValidId(id: string): boolean {
|
|
10
|
+
return /^[a-z0-9][a-z0-9-_]*$/.test(id);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function registerDeckRoute(route: ContributedDeckRoute): boolean {
|
|
14
|
+
if (!route || typeof route.id !== "string" || !isValidId(route.id)) return false;
|
|
15
|
+
if (typeof route.label !== "string" || !route.label.trim()) return false;
|
|
16
|
+
if (deckRoutes.has(route.id)) return false;
|
|
17
|
+
if (route.jumpKey != null && !/^[a-z0-9]$/.test(route.jumpKey)) return false;
|
|
18
|
+
deckRoutes.set(route.id, { ...route, label: route.label.trim() });
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function registerSignalSource(source: ContributedSignalSource): boolean {
|
|
23
|
+
if (!source || typeof source.id !== "string" || !isValidId(source.id)) return false;
|
|
24
|
+
if (typeof source.label !== "string" || !source.label.trim()) return false;
|
|
25
|
+
if (signalSources.has(source.id)) return false;
|
|
26
|
+
signalSources.set(source.id, { ...source });
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getContributedDeckRoutes(): readonly ContributedDeckRoute[] {
|
|
31
|
+
return [...deckRoutes.values()];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getContributedSignalSources(): readonly ContributedSignalSource[] {
|
|
35
|
+
return [...signalSources.values()];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Test helper — clears the registry. */
|
|
39
|
+
export function clearContributions(): void {
|
|
40
|
+
deckRoutes.clear();
|
|
41
|
+
signalSources.clear();
|
|
42
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type DeckRouteId = string;
|
|
2
|
+
|
|
3
|
+
export interface ContributedDeckRoute {
|
|
4
|
+
id: DeckRouteId;
|
|
5
|
+
label: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
jumpKey?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ContributedSignalSource {
|
|
11
|
+
id: string;
|
|
12
|
+
label: string;
|
|
13
|
+
/** Called on each Signal repaint; must not throw. */
|
|
14
|
+
render?: (ctx: unknown) => string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ContributedMotion {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PowerlineConfig } from "../../config/powerline-config.ts";
|
|
2
2
|
import type { StatusLinePreset } from "../../config/types.ts";
|
|
3
|
+
import { SETTING_DEFAULTS } from "../../config/settings-registry.ts";
|
|
3
4
|
import { PRESETS } from "../../config/presets.ts";
|
|
4
5
|
import { BashTranscriptStore } from "../../../bash-mode/transcript.ts";
|
|
5
6
|
import { BashCompletionEngine } from "../../../bash-mode/completion.ts";
|
|
@@ -31,7 +32,7 @@ import type { RuntimeState } from "./types.ts";
|
|
|
31
32
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
32
33
|
|
|
33
34
|
export let config: PowerlineConfig = {
|
|
34
|
-
preset: "
|
|
35
|
+
preset: SETTING_DEFAULTS["powerline.preset"],
|
|
35
36
|
customItems: [],
|
|
36
37
|
disabledSegments: [],
|
|
37
38
|
invalidDisabledSegments: [],
|
|
@@ -39,9 +40,9 @@ export let config: PowerlineConfig = {
|
|
|
39
40
|
invalidLayoutSegments: [],
|
|
40
41
|
separator: null,
|
|
41
42
|
segmentOptions: {},
|
|
42
|
-
placement: "
|
|
43
|
+
placement: SETTING_DEFAULTS["powerline.placement"],
|
|
43
44
|
invalidPlacement: null,
|
|
44
|
-
welcome:
|
|
45
|
+
welcome: SETTING_DEFAULTS["powerline.welcome"],
|
|
45
46
|
stashSharpSShortcut: false,
|
|
46
47
|
costAlert: null,
|
|
47
48
|
customItemsAuto: false,
|
|
@@ -50,7 +51,7 @@ export let config: PowerlineConfig = {
|
|
|
50
51
|
presets: {},
|
|
51
52
|
segmentLabels: {},
|
|
52
53
|
appearance: {},
|
|
53
|
-
motionLevel: "
|
|
54
|
+
motionLevel: SETTING_DEFAULTS["powerline.motionLevel"],
|
|
54
55
|
};
|
|
55
56
|
|
|
56
57
|
export function setConfig(next: PowerlineConfig): void {
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
* ---------------------------------------------------------------------------
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
import { SETTING_DEFAULTS } from "../../config/settings-registry.ts";
|
|
23
|
+
|
|
22
24
|
export type HookEventName = "preToolUse" | "postToolUse" | "sessionStart" | "turnEnd";
|
|
23
25
|
|
|
24
26
|
export interface HookCommand {
|
|
@@ -81,7 +83,10 @@ export function parseHooksSettings(
|
|
|
81
83
|
wishcraftSettings: unknown,
|
|
82
84
|
): { enabled: boolean; hooks: WishcraftHooksSettings } {
|
|
83
85
|
if (!isRecord(wishcraftSettings)) return { enabled: false, hooks: {} };
|
|
84
|
-
const enabled =
|
|
86
|
+
const enabled =
|
|
87
|
+
typeof wishcraftSettings.hooksEnabled === "boolean"
|
|
88
|
+
? wishcraftSettings.hooksEnabled
|
|
89
|
+
: SETTING_DEFAULTS["wishcraft.hooksEnabled"];
|
|
85
90
|
const raw = wishcraftSettings.hooks;
|
|
86
91
|
if (!isRecord(raw)) return { enabled, hooks: {} };
|
|
87
92
|
return {
|