[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View File

@@ -0,0 +1,170 @@
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import type { TerminalTheme } from '../../domain/models';
import {
idleThemeUserIntent,
pickingThemeUserIntent,
resolveGlobalTerminalAppearance,
resolveTerminalAppearance,
type ResolvedAppearance,
type TerminalAppearanceHostScope,
type TerminalAppearanceSettings,
type ThemeUserIntent,
} from '../../domain/terminalAppearanceRuntime';
import { getFollowAppTerminalThemeSelectionUpdate } from '../../domain/terminalAppearance';
import {
injectTerminalAppearanceVars,
} from '../../infrastructure/theme/terminalAppearanceVars';
export type ThemeRuntimeSettings = TerminalAppearanceSettings & {
customThemes: TerminalTheme[];
setTheme: (theme: 'light' | 'dark' | 'system') => void;
setLightUiThemeId: (id: string) => void;
setDarkUiThemeId: (id: string) => void;
};
export function useThemeRuntime(settings: ThemeRuntimeSettings) {
const {
terminalThemeId,
terminalThemeDarkId,
terminalThemeLightId,
followAppTerminalTheme,
resolvedTheme,
lightUiThemeId,
darkUiThemeId,
accentMode,
customAccent,
customThemes,
setTheme,
setLightUiThemeId,
setDarkUiThemeId,
} = settings;
const [userIntent, setUserIntent] = useState<ThemeUserIntent>(idleThemeUserIntent());
// Theme-id settings without accent — keeps resolveFocusedAppearance identity
// stable during color-picker drag (TerminalLayer memo depends on it).
const appearanceSettingsBase = useMemo((): Omit<TerminalAppearanceSettings, 'accentMode' | 'customAccent'> => ({
terminalThemeId,
terminalThemeDarkId,
terminalThemeLightId,
followAppTerminalTheme,
resolvedTheme,
lightUiThemeId,
darkUiThemeId,
}), [
terminalThemeId,
terminalThemeDarkId,
terminalThemeLightId,
followAppTerminalTheme,
resolvedTheme,
lightUiThemeId,
darkUiThemeId,
]);
const accentRef = useRef({ accentMode, customAccent });
accentRef.current = { accentMode, customAccent };
// Live accent settings are only for CSS-var injection. The published bag must
// resolve against a stable base theme so accent drag does not rebuild
// TerminalHost / AppShell domains every HSL tick.
const appearanceSettingsForInject = useMemo((): TerminalAppearanceSettings => ({
...appearanceSettingsBase,
accentMode,
customAccent,
}), [appearanceSettingsBase, accentMode, customAccent]);
const globalAppearance = useMemo(() => resolveGlobalTerminalAppearance({
userIntent,
settings: {
...appearanceSettingsBase,
accentMode: 'theme',
customAccent: '',
},
customThemes,
}), [userIntent, appearanceSettingsBase, customThemes]);
const accentedGlobalAppearance = useMemo(() => resolveGlobalTerminalAppearance({
userIntent,
settings: appearanceSettingsForInject,
customThemes,
}), [userIntent, appearanceSettingsForInject, customThemes]);
// Read accent from a ref so this callback identity does not churn on drag.
// Callers that invoke it after an accent-only update still get the latest
// accent; Terminal panes also re-apply appearanceChromeStore at the leaf.
const resolveFocusedAppearance = useCallback((hostScope: TerminalAppearanceHostScope): ResolvedAppearance => (
resolveTerminalAppearance({
userIntent,
settings: {
...appearanceSettingsBase,
accentMode: accentRef.current.accentMode,
customAccent: accentRef.current.customAccent,
},
hostScope,
customThemes,
})
), [userIntent, appearanceSettingsBase, customThemes]);
const applyFollowAppSettingsForPick = useCallback((themeId: string) => {
const update = getFollowAppTerminalThemeSelectionUpdate(themeId);
if (!update) return false;
if (update.appTheme === 'dark') {
setDarkUiThemeId(update.uiThemeId);
} else {
setLightUiThemeId(update.uiThemeId);
}
setTheme(update.appTheme);
return true;
}, [setDarkUiThemeId, setLightUiThemeId, setTheme]);
const pickTheme = useCallback((themeId: string, options?: { followApp?: boolean; scopeHostId?: string | null }) => {
const followApp = options?.followApp ?? followAppTerminalTheme;
setUserIntent(pickingThemeUserIntent(themeId, {
scopeHostId: followApp ? undefined : options?.scopeHostId,
}));
if (followApp) {
applyFollowAppSettingsForPick(themeId);
}
}, [applyFollowAppSettingsForPick, followAppTerminalTheme]);
const clearIntent = useCallback(() => {
setUserIntent(idleThemeUserIntent());
}, []);
const settleManualIntent = useCallback(() => {
setUserIntent(idleThemeUserIntent());
}, []);
// Stable bag identity so App domain memos can depend on members (or the
// bag) without thrashing on every parent render. Accented appearance is
// intentionally excluded — injection reads it separately.
return useMemo(() => ({
userIntent,
globalAppearance,
accentedGlobalAppearance,
resolveFocusedAppearance,
pickTheme,
clearIntent,
settleManualIntent,
currentTerminalTheme: globalAppearance.theme,
}), [
userIntent,
globalAppearance,
accentedGlobalAppearance,
resolveFocusedAppearance,
pickTheme,
clearIntent,
settleManualIntent,
]);
}
export function useTerminalAppearanceInjection(
appearance: ResolvedAppearance,
options?: { includeChromeSurfaces?: boolean },
): void {
const includeChromeSurfaces = options?.includeChromeSurfaces ?? true;
useLayoutEffect(() => {
injectTerminalAppearanceVars(appearance.theme, { includeChromeSurfaces });
}, [appearance.theme.id, appearance.theme, includeChromeSurfaces]);
}