Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions docs/development/native-appearance-welcome/CHECKPOINT.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"phase": "Slice 3 (Ghostty native shaders) implemented, independently reviewed, fixed, canonically verified clean; committed on feature/ghostty-native-shaders-20261028",
"phase": "Slice 4 (render routing + Chroma-driven Aurora) implemented, reviewed, focused-verified; committed on feature/render-routing-20261028",
"updated": "2026-10-28",
"slice1": {
"branch": "feature/appearance-welcome-20261028",
Expand All @@ -23,7 +23,7 @@
513,
514
],
"next": "Slice 4: render routing (src/motion/renderRouting.ts) + Chroma-driven Aurora colors; then Slices 5-7. Human: physical Ghostty QA of /appearance \u203a Ghostty shaders.",
"next": "Slice 5: Shader Studio UX with previews; then 6 Welcome Studio (decoder #518), 7 integration/perf/QA/docs.",
"slice3": {
"branch": "feature/ghostty-native-shaders-20261028",
"base": "de2afd0254327390205a5cde62af5d886f8efaaf",
Expand All @@ -39,13 +39,21 @@
"ca90b2bc"
],
"tests": "graphics 113/113 under guard",
"review": "Sonnet: no blocker; should-fix items fixed by Codex follow-up"
"review": "Sonnet: no blocker; should-fix items fixed by Codex follow-up",
"pr": 518
},
"mermaid_ci": {
"failure": "#516 run 40656373289 browserConsoleChrome.test.ts:106 (not rerun)",
"diagnosis": "mermaid-diag/REPORT.md (Codex + lead addendum)",
"local": "pinned mermaid@12.2.0, 3/3 pass at dev c690de05+diag and at #516 de2afd02",
"diag_commit": "9270894 on local diag/mermaid-ci-20261028 (unpushed)",
"verdict": "not reproduced; deadline-under-load hypothesis unproven"
}
},
"slice4": {
"branch": "feature/render-routing-20261028",
"base": "87277285 (Slice 3, PR #517)",
"tests": "focused 162/162; tsc clean; full suite left to CI per user",
"review": "Sonnet: no blocker; 5 should-fix fixed"
},
"verification_policy": "User 2026-10-28: run only tests related to the current work; no full-suite reruns of unrelated tests"
}
9 changes: 9 additions & 0 deletions docs/development/native-appearance-welcome/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,12 @@ Ruling: preserve valid unchanged numeric blur above the normal UI range during u
- Architecture: TerminalApp must not mention host products (existing guard test). All Ghostty wiring lives in `src/appearance/glslStudio.ts`; the Kitty panel and the Ghostty panel are one shared `ShaderStudioPanel` with host wording supplied per host. The footer wraps with the shared `renderControlRows`.
- Sync: gate changes rewrite only NMSh-owned files; removing passes never waits for review; adding reviewed passes back re-simulates. Inside tmux (hidden version) sync uses the version recorded at review, so it never strips passes for an unrelated reason. NMSh also syncs once at startup (no process-table read), so gate changes made while it was not running (OS Reduced Motion, an edited config) reach the written passes.
- Animation disclosure: Ghostty runs its animation loop whenever any custom shader is listed (focused surface by default; `always` = every surface; `false` = only on terminal updates). Every plan discloses this; no preset claims to redraw "only while it fades".

## Slice 4 rulings (render routing and Chroma-driven native colors, 2026-10-28)

