mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-09 15:58:34 +00:00
3490b0673d
* Cursor: Apply local changes for cloud agent * fix(terminal): stop first SSH fail under StrictMode remount Defer backend boot past the double-invoke, cancel superseded vault init, gate start on vault hydration, and harden hotkey/attach cleanup so the first connect no longer races a stale close or empty keychain. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * perf: finish StrictMode mega-subscriber plan (accent isolation + docs) Stabilize published terminal themes across custom-accent drag, apply accent at chrome/log leaves and CSS injection only, land Phase 0 design/plan docs, and lock Host-bag / Bugbot-clean contracts for the isolation checklist. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * docs: add StrictMode mega-subscriber design and plan artifacts Force-add Phase 0 spec/plan under docs/superpowers (gitignored locally) so the delivery branch carries the approved design and plan copy. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix: clear remaining StrictMode doubles and notes editor jank Gate every AI send and reject concurrent runTurn, latch tray connect flush and settings persistMounted remounts, and stop notes content drafts from rebuilding MDXEditor/shell on each keystroke. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * perf: gate hidden notes panel subscribe and skip host-link annotate on keystrokes Hidden terminal notes mounts keep a live snapshot read but stop listening to publishes. Host-link DOM annotation follows hosts/mode (preview MutationObserver) instead of every markdown value change. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix: latch SSH transport idle TTL notify under StrictMode Avoid duplicate main-process notifySettingsChanged on effect double-invoke when the idle TTL value has not changed. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * perf: freeze inactive vault notes against hosts churn and prefetch MDXEditor Gate the retained VaultNotesSection notesStore subscription while hidden, ignore hosts identity in its memo compare when inactive, and warm the lazy InlineMarkdownEditor chunk when notes become active. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix: clear remaining AI StrictMode double-fire hazards Latch Ask-AI terminal selection on requestId before draft mutation, hoist Codex App Server IPC bridge to App (ref-counted), and make Catty requestApproval join an existing waiter for the same toolCallId. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix: remount-safe AI send latch and skip stale MCP approval IPC Replace the component sendInFlightRef with a module key latch so StrictMode panel remount cannot reopen a mid-flight send, and no-op resolveApproval when the pending entry is already drained. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * test: allow gated useNotesStore in VaultNotesSection memo contract The retained notes section now passes enabled:isActive so hidden mounts do not subscribe; update the VaultView memo source assertion accordingly. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix: keep note drafts across mode toggle and external note publishes Flush ref-only content before edit/preview remount, and skip prop→editor setMarkdown while a local draft diverges from the last synced value. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * chore: strip StrictMode sermon comments after Bugbot/Comment Sicko pass Keep effect-order and public-export contracts; drop remount/perf narration that the latches and tests already encode. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix: remount-safe Ask-AI terminal selection latch Move consumed requestIds to a module Set so StrictMode remount cannot append the same selection chip twice before parent state clears. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * chore: drop remount latch comment Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
171 lines
5.6 KiB
TypeScript
171 lines
5.6 KiB
TypeScript
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]);
|
|
}
|