@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,42 @@
|
|
|
1
|
+
import type { MotionEvent } from "../motion/index.ts";
|
|
2
|
+
import { effectiveAppearanceMix, resolveAppearanceMix } from "../config/appearance.ts";
|
|
3
|
+
import type { AppearanceMixConfig } from "../config/types.ts";
|
|
4
|
+
import { config } from "../extension/core/state.ts";
|
|
5
|
+
import type { RuntimeState } from "../extension/core/types.ts";
|
|
6
|
+
import { setSignalEvent, stopSignal } from "./controller.ts";
|
|
7
|
+
|
|
8
|
+
export function dispatchSignalEvent(
|
|
9
|
+
rt: RuntimeState,
|
|
10
|
+
appearance: AppearanceMixConfig,
|
|
11
|
+
event: MotionEvent,
|
|
12
|
+
activity?: string,
|
|
13
|
+
): void {
|
|
14
|
+
const resolved = resolveAppearanceMix(
|
|
15
|
+
effectiveAppearanceMix(appearance, config.preset),
|
|
16
|
+
);
|
|
17
|
+
setSignalEvent(rt.signal, rt.motionScheduler, rt.motionPolicy, event, {
|
|
18
|
+
activity,
|
|
19
|
+
motionId: resolved.motion[event] ?? resolved.signal.animation,
|
|
20
|
+
maxTicks: isFiniteEvent(event) ? 6 : undefined,
|
|
21
|
+
});
|
|
22
|
+
rt.lastLayoutResult = null;
|
|
23
|
+
rt.tuiRef?.requestRender();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function settleSignal(rt: RuntimeState): void {
|
|
27
|
+
stopSignal(rt.signal, rt.motionScheduler, rt.motionPolicy);
|
|
28
|
+
rt.lastLayoutResult = null;
|
|
29
|
+
rt.tuiRef?.requestRender();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isFiniteEvent(event: MotionEvent): boolean {
|
|
33
|
+
return (
|
|
34
|
+
event === "tool.end" ||
|
|
35
|
+
event === "idea.capture" ||
|
|
36
|
+
event === "skill.insert" ||
|
|
37
|
+
event === "policy.deny" ||
|
|
38
|
+
event === "success" ||
|
|
39
|
+
event === "warning" ||
|
|
40
|
+
event === "error"
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Three-lane Signal renderer.
|
|
3
|
+
*
|
|
4
|
+
* Left: model/repository state. Center: live semantic activity and motion rail.
|
|
5
|
+
* Right: context/queue/usage. Modules retain the existing per-segment fault
|
|
6
|
+
* isolation because every item still renders through `renderSegment`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
10
|
+
import type {
|
|
11
|
+
PresetDef,
|
|
12
|
+
SegmentContext,
|
|
13
|
+
SignalSpec,
|
|
14
|
+
StatusLineSegmentId,
|
|
15
|
+
StatusLineSeparatorStyle,
|
|
16
|
+
} from "../config/types.ts";
|
|
17
|
+
import { mergeSegmentsWithCustomItems } from "../config/powerline-config.ts";
|
|
18
|
+
import { renderSegment } from "../segments/index.ts";
|
|
19
|
+
import { ansi, colorEnabled, getFgAnsiCode } from "../theme/colors.ts";
|
|
20
|
+
import { getSeparator } from "../theme/separators.ts";
|
|
21
|
+
import { frameAt, getMotion } from "../motion/index.ts";
|
|
22
|
+
import type { SignalRuntime } from "./controller.ts";
|
|
23
|
+
|
|
24
|
+
export interface SignalRenderOptions {
|
|
25
|
+
separatorStyle: StatusLineSeparatorStyle;
|
|
26
|
+
signal: SignalSpec;
|
|
27
|
+
ascii?: boolean;
|
|
28
|
+
layout?: import("../config/types.ts").StatusLineLayout | null;
|
|
29
|
+
disabledSegments?: StatusLineSegmentId[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface Module {
|
|
33
|
+
content: string;
|
|
34
|
+
width: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function renderSignal(
|
|
38
|
+
ctx: SegmentContext,
|
|
39
|
+
preset: PresetDef,
|
|
40
|
+
runtime: SignalRuntime,
|
|
41
|
+
availableWidth: number,
|
|
42
|
+
options: SignalRenderOptions,
|
|
43
|
+
): { topContent: string; secondaryContent: string } {
|
|
44
|
+
const merged = mergeSegmentsWithCustomItems(
|
|
45
|
+
preset,
|
|
46
|
+
ctx.effectiveCustomItems,
|
|
47
|
+
{
|
|
48
|
+
layout: options.layout ?? null,
|
|
49
|
+
disabledSegments: options.disabledSegments ?? [],
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
const left = renderModules(merged.leftSegments, ctx);
|
|
53
|
+
const right = renderModules(merged.rightSegments, ctx);
|
|
54
|
+
const secondary = renderModules(merged.secondarySegments, ctx);
|
|
55
|
+
const separator = getSeparator(options.separatorStyle).left;
|
|
56
|
+
|
|
57
|
+
const center = renderActivity(runtime, options.signal, options.ascii);
|
|
58
|
+
const top = fitLanes(left, center, right, separator, availableWidth);
|
|
59
|
+
return {
|
|
60
|
+
topContent: top,
|
|
61
|
+
secondaryContent: joinModules(secondary, separator, availableWidth),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function renderModules(ids: readonly StatusLineSegmentId[], ctx: SegmentContext): Module[] {
|
|
66
|
+
const modules: Module[] = [];
|
|
67
|
+
for (const id of ids) {
|
|
68
|
+
// renderSegment is the established per-module error boundary.
|
|
69
|
+
const rendered = renderSegment(id, ctx);
|
|
70
|
+
if (!rendered.visible || !rendered.content) continue;
|
|
71
|
+
modules.push({
|
|
72
|
+
content: rendered.content,
|
|
73
|
+
width: visibleWidth(rendered.content),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return modules;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function renderActivity(
|
|
80
|
+
runtime: SignalRuntime,
|
|
81
|
+
spec: SignalSpec,
|
|
82
|
+
ascii = false,
|
|
83
|
+
): string {
|
|
84
|
+
const def = getMotion(runtime.motionId) ?? getMotion(spec.animation);
|
|
85
|
+
const glyph = def
|
|
86
|
+
? frameAt(def, runtime.tick, ascii)
|
|
87
|
+
: ascii
|
|
88
|
+
? "*"
|
|
89
|
+
: "◆";
|
|
90
|
+
const label = runtime.activity || "ready";
|
|
91
|
+
const open = spec.caps.leftOpen ?? "";
|
|
92
|
+
const close = spec.caps.leftClose ?? "";
|
|
93
|
+
const dim = getFgAnsiCode("sep");
|
|
94
|
+
const reset = colorEnabled() ? ansi.reset : "";
|
|
95
|
+
const rail = runtime.active
|
|
96
|
+
? `${spec.separators.left}${glyph}${spec.separators.right}`
|
|
97
|
+
: `${spec.separators.left}${spec.separators.right}`;
|
|
98
|
+
return `${dim}${open}${rail}${close}${reset} ${label}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function fitLanes(
|
|
102
|
+
leftInput: Module[],
|
|
103
|
+
center: string,
|
|
104
|
+
rightInput: Module[],
|
|
105
|
+
separator: string,
|
|
106
|
+
availableWidth: number,
|
|
107
|
+
): string {
|
|
108
|
+
if (availableWidth <= 0) return "";
|
|
109
|
+
const left = [...leftInput];
|
|
110
|
+
const right = [...rightInput];
|
|
111
|
+
const sep = styledSeparator(separator);
|
|
112
|
+
const centerWidth = visibleWidth(center);
|
|
113
|
+
|
|
114
|
+
const lane = (modules: Module[]) => modules.map((module) => module.content).join(sep);
|
|
115
|
+
const totalWidth = () => {
|
|
116
|
+
const leftText = lane(left);
|
|
117
|
+
const rightText = lane(right);
|
|
118
|
+
return (
|
|
119
|
+
visibleWidth(leftText) +
|
|
120
|
+
visibleWidth(rightText) +
|
|
121
|
+
centerWidth +
|
|
122
|
+
(leftText ? 2 : 0) +
|
|
123
|
+
(rightText ? 2 : 0)
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Preserve the model/git side first, then context/queue. Remove the least
|
|
128
|
+
// important outer modules until all three lanes fit.
|
|
129
|
+
while (totalWidth() + 2 > availableWidth && right.length > 1) right.shift();
|
|
130
|
+
while (totalWidth() + 2 > availableWidth && left.length > 1) left.pop();
|
|
131
|
+
while (totalWidth() + 2 > availableWidth && right.length > 0) right.shift();
|
|
132
|
+
while (totalWidth() + 2 > availableWidth && left.length > 0) left.pop();
|
|
133
|
+
|
|
134
|
+
const leftText = lane(left);
|
|
135
|
+
const rightText = lane(right);
|
|
136
|
+
if (centerWidth + 2 > availableWidth) {
|
|
137
|
+
return ` ${truncatePlain(center, Math.max(0, availableWidth - 2))} `;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const contentWidth =
|
|
141
|
+
visibleWidth(leftText) +
|
|
142
|
+
visibleWidth(rightText) +
|
|
143
|
+
centerWidth +
|
|
144
|
+
(leftText ? 2 : 0) +
|
|
145
|
+
(rightText ? 2 : 0);
|
|
146
|
+
const flexible = Math.max(0, availableWidth - 2 - contentWidth);
|
|
147
|
+
const before = leftText ? " " : "";
|
|
148
|
+
const after = rightText ? " " : "";
|
|
149
|
+
// Extra space lives between center and right, keeping state metrics pinned.
|
|
150
|
+
return ` ${leftText}${before}${center}${after}${" ".repeat(flexible)}${rightText} `;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function joinModules(modules: Module[], separator: string, width: number): string {
|
|
154
|
+
if (modules.length === 0) return "";
|
|
155
|
+
const sep = styledSeparator(separator);
|
|
156
|
+
const fitted: Module[] = [];
|
|
157
|
+
let used = 2;
|
|
158
|
+
for (const module of modules) {
|
|
159
|
+
const next = module.width + (fitted.length ? visibleWidth(separator) + 2 : 0);
|
|
160
|
+
if (used + next > width) break;
|
|
161
|
+
fitted.push(module);
|
|
162
|
+
used += next;
|
|
163
|
+
}
|
|
164
|
+
return fitted.length ? ` ${fitted.map((module) => module.content).join(sep)} ` : "";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function styledSeparator(separator: string): string {
|
|
168
|
+
const color = getFgAnsiCode("sep");
|
|
169
|
+
const reset = colorEnabled() ? ansi.reset : "";
|
|
170
|
+
return ` ${color}${separator}${reset} `;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function truncatePlain(text: string, width: number): string {
|
|
174
|
+
if (width <= 0) return "";
|
|
175
|
+
// Activity labels are ASCII; strip ANSI before this emergency narrow path.
|
|
176
|
+
const plain = text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
177
|
+
return plain.length <= width ? plain : plain.slice(0, Math.max(0, width - 1)) + "…";
|
|
178
|
+
}
|
package/src/theme/colors.ts
CHANGED
|
@@ -12,6 +12,18 @@ export const ansi: AnsiColors = {
|
|
|
12
12
|
reset: "\x1b[0m",
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
// ponytail: NO_COLOR (de-facto standard — present and non-empty) disables all
|
|
16
|
+
// wishcraft color so the status bar stays plain text in no-color terminals
|
|
17
|
+
// and color-blind pipelines. Computed lazily so test env changes take effect.
|
|
18
|
+
let _colorEnabled: boolean | undefined;
|
|
19
|
+
export function colorEnabled(): boolean {
|
|
20
|
+
if (_colorEnabled === undefined) {
|
|
21
|
+
const v = process.env.NO_COLOR;
|
|
22
|
+
_colorEnabled = !(v != null && v !== "");
|
|
23
|
+
}
|
|
24
|
+
return _colorEnabled;
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
function hexToRgb(hex: string): [number, number, number] {
|
|
16
28
|
const cleanHex = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
17
29
|
if (!/^[0-9A-Fa-f]{6}$/.test(cleanHex)) {
|
|
@@ -51,10 +63,12 @@ for (const key of Object.keys(PALETTE)) {
|
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
export function fgOnly(color: ColorName, text: string): string {
|
|
66
|
+
if (!colorEnabled()) return text;
|
|
54
67
|
const code = ansiCodeCache.get(color as string) ?? "";
|
|
55
68
|
return code ? `${code}${text}` : text;
|
|
56
69
|
}
|
|
57
70
|
|
|
58
71
|
export function getFgAnsiCode(color: ColorName): string {
|
|
72
|
+
if (!colorEnabled()) return "";
|
|
59
73
|
return ansiCodeCache.get(color as string) ?? "";
|
|
60
74
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal and host capability detection. Pure: reads an env map, no process
|
|
3
|
+
* globals except the default argument. Motion policy is assembled elsewhere
|
|
4
|
+
* so this file stays free of the motion engine.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface TerminalEnvironment {
|
|
8
|
+
noColor: boolean;
|
|
9
|
+
lowColor: boolean;
|
|
10
|
+
screenReader: boolean;
|
|
11
|
+
reducedMotion: boolean;
|
|
12
|
+
dumb: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const MOTION_LEVELS = ["full", "reduced", "functional", "off"] as const;
|
|
16
|
+
|
|
17
|
+
export function detectEnvironment(
|
|
18
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
19
|
+
): TerminalEnvironment {
|
|
20
|
+
const term = (env.TERM ?? "").toLowerCase();
|
|
21
|
+
const colorterm = (env.COLORTERM ?? "").toLowerCase();
|
|
22
|
+
const noColor = env.NO_COLOR !== undefined && env.NO_COLOR !== "";
|
|
23
|
+
const dumb = term === "dumb" || term === "";
|
|
24
|
+
const lowColor =
|
|
25
|
+
!colorterm.includes("truecolor") &&
|
|
26
|
+
!colorterm.includes("24bit") &&
|
|
27
|
+
(term.includes("vt100") || term === "linux" || /\bansi\b/.test(term));
|
|
28
|
+
const screenReader =
|
|
29
|
+
env.WISHCRAFT_SCREEN_READER === "1" ||
|
|
30
|
+
env.ACCESSIBILITY_SCREEN_READER === "1" ||
|
|
31
|
+
env.NVDA !== undefined ||
|
|
32
|
+
env.VOICEOVER === "1";
|
|
33
|
+
const reducedMotion =
|
|
34
|
+
env.WISHCRAFT_REDUCED_MOTION === "1" ||
|
|
35
|
+
env.PREFER_REDUCED_MOTION === "1" ||
|
|
36
|
+
env.MOZ_PREFER_REDUCED_MOTION === "1";
|
|
37
|
+
|
|
38
|
+
return { noColor, lowColor: lowColor && !noColor, screenReader, reducedMotion, dumb };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function motionLevelFromEnv(
|
|
42
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
43
|
+
): (typeof MOTION_LEVELS)[number] | null {
|
|
44
|
+
const raw = (env.WISHCRAFT_MOTION ?? "").toLowerCase();
|
|
45
|
+
return (MOTION_LEVELS as readonly string[]).includes(raw)
|
|
46
|
+
? (raw as (typeof MOTION_LEVELS)[number])
|
|
47
|
+
: null;
|
|
48
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/theme/tokens/index.ts
|
|
3
|
+
* ---------------------------------------------------------------------------
|
|
4
|
+
* Public exports for Wishcraft Semantic Design Tokens.
|
|
5
|
+
* ---------------------------------------------------------------------------
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export * from "./types.ts";
|
|
9
|
+
export * from "./mapping.ts";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/theme/tokens/mapping.ts
|
|
3
|
+
* ---------------------------------------------------------------------------
|
|
4
|
+
* Default token mapping and legacy SemanticColor bridge for Wishcraft.
|
|
5
|
+
* ---------------------------------------------------------------------------
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ColorScheme, SemanticColor } from "../../config/types.ts";
|
|
9
|
+
import type { WishcraftTokens } from "./types.ts";
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_TOKENS: WishcraftTokens = {
|
|
12
|
+
surface: "#0f172a",
|
|
13
|
+
surfaceRaised: "#1e293b",
|
|
14
|
+
text: "#f8fafc",
|
|
15
|
+
textMuted: "dim",
|
|
16
|
+
primary: "#f59e0b",
|
|
17
|
+
secondary: "#00afaf",
|
|
18
|
+
accent: "#ea580c",
|
|
19
|
+
success: "success",
|
|
20
|
+
warning: "warning",
|
|
21
|
+
error: "error",
|
|
22
|
+
focus: "#38bdf8",
|
|
23
|
+
selection: "#334155",
|
|
24
|
+
motionDim: "dim",
|
|
25
|
+
motionHot: "#fbbf24",
|
|
26
|
+
motionTrail: "#78350f",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Derives legacy SemanticColor configuration from modern WishcraftTokens.
|
|
31
|
+
*/
|
|
32
|
+
export function deriveColorSchemeFromTokens(tokens: WishcraftTokens): Required<ColorScheme> {
|
|
33
|
+
return {
|
|
34
|
+
model: tokens.primary,
|
|
35
|
+
shellMode: tokens.accent,
|
|
36
|
+
path: tokens.secondary,
|
|
37
|
+
gitClean: tokens.success,
|
|
38
|
+
gitDirty: tokens.warning,
|
|
39
|
+
thinking: "thinkingOff",
|
|
40
|
+
thinkingMinimal: "thinkingMinimal",
|
|
41
|
+
thinkingLow: "thinkingLow",
|
|
42
|
+
thinkingMedium: "thinkingMedium",
|
|
43
|
+
context: tokens.textMuted,
|
|
44
|
+
contextWarn: tokens.warning,
|
|
45
|
+
contextError: tokens.error,
|
|
46
|
+
cost: tokens.text,
|
|
47
|
+
tokens: tokens.textMuted,
|
|
48
|
+
queue: tokens.accent,
|
|
49
|
+
separator: tokens.textMuted,
|
|
50
|
+
border: tokens.surfaceRaised,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Creates WishcraftTokens by merging overrides onto defaults.
|
|
56
|
+
*/
|
|
57
|
+
export function createTokens(overrides?: Partial<WishcraftTokens>): WishcraftTokens {
|
|
58
|
+
return {
|
|
59
|
+
...DEFAULT_TOKENS,
|
|
60
|
+
...overrides,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/theme/tokens/types.ts
|
|
3
|
+
* ---------------------------------------------------------------------------
|
|
4
|
+
* Semantic Design Tokens for Wishcraft (Crush pattern).
|
|
5
|
+
* ---------------------------------------------------------------------------
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
|
|
10
|
+
export type ColorValue = ThemeColor | `#${string}`;
|
|
11
|
+
|
|
12
|
+
export interface WishcraftTokens {
|
|
13
|
+
// Surfaces & Backgrounds
|
|
14
|
+
surface: ColorValue;
|
|
15
|
+
surfaceRaised: ColorValue;
|
|
16
|
+
|
|
17
|
+
// Typography
|
|
18
|
+
text: ColorValue;
|
|
19
|
+
textMuted: ColorValue;
|
|
20
|
+
|
|
21
|
+
// Brand & Accents
|
|
22
|
+
primary: ColorValue;
|
|
23
|
+
secondary: ColorValue;
|
|
24
|
+
accent: ColorValue;
|
|
25
|
+
|
|
26
|
+
// Feedback & State
|
|
27
|
+
success: ColorValue;
|
|
28
|
+
warning: ColorValue;
|
|
29
|
+
error: ColorValue;
|
|
30
|
+
|
|
31
|
+
// Interaction
|
|
32
|
+
focus: ColorValue;
|
|
33
|
+
selection: ColorValue;
|
|
34
|
+
|
|
35
|
+
// Motion & Animation Channels
|
|
36
|
+
motionDim: ColorValue;
|
|
37
|
+
motionHot: ColorValue;
|
|
38
|
+
motionTrail: ColorValue;
|
|
39
|
+
}
|
package/src/welcome/renderer.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
-
import { ansi, fgOnly, getFgAnsiCode } from "../theme/colors.ts";
|
|
2
|
+
import { ansi, colorEnabled, fgOnly, getFgAnsiCode } from "../theme/colors.ts";
|
|
3
3
|
import { centerText, fitToWidth, getBoxLayout } from "./layout.ts";
|
|
4
4
|
import type { WelcomeData, WelcomeWidget, WidgetRenderContext } from "./types.ts";
|
|
5
5
|
|
|
@@ -32,7 +32,7 @@ function bold(text: string): string {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export function dim(text: string): string {
|
|
35
|
-
return getFgAnsiCode("sep") + text + ansi.reset;
|
|
35
|
+
return getFgAnsiCode("sep") + text + (colorEnabled() ? ansi.reset : "");
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
function gradientLine(line: string): string {
|
package/src/welcome/whats-new.ts
CHANGED
|
@@ -16,6 +16,9 @@ import { getAgentPath } from "../paths/agent-dirs.ts";
|
|
|
16
16
|
|
|
17
17
|
const MAX_WHATSNEW_LINES = 8;
|
|
18
18
|
const WHATS_NEW_STATE_FILE = "whats-new.json";
|
|
19
|
+
// First GroepOnline release. On a first run (no stored version) the welcome
|
|
20
|
+
// panel shows history from here forward, never the pre-1.0 `0.x` prehistory.
|
|
21
|
+
const FIRST_OWN_RELEASE = "1.0.0";
|
|
19
22
|
|
|
20
23
|
const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
21
24
|
|
|
@@ -37,38 +40,71 @@ function isVersionNewer(version: string, lastSeen: string | null): boolean {
|
|
|
37
40
|
return false;
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
function isVersionAtOrNewer(version: string, floor: string): boolean {
|
|
44
|
+
const next = parseVersion(version);
|
|
45
|
+
const min = parseVersion(floor);
|
|
46
|
+
if (!next || !min) return false;
|
|
47
|
+
for (let i = 0; i < 3; i++) {
|
|
48
|
+
if (next[i] > min[i]) return true;
|
|
49
|
+
if (next[i] < min[i]) return false;
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
/**
|
|
41
55
|
* Extract the bullet lines from released changelog sections whose version is
|
|
42
|
-
* strictly newer than `lastSeenVersion
|
|
43
|
-
*
|
|
44
|
-
*
|
|
56
|
+
* strictly newer than `lastSeenVersion`. On a first run (`lastSeenVersion` is
|
|
57
|
+
* null) `floorVersion` becomes the lower bound, so the panel shows history from
|
|
58
|
+
* the first own release forward and skips the pre-1.0 prehistory. Sections are
|
|
59
|
+
* returned oldest-first (chronological "since your last version") and capped at
|
|
60
|
+
* `maxLines`. Markdown bold markers are stripped; backticks are kept so commands
|
|
61
|
+
* like `/powerline doctor` stay readable in the terminal.
|
|
45
62
|
*/
|
|
46
63
|
export function parseChangelogDelta(
|
|
47
64
|
changelog: string,
|
|
48
65
|
lastSeenVersion: string | null,
|
|
49
66
|
maxLines: number = MAX_WHATSNEW_LINES,
|
|
67
|
+
floorVersion: string | null = FIRST_OWN_RELEASE,
|
|
50
68
|
): string[] {
|
|
51
|
-
|
|
52
|
-
|
|
69
|
+
// The changelog is newest-first; collect per section, then reverse to read
|
|
70
|
+
// oldest-first before capping.
|
|
71
|
+
const sections: string[][] = [];
|
|
72
|
+
let current: string[] | null = null;
|
|
73
|
+
// A stored last-seen version is a strict lower bound (only strictly newer
|
|
74
|
+
// sections). On a first run the floor is inclusive, so the first own release
|
|
75
|
+
// itself is shown while the pre-1.0 prehistory is skipped.
|
|
76
|
+
const matchesBound =
|
|
77
|
+
lastSeenVersion !== null
|
|
78
|
+
? (version: string) => isVersionNewer(version, lastSeenVersion)
|
|
79
|
+
: floorVersion !== null
|
|
80
|
+
? (version: string) => isVersionAtOrNewer(version, floorVersion)
|
|
81
|
+
: () => true;
|
|
53
82
|
|
|
54
83
|
for (const rawLine of changelog.split("\n")) {
|
|
55
84
|
const line = rawLine.trimEnd();
|
|
56
85
|
const versionMatch = line.match(/^##\s+\[([^\]]+)\]/);
|
|
57
86
|
if (versionMatch) {
|
|
58
87
|
const version = versionMatch[1].trim();
|
|
59
|
-
|
|
60
|
-
|
|
88
|
+
const include = version !== "Unreleased" && matchesBound(version);
|
|
89
|
+
current = include ? [] : null;
|
|
90
|
+
if (current) sections.push(current);
|
|
61
91
|
continue;
|
|
62
92
|
}
|
|
63
|
-
if (!
|
|
93
|
+
if (!current) continue;
|
|
64
94
|
if (!line.startsWith("- ")) continue;
|
|
65
95
|
|
|
66
96
|
const cleaned = line.slice(2).trim().replace(/\*\*/g, "").trim();
|
|
67
97
|
if (!cleaned) continue;
|
|
68
|
-
|
|
69
|
-
if (entries.length >= maxLines) break;
|
|
98
|
+
current.push(cleaned);
|
|
70
99
|
}
|
|
71
100
|
|
|
101
|
+
const entries: string[] = [];
|
|
102
|
+
for (const section of sections.reverse()) {
|
|
103
|
+
for (const entry of section) {
|
|
104
|
+
entries.push(entry);
|
|
105
|
+
if (entries.length >= maxLines) return entries;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
72
108
|
return entries;
|
|
73
109
|
}
|
|
74
110
|
|