- One animation model: `Transitions`/`MotionGate` (NMSh rows) and `chooseBackend` (cursor) stay the only decision makers. `src/motion/renderRouting.ts` is a read-only view that reports, per semantic surface, whether the host GPU, NMSh's Portable renderer or nothing draws it, and why. It never schedules or draws.
- Native routing only where the host gives reliable targeting: the cursor (cursor uniforms, via the cursor backend) and whole-window focus/display passes that NMSh wrote (Ghostty ledger / Kitty ledger, hook present). Prompt transitions, command feedback, notifications, idle and welcome are always Portable or off: a terminal shader sees the whole window, not NMSh's rows. No per-widget GPU claim.
- Gates are applied as the app applies them: Effects Off, Reduced Motion and no color switch NMSh rows and Portable cursor drawing off; Settings › Motion rows set to Off are off; cursor effect None and motion Off are off; screen-mode screensavers do not start by themselves under Reduced Motion. A native trail under a gate is reported with "NMSh removes its host shader on the next sync".
- Written ≠ loaded: native focus/display routes say "written; the host loads it on its next config reload".
- `/appearance › Effects routing` shows the routes (refreshed on entering the hub or the view; no polling). The host-specific reads live in `src/appearance/nativePasses.ts`; TerminalApp stays host-neutral.
- Chroma: Ghostty Aurora gains Colors = Follow Chroma (default) / Custom. Follow Chroma uses the idle visuals' resolver (`idleStops(config, 'appearance')`: Chroma palette, or the theme while Chroma is off), its first and last stops. A Chroma or theme settings change re-syncs NMSh's Ghostty passes (only NMSh-owned files, only when the generated source differs); never per event; Ghostty applies it on its next reload. Aurora settings saved before the choice existed and carrying their own colors keep them (Custom).
7 changes: 7 additions & 0 deletions docs/development/native-appearance-welcome/HANDOFF.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Native appearance and Welcome Studio handoff

## Slice 4 state (render routing) — read first

