@groeponline/pi-wishcraft 1.0.5 → 1.2.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 +20 -0
- package/README.md +25 -9
- package/ROADMAP.md +41 -260
- package/docs/commands.md +6 -2
- package/docs/configuration.md +21 -0
- package/docs/design/accessibility.md +71 -0
- package/docs/design/deck-layout.md +69 -0
- package/docs/design/motion-gallery.md +80 -0
- package/docs/design/motion-system.md +119 -0
- package/docs/design/presets.md +196 -0
- package/docs/design/regression-testing.md +54 -0
- package/docs/design/responsive.md +53 -0
- package/docs/design/signal.md +68 -0
- package/docs/design/theme-contract.md +132 -0
- package/docs/design/vnext-overview.md +55 -0
- package/docs/design/vnext-release-plan.md +212 -0
- package/docs/index.md +19 -1
- package/docs/segments.md +1 -1
- package/package.json +16 -9
- package/src/config/appearance.ts +230 -0
- package/src/config/parse.ts +41 -0
- package/src/config/presets.ts +184 -0
- package/src/config/structural-presets.ts +555 -0
- package/src/config/tokens.ts +154 -0
- package/src/config/types.ts +210 -2
- package/src/extension/commands/commands.ts +36 -12
- package/src/extension/commands/powerline-completions.ts +4 -0
- package/src/extension/commands/queue-commands.ts +6 -0
- package/src/extension/core/segment-context.ts +7 -1
- package/src/extension/core/state.ts +14 -0
- package/src/extension/core/types.ts +7 -0
- package/src/extension/session/session-lifecycle.ts +20 -0
- package/src/extension/settings/appearance-write.ts +135 -0
- package/src/extension/settings/wishcraft-config-items.ts +119 -0
- package/src/extension/settings/wishcraft-config.ts +28 -114
- package/src/extension/skills/skill-manager.ts +57 -3
- package/src/extension/ui/custom-editor.ts +7 -4
- package/src/extension/ui/deck/component.ts +358 -0
- package/src/extension/ui/deck/index.ts +34 -0
- package/src/extension/ui/deck/render.ts +260 -0
- package/src/extension/ui/deck/route-bodies.ts +209 -0
- package/src/extension/ui/deck/routes.ts +37 -0
- package/src/extension/ui/deck/session-snapshot.ts +104 -0
- package/src/extension/ui/deck/types.ts +92 -0
- package/src/extension/ui/layout.ts +3 -2
- package/src/extension/ui/menu-views.ts +6 -1
- package/src/extension/ui/powerline-menu-view.ts +19 -15
- package/src/extension/ui/signal-layout.ts +77 -0
- package/src/extension/ui/status-line-renderers.ts +24 -5
- package/src/motion/accessibility.ts +59 -0
- package/src/motion/catalog-extra.ts +429 -0
- package/src/motion/catalog.ts +336 -0
- package/src/motion/composer.ts +147 -0
- package/src/motion/frames.ts +84 -0
- package/src/motion/gallery.ts +77 -0
- package/src/motion/index.ts +78 -0
- package/src/motion/policy.ts +128 -0
- package/src/motion/scheduler.ts +159 -0
- package/src/motion/types.ts +132 -0
- package/src/render/timer.ts +1 -0
- package/src/segments/registry.ts +18 -6
- package/src/segments/system.ts +1 -1
- package/src/signal/controller.ts +123 -0
- package/src/signal/integration.ts +42 -0
- package/src/signal/render.ts +178 -0
- package/src/theme/colors.ts +14 -0
- package/src/theme/detect.ts +48 -0
- package/src/theme/tokens/index.ts +9 -0
- package/src/theme/tokens/mapping.ts +62 -0
- package/src/theme/tokens/types.ts +39 -0
- package/src/welcome/renderer.ts +2 -2
- package/src/welcome/whats-new.ts +46 -10
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* appearance-write.ts
|
|
3
|
+
* ---------------------------------------------------------------------------
|
|
4
|
+
* Persist `powerline.appearance.base` and refresh the live Signal.
|
|
5
|
+
* ---------------------------------------------------------------------------
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { StructuralPresetName } from "../../config/types.ts";
|
|
9
|
+
import type { MotionEvent } from "../../motion/types.ts";
|
|
10
|
+
import { isStructuralPresetName } from "../../config/structural-presets.ts";
|
|
11
|
+
import {
|
|
12
|
+
effectiveAppearanceMix,
|
|
13
|
+
resolveAppearanceMix,
|
|
14
|
+
} from "../../config/appearance.ts";
|
|
15
|
+
import {
|
|
16
|
+
isMotionLevel,
|
|
17
|
+
policyFromEnvironment,
|
|
18
|
+
type MotionLevel,
|
|
19
|
+
} from "../../motion/accessibility.ts";
|
|
20
|
+
import { parsePowerlineConfig } from "../../config/powerline-config.ts";
|
|
21
|
+
import { isRecord, writePowerlineSetting } from "./settings-io.ts";
|
|
22
|
+
import { config, PRESET_NAMES, setConfig } from "../core/state.ts";
|
|
23
|
+
import {
|
|
24
|
+
requestImmediateStatusRender,
|
|
25
|
+
resetLayoutCache,
|
|
26
|
+
} from "../core/segment-context.ts";
|
|
27
|
+
import type { RuntimeState } from "../core/types.ts";
|
|
28
|
+
|
|
29
|
+
export function writeAppearanceBase(
|
|
30
|
+
cwd: string,
|
|
31
|
+
name: StructuralPresetName,
|
|
32
|
+
): boolean {
|
|
33
|
+
return writePowerlineSetting(cwd, (existing) => {
|
|
34
|
+
const node: Record<string, unknown> = isRecord(existing)
|
|
35
|
+
? { ...existing }
|
|
36
|
+
: typeof existing === "string"
|
|
37
|
+
? { preset: existing }
|
|
38
|
+
: {};
|
|
39
|
+
const appearance = isRecord(node.appearance) ? { ...node.appearance } : {};
|
|
40
|
+
appearance.base = name;
|
|
41
|
+
node.appearance = appearance;
|
|
42
|
+
return node;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Write the structural base, keep session config in sync, and repaint Signal. */
|
|
47
|
+
export function applyAppearanceBase(
|
|
48
|
+
rt: RuntimeState,
|
|
49
|
+
cwd: string,
|
|
50
|
+
name: StructuralPresetName,
|
|
51
|
+
): boolean {
|
|
52
|
+
config.appearance = { ...config.appearance, base: name };
|
|
53
|
+
const ok = writeAppearanceBase(cwd, name);
|
|
54
|
+
resetLayoutCache(rt);
|
|
55
|
+
requestImmediateStatusRender(rt);
|
|
56
|
+
return ok;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** `/signal hexforge` also sets appearance.base so colors match the layout name. */
|
|
60
|
+
export function syncAppearanceForLayoutPreset(
|
|
61
|
+
rt: RuntimeState,
|
|
62
|
+
cwd: string,
|
|
63
|
+
preset: string,
|
|
64
|
+
): void {
|
|
65
|
+
if (!isStructuralPresetName(preset)) return;
|
|
66
|
+
applyAppearanceBase(rt, cwd, preset);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function applyPersistedMotionPolicy(rt: RuntimeState): void {
|
|
70
|
+
rt.motionPolicy = policyFromEnvironment(process.env, config.motionLevel);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Re-parse `settings.powerline` into session config and repaint Signal. */
|
|
74
|
+
export function reloadPowerlineFromSettings(
|
|
75
|
+
rt: RuntimeState,
|
|
76
|
+
settings: Record<string, unknown>,
|
|
77
|
+
): void {
|
|
78
|
+
setConfig({
|
|
79
|
+
...config,
|
|
80
|
+
...parsePowerlineConfig(settings.powerline, PRESET_NAMES),
|
|
81
|
+
});
|
|
82
|
+
applyPersistedMotionPolicy(rt);
|
|
83
|
+
resetLayoutCache(rt);
|
|
84
|
+
requestImmediateStatusRender(rt);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function applyMotionLevel(
|
|
88
|
+
rt: RuntimeState,
|
|
89
|
+
cwd: string,
|
|
90
|
+
level: MotionLevel,
|
|
91
|
+
): boolean {
|
|
92
|
+
if (!isMotionLevel(level)) return false;
|
|
93
|
+
config.motionLevel = level;
|
|
94
|
+
applyPersistedMotionPolicy(rt);
|
|
95
|
+
const ok = writePowerlineSetting(cwd, (existing) => {
|
|
96
|
+
const node: Record<string, unknown> = isRecord(existing)
|
|
97
|
+
? { ...existing }
|
|
98
|
+
: typeof existing === "string"
|
|
99
|
+
? { preset: existing }
|
|
100
|
+
: {};
|
|
101
|
+
node.motionLevel = level;
|
|
102
|
+
return node;
|
|
103
|
+
});
|
|
104
|
+
resetLayoutCache(rt);
|
|
105
|
+
requestImmediateStatusRender(rt);
|
|
106
|
+
return ok;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function applyMotionAssignment(
|
|
110
|
+
rt: RuntimeState,
|
|
111
|
+
cwd: string,
|
|
112
|
+
event: MotionEvent,
|
|
113
|
+
motionId: string,
|
|
114
|
+
): boolean {
|
|
115
|
+
const current = {
|
|
116
|
+
...resolveAppearanceMix(effectiveAppearanceMix(config.appearance, config.preset))
|
|
117
|
+
.motion,
|
|
118
|
+
[event]: motionId,
|
|
119
|
+
};
|
|
120
|
+
config.appearance = { ...config.appearance, motion: current };
|
|
121
|
+
const ok = writePowerlineSetting(cwd, (existing) => {
|
|
122
|
+
const node: Record<string, unknown> = isRecord(existing)
|
|
123
|
+
? { ...existing }
|
|
124
|
+
: typeof existing === "string"
|
|
125
|
+
? { preset: existing }
|
|
126
|
+
: {};
|
|
127
|
+
const appearance = isRecord(node.appearance) ? { ...node.appearance } : {};
|
|
128
|
+
appearance.motion = current;
|
|
129
|
+
node.appearance = appearance;
|
|
130
|
+
return node;
|
|
131
|
+
});
|
|
132
|
+
resetLayoutCache(rt);
|
|
133
|
+
requestImmediateStatusRender(rt);
|
|
134
|
+
return ok;
|
|
135
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative groups for `/wishcraft settings`.
|
|
3
|
+
* Kept beside the overlay so the TUI file stays under the size budget.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { STRUCTURAL_PRESET_NAMES } from "../../config/types.ts";
|
|
7
|
+
import { MOTION_LEVELS } from "../../motion/accessibility.ts";
|
|
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
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ConfigGroup {
|
|
29
|
+
title: string;
|
|
30
|
+
items: ConfigItem[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const SEPARATORS = [
|
|
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). */
|
|
47
|
+
export function buildConfigGroups(_settings: Record<string, unknown>): ConfigGroup[] {
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
title: "Status bar",
|
|
51
|
+
items: [
|
|
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
|
+
];
|
|
119
|
+
}
|
|
@@ -11,38 +11,20 @@
|
|
|
11
11
|
import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
13
13
|
import type { RuntimeState } from "../core/types.ts";
|
|
14
|
+
import { openWishcraftDeck } from "../ui/deck/index.ts";
|
|
15
|
+
import { parseDeckRouteArg } from "../ui/deck/routes.ts";
|
|
14
16
|
import { readSettings, writeSettingKey } from "../settings/settings-io.ts";
|
|
15
17
|
import { isRecord } from "../settings/settings-io.ts";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
+
import { reloadPowerlineFromSettings } from "./appearance-write.ts";
|
|
19
|
+
import {
|
|
20
|
+
buildConfigGroups,
|
|
21
|
+
type ConfigGroup,
|
|
22
|
+
type ConfigItem,
|
|
23
|
+
type ConfigValue,
|
|
24
|
+
} from "./wishcraft-config-items.ts";
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
|
|
23
|
-
export type ConfigValue = boolean | string | number | null;
|
|
24
|
-
|
|
25
|
-
export interface ConfigItem {
|
|
26
|
-
/** Label in the list. */
|
|
27
|
-
label: string;
|
|
28
|
-
/** Path inside settings ("powerline.placement", "wishcraft.hooksEnabled", ...). */
|
|
29
|
-
path: string;
|
|
30
|
-
/** Edit kind. */
|
|
31
|
-
kind: "toggle" | "select" | "text" | "number";
|
|
32
|
-
/** For select: the choices. */
|
|
33
|
-
choices?: string[];
|
|
34
|
-
/** Hint under the group. */
|
|
35
|
-
hint?: string;
|
|
36
|
-
/** Detail explanation (optional). */
|
|
37
|
-
description?: string;
|
|
38
|
-
/** Effective value when the setting is absent (toggles only; defaults to false). */
|
|
39
|
-
default?: boolean;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface ConfigGroup {
|
|
43
|
-
title: string;
|
|
44
|
-
items: ConfigItem[];
|
|
45
|
-
}
|
|
26
|
+
export type { ConfigGroup, ConfigItem, ConfigValue };
|
|
27
|
+
export { buildConfigGroups };
|
|
46
28
|
|
|
47
29
|
/** Nested read: "wishcraft.hooksEnabled" → settings.wishcraft.hooksEnabled. */
|
|
48
30
|
export function readConfigPath(settings: Record<string, unknown>, path: string): ConfigValue {
|
|
@@ -118,80 +100,6 @@ export function writeConfigPath(
|
|
|
118
100
|
});
|
|
119
101
|
}
|
|
120
102
|
|
|
121
|
-
const SEPARATORS = [
|
|
122
|
-
"powerline",
|
|
123
|
-
"powerline-thin",
|
|
124
|
-
"slash",
|
|
125
|
-
"pipe",
|
|
126
|
-
"block",
|
|
127
|
-
"none",
|
|
128
|
-
"ascii",
|
|
129
|
-
"dot",
|
|
130
|
-
"chevron",
|
|
131
|
-
"star",
|
|
132
|
-
];
|
|
133
|
-
|
|
134
|
-
/** Build groups from current settings (values shown live). */
|
|
135
|
-
export function buildConfigGroups(settings: Record<string, unknown>): ConfigGroup[] {
|
|
136
|
-
return [
|
|
137
|
-
{
|
|
138
|
-
title: "Status bar",
|
|
139
|
-
items: [
|
|
140
|
-
{ label: "Preset", path: "powerline.preset", kind: "select", choices: ["default", "minimal", "compact", "full", "nerd", "ascii", "chef"] },
|
|
141
|
-
{ label: "Separator", path: "powerline.separator", kind: "select", choices: SEPARATORS },
|
|
142
|
-
{ label: "Placement", path: "powerline.placement", kind: "select", choices: ["above", "below"] },
|
|
143
|
-
{ label: "Path mode", path: "powerline.segmentOptions.path.mode", kind: "select", choices: ["basename", "abbreviated", "full"] },
|
|
144
|
-
{ label: "Path max length", path: "powerline.segmentOptions.path.maxLength", kind: "number", hint: "0 = unlimited" },
|
|
145
|
-
{ label: "Time format", path: "powerline.segmentOptions.time.format", kind: "select", choices: ["12h", "24h"] },
|
|
146
|
-
{ label: "Time seconds", path: "powerline.segmentOptions.time.showSeconds", kind: "toggle" },
|
|
147
|
-
{ label: "Git host icons", path: "powerline.segmentOptions.git.hostIcon", kind: "toggle" },
|
|
148
|
-
{ label: "Git ahead/behind", path: "powerline.segmentOptions.git.showAheadBehind", kind: "toggle" },
|
|
149
|
-
{ label: "Git latest commit", path: "powerline.segmentOptions.git.showCommit", kind: "toggle" },
|
|
150
|
-
{ label: "Context format", path: "powerline.segmentOptions.context.format", kind: "select", choices: ["full", "percent"] },
|
|
151
|
-
{ label: "Cache-read format", path: "powerline.segmentOptions.cache_read.format", kind: "select", choices: ["tokens", "percent", "both"] },
|
|
152
|
-
{ label: "Cost display", path: "powerline.segmentOptions.cost.subscriptionDisplay", kind: "select", choices: ["subscription", "reported-cost", "both"] },
|
|
153
|
-
{ label: "Currency", path: "powerline.segmentOptions.cost.currency", kind: "text" },
|
|
154
|
-
{ label: "Ports include UDP", path: "powerline.segmentOptions.openPorts.includeUdp", kind: "toggle" },
|
|
155
|
-
{ label: "TPS window (ms)", path: "powerline.segmentOptions.tps.windowMs", kind: "number", hint: "default 1000" },
|
|
156
|
-
{ label: "TPS mode", path: "powerline.segmentOptions.tps.mode", kind: "select", choices: ["both", "out", "in"] },
|
|
157
|
-
{ label: "TPS label", path: "powerline.segmentLabels.tps", kind: "text", hint: "empty = no label" },
|
|
158
|
-
],
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
title: "Welcome & vibes",
|
|
162
|
-
items: [
|
|
163
|
-
{ label: "Welcome overlay", path: "powerline.welcome", kind: "toggle", hint: "on = overlay at startup, off = no welcome" },
|
|
164
|
-
{ label: "Animate wishcraft lantern", path: "wishcraft.welcome.animateLantern", kind: "toggle", hint: "flicker on the lantern" },
|
|
165
|
-
],
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
title: "Skills",
|
|
169
|
-
items: [
|
|
170
|
-
{ label: "Inline expand /command and $skill", path: "wishcraft.inlineSkills", kind: "toggle", hint: "needs a restart to take effect" },
|
|
171
|
-
{ label: "Read hints", path: "wishcraft.readHints", kind: "toggle", default: true, hint: "off = no continuation hint after partial reads" },
|
|
172
|
-
],
|
|
173
|
-
},
|
|
174
|
-
{
|
|
175
|
-
title: "Hooks & repairs (harness)",
|
|
176
|
-
items: [
|
|
177
|
-
{ label: "Hooks enabled", path: "wishcraft.hooksEnabled", kind: "toggle", hint: "preToolUse / postToolUse / sessionStart command hooks" },
|
|
178
|
-
{ label: "Tool-input repairs", path: "wishcraft.repairsEnabled", kind: "toggle", hint: "null-for-optional, auto-link, json-array, path aliases" },
|
|
179
|
-
{ label: "Daily token budget", path: "wishcraft.tokenBudget.daily", kind: "number", hint: "colours the cost segment; never blocks. 0 = off" },
|
|
180
|
-
],
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
title: "Shortcuts",
|
|
184
|
-
items: [
|
|
185
|
-
{ label: "Menu", path: "powerlineShortcuts.menu", kind: "text", hint: "e.g. alt+p" },
|
|
186
|
-
{ label: "Info", path: "powerlineShortcuts.info", kind: "text" },
|
|
187
|
-
{ label: "Stash", path: "powerlineShortcuts.stashHistory", kind: "text" },
|
|
188
|
-
{ label: "Idea", path: "powerlineShortcuts.ideaCapture", kind: "text" },
|
|
189
|
-
{ label: "Queue", path: "powerlineShortcuts.queueOpen", kind: "text" },
|
|
190
|
-
],
|
|
191
|
-
},
|
|
192
|
-
];
|
|
193
|
-
}
|
|
194
|
-
|
|
195
103
|
// ---------------------------------------------------------------------------
|
|
196
104
|
// Overlay
|
|
197
105
|
// ---------------------------------------------------------------------------
|
|
@@ -272,13 +180,8 @@ export async function showWishcraftConfig(rt: RuntimeState, ctx: any): Promise<v
|
|
|
272
180
|
const ok = writeConfigPath(cwd, item.path, value);
|
|
273
181
|
settings = readSettings(cwd);
|
|
274
182
|
groups = buildConfigGroups(settings);
|
|
275
|
-
// live-reload powerline config + status bar
|
|
276
183
|
if (item.path.startsWith("powerline")) {
|
|
277
|
-
|
|
278
|
-
...stateConfig,
|
|
279
|
-
...parsePowerlineConfig(settings.powerline, PRESET_NAMES),
|
|
280
|
-
});
|
|
281
|
-
rt.tuiRef?.requestRender?.();
|
|
184
|
+
reloadPowerlineFromSettings(rt, settings);
|
|
282
185
|
}
|
|
283
186
|
ctx.ui.notify(
|
|
284
187
|
ok ? `${item.label}: ${displayValue(item, value)} (saved)` : `${item.label} not saved (settings.json?)`,
|
|
@@ -294,6 +197,9 @@ export async function showWishcraftConfig(rt: RuntimeState, ctx: any): Promise<v
|
|
|
294
197
|
const ok = writeConfigPath(cwd, item.path, next);
|
|
295
198
|
settings = readSettings(cwd);
|
|
296
199
|
groups = buildConfigGroups(settings);
|
|
200
|
+
if (item.path.startsWith("powerline")) {
|
|
201
|
+
reloadPowerlineFromSettings(rt, settings);
|
|
202
|
+
}
|
|
297
203
|
ctx.ui.notify(
|
|
298
204
|
ok ? `${item.label}: ${next} (saved)` : `${item.label} not saved`,
|
|
299
205
|
ok ? "info" : "warning",
|
|
@@ -305,6 +211,9 @@ export async function showWishcraftConfig(rt: RuntimeState, ctx: any): Promise<v
|
|
|
305
211
|
const ok = writeConfigPath(cwd, item.path, nextToggleValue(item, cur));
|
|
306
212
|
settings = readSettings(cwd);
|
|
307
213
|
groups = buildConfigGroups(settings);
|
|
214
|
+
if (item.path.startsWith("powerline")) {
|
|
215
|
+
reloadPowerlineFromSettings(rt, settings);
|
|
216
|
+
}
|
|
308
217
|
ctx.ui.notify(
|
|
309
218
|
ok ? `${item.label}: ${!(cur === true) ? "on" : "off"} (saved)` : `${item.label} not saved`,
|
|
310
219
|
ok ? "info" : "warning",
|
|
@@ -437,17 +346,22 @@ export async function showWishcraftConfig(rt: RuntimeState, ctx: any): Promise<v
|
|
|
437
346
|
);
|
|
438
347
|
}
|
|
439
348
|
|
|
440
|
-
/** Register /wishcraft. */
|
|
349
|
+
/** Register /wishcraft — opens the Deck; `settings`/`config` open the flat list. */
|
|
441
350
|
export function registerWishcraftConfigCommand(pi: ExtensionAPI, rt: RuntimeState): void {
|
|
442
351
|
pi.registerCommand("wishcraft", {
|
|
443
|
-
description: "
|
|
444
|
-
handler: async (
|
|
352
|
+
description: "Open the Wishcraft Deck, or settings/config for the flat list",
|
|
353
|
+
handler: async (args: string, ctx: any) => {
|
|
445
354
|
if (!rt.enabled || !ctx.hasUI) {
|
|
446
|
-
ctx.ui.notify("
|
|
355
|
+
ctx.ui.notify("Signal UI is disabled", "info");
|
|
447
356
|
return;
|
|
448
357
|
}
|
|
449
358
|
rt.currentCtx = ctx;
|
|
450
|
-
|
|
359
|
+
const trimmed = args?.trim() ?? "";
|
|
360
|
+
if (trimmed === "config" || trimmed === "settings") {
|
|
361
|
+
await showWishcraftConfig(rt, ctx);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
await openWishcraftDeck(rt, ctx, parseDeckRouteArg(trimmed));
|
|
451
365
|
},
|
|
452
366
|
});
|
|
453
367
|
}
|
|
@@ -13,8 +13,11 @@
|
|
|
13
13
|
|
|
14
14
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
16
|
-
import { rmSync } from "node:fs";
|
|
16
|
+
import { realpathSync, rmSync } from "node:fs";
|
|
17
|
+
import { join, relative } from "node:path";
|
|
17
18
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import { getAgentDir } from "../../paths/agent-dirs.ts";
|
|
20
|
+
import { openWishcraftDeck } from "../ui/deck/index.ts";
|
|
18
21
|
import type { RuntimeState } from "../core/types.ts";
|
|
19
22
|
import {
|
|
20
23
|
applySkillFilter,
|
|
@@ -84,9 +87,49 @@ function shellQuote(value: string): string {
|
|
|
84
87
|
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
85
88
|
}
|
|
86
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Resolve `target` through symlinks and confirm it stays within one of the
|
|
92
|
+
* canonical skill roots (agent skills dir, `<cwd>/.pi/skills`, `<cwd>/skills`).
|
|
93
|
+
* Guards the recursive delete against catalog paths that a cloned/untrusted
|
|
94
|
+
* repo could point outside the expected trees. Returns false when the path
|
|
95
|
+
* cannot be resolved (e.g. already removed) so callers fail closed.
|
|
96
|
+
*/
|
|
97
|
+
export function isContainedInSkillRoots(target: string, cwd: string): boolean {
|
|
98
|
+
const roots = [
|
|
99
|
+
join(getAgentDir(), "skills"),
|
|
100
|
+
join(cwd, ".pi", "skills"),
|
|
101
|
+
join(cwd, "skills"),
|
|
102
|
+
];
|
|
103
|
+
let real: string;
|
|
104
|
+
try {
|
|
105
|
+
real = realpathSync(target);
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
return roots.some((root) => {
|
|
110
|
+
let realRoot: string;
|
|
111
|
+
try {
|
|
112
|
+
realRoot = realpathSync(root);
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
const rel = relative(realRoot, real);
|
|
117
|
+
return rel !== "" && !rel.startsWith("..") && !rel.startsWith("/");
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Only allow a bare, path-like editor invocation (no arguments, no shell
|
|
123
|
+
* metacharacters) so a hostile `EDITOR` value cannot inject into the `!` flow.
|
|
124
|
+
* Falls back to `nvim` when the value is unusable.
|
|
125
|
+
*/
|
|
126
|
+
export function safeEditor(): string {
|
|
127
|
+
const ed = process.env.EDITOR?.trim();
|
|
128
|
+
return ed && /^[\w./-]+$/.test(ed) ? ed : "nvim";
|
|
129
|
+
}
|
|
130
|
+
|
|
87
131
|
function editorCommand(path: string): string {
|
|
88
|
-
|
|
89
|
-
return `!${ed} ${shellQuote(path)}`;
|
|
132
|
+
return `!${safeEditor()} ${shellQuote(path)}`;
|
|
90
133
|
}
|
|
91
134
|
|
|
92
135
|
export async function showSkillManager(ctx: any): Promise<"new" | null> {
|
|
@@ -137,11 +180,18 @@ export async function showSkillManager(ctx: any): Promise<"new" | null> {
|
|
|
137
180
|
const close = () => done(null);
|
|
138
181
|
|
|
139
182
|
const doDelete = (entry: SkillEntry) => {
|
|
183
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
140
184
|
try {
|
|
141
185
|
if (entry.isDirectorySkill && entry.filePath.endsWith("SKILL.md") &&
|
|
142
186
|
(entry.category === "global" || entry.category === "project")) {
|
|
187
|
+
if (!isContainedInSkillRoots(entry.baseDir, cwd)) {
|
|
188
|
+
throw new Error(`refusing to delete outside skill roots: ${entry.baseDir}`);
|
|
189
|
+
}
|
|
143
190
|
rmSync(entry.baseDir, { recursive: true, force: true });
|
|
144
191
|
} else {
|
|
192
|
+
if (!isContainedInSkillRoots(entry.filePath, cwd)) {
|
|
193
|
+
throw new Error(`refusing to delete outside skill roots: ${entry.filePath}`);
|
|
194
|
+
}
|
|
145
195
|
rmSync(entry.filePath, { force: true });
|
|
146
196
|
}
|
|
147
197
|
ctx.ui.notify(`Skill deleted: ${entry.name}`, "info");
|
|
@@ -479,6 +529,10 @@ export function registerSkillManagerCommand(
|
|
|
479
529
|
return;
|
|
480
530
|
}
|
|
481
531
|
const sub = args?.trim().split(/\s+/)[0]?.toLowerCase();
|
|
532
|
+
if (sub === "deck") {
|
|
533
|
+
await openWishcraftDeck(rt, ctx, "skills");
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
482
536
|
if (sub === "doctor") {
|
|
483
537
|
await runDoctor(ctx);
|
|
484
538
|
return;
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
passAutocompleteProviderThroughPreviousEditor,
|
|
22
22
|
} from "../../editor/autocomplete-chain.ts";
|
|
23
23
|
import { subscribeGitUpdates } from "../../git/status.ts";
|
|
24
|
-
import { ansi, getFgAnsiCode } from "../../theme/colors.ts";
|
|
24
|
+
import { ansi, colorEnabled, getFgAnsiCode } from "../../theme/colors.ts";
|
|
25
25
|
import {
|
|
26
26
|
restorePromptHistory,
|
|
27
27
|
snapshotPromptHistory,
|
|
@@ -320,7 +320,8 @@ export function setupCustomEditor(
|
|
|
320
320
|
return originalRender(width);
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
-
const bc = (s: string) =>
|
|
323
|
+
const bc = (s: string) =>
|
|
324
|
+
`${getFgAnsiCode("sep")}${s}${colorEnabled() ? ansi.reset : ""}`;
|
|
324
325
|
const captureDraft =
|
|
325
326
|
!rt.bashModeActive && isSigilIdeaDraft(editor.getExpandedText());
|
|
326
327
|
const promptGlyph = rt.bashModeActive
|
|
@@ -330,8 +331,10 @@ export function setupCustomEditor(
|
|
|
330
331
|
: ">";
|
|
331
332
|
const promptColor = captureDraft
|
|
332
333
|
? getFgAnsiCode("queue")
|
|
333
|
-
:
|
|
334
|
-
|
|
334
|
+
: colorEnabled()
|
|
335
|
+
? ansi.getFgAnsi(200, 200, 200)
|
|
336
|
+
: "";
|
|
337
|
+
const prompt = `${promptColor}${promptGlyph}${colorEnabled() ? ansi.reset : ""}`;
|
|
335
338
|
const promptPrefix = ` ${prompt} `;
|
|
336
339
|
const contPrefix = " ";
|
|
337
340
|
const contentWidth = Math.max(1, width - 3);
|