diff --git a/extensions/capabilities/index.ts b/extensions/capabilities/index.ts index 25835ecd..c8d66370 100644 --- a/extensions/capabilities/index.ts +++ b/extensions/capabilities/index.ts @@ -85,6 +85,29 @@ export function createCapabilitiesExtension( }, ) { return function capabilities(pi: ExtensionAPI) { + let shimmerPhase = 0; + let shimmerTimer: ReturnType | undefined; + let shimmerActive = false; + + const stopShimmer = () => { + if (shimmerTimer !== undefined) clearInterval(shimmerTimer); + shimmerTimer = undefined; + shimmerActive = false; + }; + + const setShimmerActive = (active: boolean, requestRender: () => void) => { + shimmerActive = active; + if (!active) { + stopShimmer(); + return; + } + shimmerTimer ??= setInterval(() => { + if (!shimmerActive) return; + shimmerPhase = (shimmerPhase + 0.08) % 1; + requestRender(); + }, 120); + }; + const reconcileDiscoveryGateway = () => { const adaptive = dependencies.loadConfig().capabilities.discovery === "adaptive"; @@ -98,6 +121,8 @@ export function createCapabilitiesExtension( pi.events.on(SETUP_CONFIG_CHANGED_CHANNEL, reconcileDiscoveryGateway); pi.on("session_start", (_event, ctx) => { + stopShimmer(); + shimmerPhase = 0; resetOpenPiToolSurface( pi, dependencies.sourcePath @@ -108,17 +133,27 @@ export function createCapabilitiesExtension( registerEditorLayer(pi, ctx, { id: "capability-intent-highlight", order: 150, - wrap: (base, _tui, _theme, keybindings) => - new CapabilityIntentHighlightEditor(base, keybindings, (text) => - colorCapabilityKeyword(text, { - colorMode: ctx.ui.theme.getColorMode(), - light: isLightNamedTheme(ctx.ui.theme.name), - }), - ), + wrap: (base, tui, _theme, keybindings) => { + return new CapabilityIntentHighlightEditor( + base, + keybindings, + (text) => + colorCapabilityKeyword( + text, + { + colorMode: ctx.ui.theme.getColorMode(), + light: isLightNamedTheme(ctx.ui.theme.name), + }, + shimmerPhase, + ), + (active) => setShimmerActive(active, () => tui.requestRender()), + ); + }, }); }); pi.on("session_shutdown", () => { + stopShimmer(); removeEditorLayer(pi, "capability-intent-highlight"); }); diff --git a/extensions/capabilities/src/ui.ts b/extensions/capabilities/src/ui.ts index e8a7921f..dc535056 100644 --- a/extensions/capabilities/src/ui.ts +++ b/extensions/capabilities/src/ui.ts @@ -15,6 +15,21 @@ interface CapabilityKeywordColorOptions { readonly light: boolean; } +type Rgb = readonly [number, number, number]; + +const DARK_SHIMMER_PALETTE: readonly Rgb[] = [ + [210, 168, 255], + [239, 220, 255], + [178, 125, 244], + [210, 168, 255], +]; +const LIGHT_SHIMMER_PALETTE: readonly Rgb[] = [ + [130, 80, 223], + [92, 42, 174], + [161, 111, 239], + [130, 80, 223], +]; + export function isLightNamedTheme(name: string | undefined) { return name !== undefined && /(?:^|[-_])light(?:$|[-_])/iu.test(name); } @@ -26,6 +41,7 @@ export function isLightNamedTheme(name: string | undefined) { export function colorCapabilityKeyword( text: string, options: CapabilityKeywordColorOptions, + phase?: number, ) { const start = options.light ? options.colorMode === "truecolor" @@ -34,7 +50,34 @@ export function colorCapabilityKeyword( : options.colorMode === "truecolor" ? "\u001b[38;2;210;168;255m" : "\u001b[38;5;183m"; - return `${start}${text}${FOREGROUND_RESET}`; + if (phase === undefined || options.colorMode !== "truecolor") { + return `${start}${text}${FOREGROUND_RESET}`; + } + + const palette = options.light ? LIGHT_SHIMMER_PALETTE : DARK_SHIMMER_PALETTE; + const chars = [...text]; + const span = Math.max(chars.length - 1, 1); + const normalized = ((phase % 1) + 1) % 1; + const sample = (position: number) => { + const scaled = ((((position + normalized) % 1) + 1) % 1) * palette.length; + const index = Math.floor(scaled); + const amount = scaled - index; + const from = palette[index]!; + const to = palette[(index + 1) % palette.length]!; + return [ + Math.round(from[0] + (to[0] - from[0]) * amount), + Math.round(from[1] + (to[1] - from[1]) * amount), + Math.round(from[2] + (to[2] - from[2]) * amount), + ]; + }; + return ( + chars + .map((character, index) => { + const [red, green, blue] = sample(index / span); + return `\u001b[38;2;${red};${green};${blue}m${character}`; + }) + .join("") + FOREGROUND_RESET + ); } export function highlightCapabilityNames( @@ -59,11 +102,13 @@ export function highlightCapabilityNames( */ export class CapabilityIntentHighlightEditor extends BelowEditorNavigationEditor { private readonly highlight: (text: string) => string; + private readonly onShimmerActive?: (active: boolean) => void; constructor( base: EditorComponent, keybindings: KeybindingsManager, highlight: (text: string) => string, + onShimmerActive?: (active: boolean) => void, ) { super( base, @@ -74,14 +119,15 @@ export class CapabilityIntentHighlightEditor extends BelowEditorNavigationEditor () => undefined, ); this.highlight = highlight; + this.onShimmerActive = onShimmerActive; } override render(width: number) { const capabilities = capabilitiesRequestedByPrompt(this.getText()); - if ( - !capabilities.includes("delegate") && - !capabilities.includes("workflow") - ) { + const active = + capabilities.includes("delegate") || capabilities.includes("workflow"); + this.onShimmerActive?.(active); + if (!active) { return super.render(width); } return super diff --git a/tests/extensions/capabilities/ui.test.ts b/tests/extensions/capabilities/ui.test.ts index 8cc3b24a..237f0241 100644 --- a/tests/extensions/capabilities/ui.test.ts +++ b/tests/extensions/capabilities/ui.test.ts @@ -75,6 +75,33 @@ test("uses Claude-style lavender with a contrast-safe light variant", () => { ); }); +test("shimmer phase changes only the keyword colors", () => { + const first = colorCapabilityKeyword( + "workflow", + { colorMode: "truecolor", light: false }, + 0, + ); + const second = colorCapabilityKeyword( + "workflow", + { colorMode: "truecolor", light: false }, + 0.4, + ); + assert.notEqual(first, second); + assert.match(first, /^\u001b\[38;2;\d+;\d+;\d+m/u); + assert.equal( + first.replace(/\u001b\[38;2;\d+;\d+;\d+m/gu, "").replace("\u001b[39m", ""), + "workflow", + ); + assert.equal( + colorCapabilityKeyword( + "workflow", + { colorMode: "256color", light: false }, + 0.4, + ), + "\u001b[38;5;183mworkflow\u001b[39m", + ); +}); + test("highlights capability names only when shared intent authorizes them", () => { const reserved = editor("subagent, workflow"); assert.deepEqual(reserved.highlighted.render(120), [