- Branch `feature/render-routing-20261028` (worktree `/Volumes/T7Tmp/ai-dev/worktrees/notMyShell/render-routing-20261028`) stacked on Slice 3 (`feature/ghostty-native-shaders-20261028`, PR #517). Decoder PR #518 targets dev independently.
- Code map: `src/motion/renderRouting.ts` (read-only routes), `src/appearance/nativePasses.ts` (host-neutral written passes), hub view `routing` in `AppearanceHub.ts`, `chromaEnds` in `glslStudio.ts`, Aurora `COLORS` in `ghosttyShaderCatalog.ts`.
- Verification policy (user, 2026-10-28): run only tests related to the current work; CI is the full check.
- Next: Slice 5 Shader Studio previews; Slice 6 Welcome Studio on #518; Slice 7.

## Slice 3 state (Ghostty native shaders) — read first

- Branch `feature/ghostty-native-shaders-20261028` (worktree `/Volumes/T7Tmp/ai-dev/worktrees/notMyShell/ghostty-native-shaders-20261028`) is stacked on Slice 2 `feature/kitty-native-shaders-20261028` @ de2afd02 (PR #516, draft). Its draft PR targets the Slice 2 branch; retarget only after review, never automatically.
Expand Down
8 changes: 8 additions & 0 deletions docs/development/native-appearance-welcome/TEST_RESULTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,11 @@ Source: branch feature/ghostty-native-shaders-20261028 from de2afd02. Every auto
- Mermaid (#516 run 40656373289, browserConsoleChrome.test.ts:106): not rerun in CI; local controlled runs 3/3 pass at dev+diagnostic and at #516 (mermaid-diag/REPORT.md). Unexplained in CI; deadline-under-load hypothesis, not proven.

NOT RUN: physical Ghostty/Kitty/Zed QA, GPU rendering, performance measurement.

## Slice 4 — render routing and Chroma-driven native colors (Claude, 2026-10-28)

- Focused tests covering the changed modules and their consumers (renderRouting, ghosttyShaders, ghosttyShaderPanel, ghosttyShadersLive, appearanceHub, kittyShader*, hostIntegration, hostAppearance*, cursor*, idleVisuals, motionTransitions): 162/162 (slice4-focused-final.log); tsc clean; diff --check pass.
- Full `npm run verify` was started but stopped at the user's direction (2026-10-28): the machine was under heavy external load (another session's xcodebuild, load 50–130), which produced live shell startup timeouts unrelated to this slice. Per the user, only tests related to the current work are run locally; CI on the pushed head is the full check.
- Independent Sonnet review: no blocker; five should-fix items fixed with regressions.

NOT RUN: physical Ghostty/Kitty QA, GPU rendering.
8 changes: 8 additions & 0 deletions docs/development/native-appearance-welcome/progress.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Native appearance / welcome progress

## Slice 4 — render routing and Chroma-driven native colors (Claude, 2026-10-28)

Branch `feature/render-routing-20261028` from 87277285 (Slice 3, PR #517) in worktree `/Volumes/T7Tmp/ai-dev/worktrees/notMyShell/render-routing-20261028`.

- Implemented (DECISIONS, Slice 4): `src/motion/renderRouting.ts`; `src/appearance/nativePasses.ts` (`writtenGhosttyPasses`, `writtenKittyPresets`); `/appearance › Effects routing` hub view; Aurora Follow Chroma colors (`chromaEnds`) with sync on Chroma/theme changes.
- Independent Sonnet review: no blocker; 5 should-fix (effect None, gated cursor rows, idle under Reduced Motion, legacy Aurora colors, Motion rows Off) + refresh-on-enter fixed with regressions.
- Tests: tests/renderRouting.test.ts; hub test row indices shifted by the new row.

## Slice 3 — Ghostty native shaders (Claude, 2026-10-28)

Branch `feature/ghostty-native-shaders-20261028` from de2afd02 (head of draft PR #516, Slice 2) in worktree `/Volumes/T7Tmp/ai-dev/worktrees/notMyShell/ghostty-native-shaders-20261028`. Slice 1/2 worktrees untouched; #513/#514/#515/#516 unmerged and unchanged.
Expand Down
21 changes: 20 additions & 1 deletion src/app/TerminalApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,8 @@ import {appearanceHubKey, createAppearanceHub, hubMotionPreview, renderAppearanc
import {createKittyShaderPanel, kittyShaderKey, renderKittyShaderPanel, type KittyShaderPanelState} from '../appearance/KittyShaderPanel.js';
import {discoverKittyCatalog, kittyBinary, type KittyCatalog} from '../appearance/kittyShaderCatalog.js';
import {GlslShaderStudio} from '../appearance/glslStudio.js';
import {writtenNativePasses} from '../appearance/nativePasses.js';
import {renderRoutes} from '../motion/renderRouting.js';
import {applyKittyShaderPlan, inspectKittyLaunch, inspectKittyShaders, kittyShaderPaths, planKittyShaderRemoval, planKittyShaders, readProcessCommand, syncKittyShaders, writtenKittyTrail,
type KittyShaderContext, type ShaderGates} from '../appearance/kittyShaders.js';
import {diffModules, progress, transitionPaint, Transitions, type MotionGate} from '../motion/transitions.js';
Expand Down Expand Up @@ -641,7 +643,10 @@ export class TerminalApp {
const cursorLook = JSON.stringify(previous.cursor) !== JSON.stringify(next.cursor) || previous.nmsh.palette !== next.nmsh.palette || previous.nmsh.accent !== next.nmsh.accent
|| JSON.stringify(previous.customTheme) !== JSON.stringify(next.customTheme);
// The GLSL studio's passes first: a written studio trail pauses NMSh's older /cursor shader (never two trails).
if (cursorLook || previous.presentation.reducedMotion !== next.presentation.reducedMotion || previous.presentation.effectsOff !== next.presentation.effectsOff) this.glslStudio.sync(next);
// Effects that follow Chroma are rewritten when the Chroma palette changes (a settings change, never a per-event write).
const chromaLook = previous.presentation.preset !== next.presentation.preset || JSON.stringify(previous.presentation.customStops) !== JSON.stringify(next.presentation.customStops)
|| previous.nmsh.vibrance !== next.nmsh.vibrance;
if (cursorLook || chromaLook || previous.presentation.reducedMotion !== next.presentation.reducedMotion || previous.presentation.effectsOff !== next.presentation.effectsOff) this.glslStudio.sync(next);
if (cursorLook) this.syncCursorNative(next);
if (previous.cursor.renderer !== next.cursor.renderer || previous.presentation.reducedMotion !== next.presentation.reducedMotion
|| previous.presentation.effectsOff !== next.presentation.effectsOff) void this.syncKittyShaderFiles(next);
Expand Down Expand Up @@ -2503,7 +2508,9 @@ export class TerminalApp {
return;
}
if (this.appearanceHub) {
const wasRouting = this.appearanceHub.view === 'routing';
const action = appearanceHubKey(this.appearanceHub, key, this.promptConfiguration);
if (!wasRouting && this.appearanceHub.view === 'routing') this.refreshRoutes(this.appearanceHub);
if (action?.kind === 'close') { this.appearanceHub = undefined; this.returnFromPanel(); }
else if (action?.kind === 'motion') this.updateConfiguration(configuration => { configuration.motion = action.motion; });
else if (action?.kind === 'saveHost') void this.saveAppearance();
Expand Down Expand Up @@ -3867,6 +3874,7 @@ export class TerminalApp {

/** In Kitty the hub gains its shader row; the summary says what is chosen, never that it is loaded. */
private addKittyShaderRow(hub: AppearanceHubState): void {
this.refreshRoutes(hub);
if (this.glslStudio.available()) { this.glslStudio.addHubRow(hub); return; }
if (terminalProfile(process.env) !== 'kitty') return;
const chosen = this.promptConfiguration.hostShaders.kitty.effects.filter(effect => effect.enabled).length;
Expand Down Expand Up @@ -12698,6 +12706,17 @@ export class TerminalApp {
else if (!busy && this.transitionClock) { this.transitionClock(); this.transitionClock = undefined; }
}

/** /appearance › Effects routing: who draws each animated surface now (read from the cursor choice, gates and written passes). */
private refreshRoutes(hub: AppearanceHubState): void {
const config = this.promptConfiguration;
const native = writtenNativePasses();
hub.routes = renderRoutes({gate: {reducedMotion: isReducedMotion() || config.presentation.reducedMotion, effectsOff: config.presentation.effectsOff, color: colorLevel() !== 'none'},
look: config.motion.rendering === 'rich' ? 'rich' : 'clean', cursor: this.cursorBackend(), cursorMotion: config.cursor.motion, cursorEffect: config.cursor.effect,
...(native.hostName ? {hostName: native.hostName} : {}), nativePasses: native.passes, idleTimeout: config.idleVisuals.timeout,
idleScreenMode: SCREEN_MODE_EFFECT[config.idleVisuals.mode] !== undefined || config.idleVisuals.mode === 'random',
motionOff: {launch: config.motion.commandLaunch === 'off', context: config.motion.contextTransitions === 'off', events: config.motion.eventFeedback === 'off'}});
}

private decorativeMotionAllowed(): boolean {
return !isReducedMotion() && !this.promptConfiguration.presentation.reducedMotion && !this.promptConfiguration.presentation.effectsOff;
}
Expand Down
27 changes: 24 additions & 3 deletions src/appearance/AppearanceHub.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type {SurfaceRoute} from '../motion/renderRouting.js';
import type {HostAppearancePlan} from './hostAppearance.js';
import {stripTerminalControls} from '../util/terminalControls.js';
import {chromaPreviewNote} from './chromaNotes.js';
Expand Down Expand Up @@ -26,7 +27,7 @@ import {BRIDGE_TARGETS, BRIDGE_TARGET_LABELS, effectiveMode} from '../themeBridg
export type HubDestination = 'theme' | 'prompt' | 'cursor' | 'chrome' | 'chroma' | 'themeBridge' | 'kittyShaders' | 'glslShaders';

export interface AppearanceHubState {
view: 'hub' | 'motion' | 'motionAdvanced';
view: 'hub' | 'motion' | 'motionAdvanced' | 'routing';
selected: number;
/** Host window editing (the existing AppearanceState), when the host has appearance integration. */
host?: AppearanceState;
Expand All @@ -44,14 +45,16 @@ export interface AppearanceHubState {
kittyShaders?: string;
/** Present in Ghostty: the Ghostty shaders row's summary (the row exists only there). */
ghosttyShaders?: string;
/** Who draws each animated surface now (read-only; refreshed by the app whenever the hub renders). */
routes?: SurfaceRoute[];
}

export type HubAction = {kind: 'close'} | {kind: 'open'; destination: HubDestination} | {kind: 'motion'; motion: MotionSettings} | {kind: 'saveHost'} | {kind: 'confirmHost'} | {kind: 'restoreHost'};

/** Compact launcher: each row opens its canonical editor (/theme, /prompt, /cursor, UI chrome, /chroma, Motion, /theme-bridge). */
const BASE_ROWS: Array<{id: HubDestination | 'motion'; label: string}> = [
const BASE_ROWS: Array<{id: HubDestination | 'motion' | 'routing'; label: string}> = [
{id: 'theme', label: 'Theme Studio'}, {id: 'prompt', label: 'Prompt'}, {id: 'cursor', label: 'Cursor & effects'}, {id: 'chrome', label: 'UI chrome'},
{id: 'chroma', label: 'Chroma'}, {id: 'motion', label: 'Motion'}, {id: 'themeBridge', label: 'Theme Bridge'},
{id: 'chroma', label: 'Chroma'}, {id: 'motion', label: 'Motion'}, {id: 'themeBridge', label: 'Theme Bridge'}, {id: 'routing', label: 'Effects routing'},
];
/** The launcher rows for this host: Kitty or Ghostty adds its shader studio after Cursor & effects. */
const hubRows = (state: AppearanceHubState) => state.kittyShaders !== undefined
Expand Down Expand Up @@ -87,6 +90,10 @@ export function appearanceHubKey(state: AppearanceHubState, key: Key, configurat
}
return undefined;
}
if (state.view === 'routing') {
if (key.kind === 'escape' || key.kind === 'interrupt' || key.kind === 'enter') { state.view = 'hub'; state.selected = NMSH_ROWS.findIndex(row => row.id === 'routing'); }
return undefined;
}
if (state.view === 'motion' || state.view === 'motionAdvanced') {
const advanced = state.view === 'motionAdvanced';
// The Motion list: Rendering, the five effects, then Advanced (a child screen with the selected rendering's own tuning).
Expand Down Expand Up @@ -118,6 +125,7 @@ export function appearanceHubKey(state: AppearanceHubState, key: Key, configurat
if (key.kind !== 'enter' && key.kind !== 'right') return undefined;
const row = NMSH_ROWS[state.selected]!;
if (row.id === 'motion') { state.view = 'motion'; state.selected = 0; state.previewStart = now; return undefined; }
if (row.id === 'routing') { state.view = 'routing'; return undefined; }
return {kind: 'open', destination: row.id};
}
// Host rows: the existing opacity/blur editor, with Enter saving through the host integration.
Expand Down Expand Up @@ -160,6 +168,17 @@ export function renderAppearanceHub(state: AppearanceHubState, configuration: Pr
...lines.map(line => ` ${stripTerminalControls(line)}`), '', ` ${stripTerminalControls(review.plan.guidance)}`,
renderControls([['↑↓', 'scroll diff'], ['Enter', 'confirm write'], ['Esc', 'cancel']])].map(row => truncateAnsi(row, columns));
}
if (state.view === 'routing') {
const routes = state.routes ?? [];
const routeLabel = {native: `${accent}Host GPU${reset}`, portable: `${secondary}Portable${reset}`, off: `${subtle}Off${reset}`} as const;
const head = [`${primary} Appearance › Effects routing${reset}`,
` ${subtle}Who draws each animated surface now. Host GPU shaders only draw what the host can target (the cursor, whole-window focus and display); NMSh draws its own rows.${reset}`, ''];
const list = routes.flatMap(route => [` ${padCells(route.label, 20)}${routeLabel[route.route]}`, ` ${subtle}${stripTerminalControls(route.reason)}${reset}`]);
const controls = ['', renderControls([['Esc', 'back']])];
const all = [...head, ...list, ...controls];
const rows = height === undefined || all.length <= height ? all : [head[0]!, ...list.slice(0, Math.max(0, height - 3)), ...controls];
return rows.map(row => truncateAnsi(row, columns));
}
if (state.view === 'motion' || state.view === 'motionAdvanced') {
const advanced = state.view === 'motionAdvanced';
const rendering = configuration.motion.rendering === 'rich' ? 'Rich' : 'Clean';
Expand Down Expand Up @@ -200,6 +219,8 @@ export function renderAppearanceHub(state: AppearanceHubState, configuration: Pr
chroma: configuration.presentation.preset === 'off' ? 'Off' : `${configuration.presentation.preset} · ${configuration.presentation.motion}`,
kittyShaders: state.kittyShaders ?? '',
glslShaders: state.ghosttyShaders ?? '',
routing: (() => { const routes = state.routes ?? []; const count = (value: string) => routes.filter(route => route.route === value).length;
return routes.length ? [count('native') ? `${count('native')} on host GPU` : '', `${count('portable')} Portable`, count('off') ? `${count('off')} off` : ''].filter(Boolean).join(' · ') : 'Who draws each effect'; })(),
motion: anyMotion ? `${motion.rendering === 'rich' ? 'Rich' : 'Clean'} · Launch ${MOTION_LABELS[motion.commandLaunch]} · Context ${MOTION_LABELS[motion.contextTransitions]} · Events ${MOTION_LABELS[motion.eventFeedback]}` : 'Off',
};
const rows = [`${primary} Appearance${reset}`, ` ${subtle}Everything visual in one place; each row opens its own editor.${reset}`, '', ` ${subtle}NMSh${reset}`];
Expand Down
Loading
Loading