mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-29 18:11:40 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8c220dbd1 |
No files matched your search
@@ -464,8 +464,6 @@ function AppViewInner({ domains }: AppViewProps) {
|
||||
identities={identities}
|
||||
snippets={snippets}
|
||||
snippetPackages={snippetPackages}
|
||||
notes={notes}
|
||||
noteGroups={noteGroups}
|
||||
sessions={sessions}
|
||||
workspaces={workspaces}
|
||||
knownHosts={effectiveKnownHosts}
|
||||
@@ -529,8 +527,6 @@ function AppViewInner({ domains }: AppViewProps) {
|
||||
updateHosts={updateTerminalHosts}
|
||||
updateSnippets={updateSnippets}
|
||||
updateSnippetPackages={updateSnippetPackages}
|
||||
updateNotes={updateNotes}
|
||||
updateNoteGroups={updateNoteGroups}
|
||||
sftpDefaultViewMode={sftpDefaultViewMode}
|
||||
sftpDoubleClickBehavior={sftpDoubleClickBehavior}
|
||||
sftpAutoSync={sftpAutoSync}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
getAppearanceChromeSnapshot,
|
||||
publishAppearanceChromeSnapshot,
|
||||
subscribeAppearanceChrome,
|
||||
} from './appearanceChromeStore.ts';
|
||||
|
||||
test('appearanceChromeStore notifies subscribers only when accent fields change', () => {
|
||||
const events: string[] = [];
|
||||
const unsubscribe = subscribeAppearanceChrome(() => {
|
||||
const snap = getAppearanceChromeSnapshot();
|
||||
events.push(`${snap.accentMode}:${snap.customAccent}`);
|
||||
});
|
||||
|
||||
publishAppearanceChromeSnapshot({ accentMode: 'custom', customAccent: '#ff0000' });
|
||||
assert.equal(events.at(-1), 'custom:#ff0000');
|
||||
assert.equal(getAppearanceChromeSnapshot().customAccent, '#ff0000');
|
||||
|
||||
publishAppearanceChromeSnapshot({ accentMode: 'custom', customAccent: '#ff0000' });
|
||||
assert.equal(events.length, 1);
|
||||
|
||||
publishAppearanceChromeSnapshot({ accentMode: 'custom', customAccent: '#00ff00' });
|
||||
assert.equal(events.at(-1), 'custom:#00ff00');
|
||||
|
||||
publishAppearanceChromeSnapshot({ accentMode: 'theme', customAccent: '#00ff00' });
|
||||
assert.equal(events.at(-1), 'theme:#00ff00');
|
||||
|
||||
unsubscribe();
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
type Listener = () => void;
|
||||
|
||||
export type AppearanceChromeSnapshot = {
|
||||
accentMode: 'theme' | 'custom';
|
||||
customAccent: string;
|
||||
};
|
||||
|
||||
const DEFAULT_SNAPSHOT: AppearanceChromeSnapshot = Object.freeze({
|
||||
accentMode: 'theme',
|
||||
customAccent: '',
|
||||
});
|
||||
|
||||
/**
|
||||
* External store for app accent chrome so Terminal leaves can apply custom
|
||||
* accent without TerminalLayer rebuilding on every color-picker drag tick.
|
||||
*/
|
||||
class AppearanceChromeStore {
|
||||
private snapshot: AppearanceChromeSnapshot = DEFAULT_SNAPSHOT;
|
||||
private listeners = new Set<Listener>();
|
||||
|
||||
getSnapshot = (): AppearanceChromeSnapshot => this.snapshot;
|
||||
|
||||
subscribe = (listener: Listener): (() => void) => {
|
||||
this.listeners.add(listener);
|
||||
return () => {
|
||||
this.listeners.delete(listener);
|
||||
};
|
||||
};
|
||||
|
||||
setSnapshot(next: AppearanceChromeSnapshot): void {
|
||||
if (
|
||||
this.snapshot.accentMode === next.accentMode
|
||||
&& this.snapshot.customAccent === next.customAccent
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.snapshot = next;
|
||||
for (const listener of this.listeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const appearanceChromeStore = new AppearanceChromeStore();
|
||||
|
||||
export function publishAppearanceChromeSnapshot(
|
||||
snapshot: AppearanceChromeSnapshot,
|
||||
): void {
|
||||
appearanceChromeStore.setSnapshot(snapshot);
|
||||
}
|
||||
|
||||
export function getAppearanceChromeSnapshot(): AppearanceChromeSnapshot {
|
||||
return appearanceChromeStore.getSnapshot();
|
||||
}
|
||||
|
||||
export function subscribeAppearanceChrome(listener: Listener): () => void {
|
||||
return appearanceChromeStore.subscribe(listener);
|
||||
}
|
||||
|
||||
export function useAppearanceChromeStore(): AppearanceChromeSnapshot {
|
||||
return useSyncExternalStore(
|
||||
subscribeAppearanceChrome,
|
||||
getAppearanceChromeSnapshot,
|
||||
getAppearanceChromeSnapshot,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
EMPTY_NOTES_SNAPSHOT,
|
||||
getEmptyNotesSnapshot,
|
||||
getNotesActions,
|
||||
getNotesSnapshot,
|
||||
publishNotesSnapshot,
|
||||
registerNotesActions,
|
||||
subscribeNotes,
|
||||
subscribeNotesNoop,
|
||||
} from './notesStore.ts';
|
||||
|
||||
test('notesStore notifies subscribers only when snapshot identity changes', () => {
|
||||
const events: number[] = [];
|
||||
const unsubscribe = subscribeNotes(() => {
|
||||
events.push(getNotesSnapshot().notes.length);
|
||||
});
|
||||
|
||||
const firstNotes = [{
|
||||
id: 'n1',
|
||||
title: 'One',
|
||||
content: 'body',
|
||||
tags: [],
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
order: 1000,
|
||||
}];
|
||||
const firstGroups = ['Ops'];
|
||||
publishNotesSnapshot({ notes: firstNotes, noteGroups: firstGroups });
|
||||
assert.equal(events.at(-1), 1);
|
||||
assert.equal(getNotesSnapshot().notes, firstNotes);
|
||||
assert.equal(getNotesSnapshot().noteGroups, firstGroups);
|
||||
|
||||
publishNotesSnapshot({ notes: firstNotes, noteGroups: firstGroups });
|
||||
assert.equal(events.length, 1);
|
||||
|
||||
const secondNotes = [
|
||||
...firstNotes,
|
||||
{
|
||||
id: 'n2',
|
||||
title: 'Two',
|
||||
content: 'more',
|
||||
tags: [],
|
||||
createdAt: 2,
|
||||
updatedAt: 2,
|
||||
order: 2000,
|
||||
},
|
||||
];
|
||||
publishNotesSnapshot({ notes: secondNotes, noteGroups: firstGroups });
|
||||
assert.equal(events.at(-1), 2);
|
||||
|
||||
publishNotesSnapshot({ notes: secondNotes, noteGroups: ['Ops', 'DB'] });
|
||||
assert.equal(events.length, 3);
|
||||
assert.deepEqual(getNotesSnapshot().noteGroups, ['Ops', 'DB']);
|
||||
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
test('notesStore gated helpers stay empty and never notify', () => {
|
||||
let called = 0;
|
||||
const unsub = subscribeNotesNoop(() => {
|
||||
called += 1;
|
||||
});
|
||||
publishNotesSnapshot({
|
||||
notes: [{
|
||||
id: 'n1',
|
||||
title: 'One',
|
||||
content: 'body',
|
||||
tags: [],
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
order: 1000,
|
||||
}],
|
||||
noteGroups: ['Ops'],
|
||||
});
|
||||
assert.equal(called, 0);
|
||||
assert.equal(getEmptyNotesSnapshot(), EMPTY_NOTES_SNAPSHOT);
|
||||
assert.equal(getEmptyNotesSnapshot().notes.length, 0);
|
||||
unsub();
|
||||
});
|
||||
|
||||
test('registerNotesActions exposes update handlers', () => {
|
||||
const calls: string[] = [];
|
||||
registerNotesActions({
|
||||
updateNotes: () => {
|
||||
calls.push('notes');
|
||||
},
|
||||
updateNoteGroups: () => {
|
||||
calls.push('groups');
|
||||
},
|
||||
});
|
||||
const actions = getNotesActions();
|
||||
assert.ok(actions);
|
||||
actions!.updateNotes([]);
|
||||
actions!.updateNoteGroups([]);
|
||||
assert.deepEqual(calls, ['notes', 'groups']);
|
||||
registerNotesActions(null);
|
||||
assert.equal(getNotesActions(), null);
|
||||
});
|
||||
@@ -0,0 +1,138 @@
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
import type { VaultNote } from '../../domain/models';
|
||||
|
||||
type Listener = () => void;
|
||||
|
||||
export type NotesSnapshot = {
|
||||
notes: readonly VaultNote[];
|
||||
noteGroups: readonly string[];
|
||||
};
|
||||
|
||||
export type NotesActions = {
|
||||
updateNotes: (notes: VaultNote[]) => void;
|
||||
updateNoteGroups: (groups: string[]) => void;
|
||||
};
|
||||
|
||||
const EMPTY_NOTES: readonly VaultNote[] = Object.freeze([]);
|
||||
const EMPTY_NOTE_GROUPS: readonly string[] = Object.freeze([]);
|
||||
|
||||
export const EMPTY_NOTES_SNAPSHOT: NotesSnapshot = Object.freeze({
|
||||
notes: EMPTY_NOTES,
|
||||
noteGroups: EMPTY_NOTE_GROUPS,
|
||||
});
|
||||
|
||||
/**
|
||||
* External store for vault notes so Notes / AI side-panel consumers can
|
||||
* subscribe without forcing TerminalLayer re-renders on every note edit.
|
||||
*/
|
||||
class NotesStore {
|
||||
private snapshot: NotesSnapshot = EMPTY_NOTES_SNAPSHOT;
|
||||
private actions: NotesActions | null = null;
|
||||
private listeners = new Set<Listener>();
|
||||
private actionListeners = new Set<Listener>();
|
||||
|
||||
getSnapshot = (): NotesSnapshot => this.snapshot;
|
||||
|
||||
subscribe = (listener: Listener): (() => void) => {
|
||||
this.listeners.add(listener);
|
||||
return () => {
|
||||
this.listeners.delete(listener);
|
||||
};
|
||||
};
|
||||
|
||||
setSnapshot(next: NotesSnapshot): void {
|
||||
if (
|
||||
this.snapshot.notes === next.notes
|
||||
&& this.snapshot.noteGroups === next.noteGroups
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.snapshot = next;
|
||||
for (const listener of this.listeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
getActions = (): NotesActions | null => this.actions;
|
||||
|
||||
subscribeActions = (listener: Listener): (() => void) => {
|
||||
this.actionListeners.add(listener);
|
||||
return () => {
|
||||
this.actionListeners.delete(listener);
|
||||
};
|
||||
};
|
||||
|
||||
setActions(next: NotesActions | null): void {
|
||||
if (this.actions === next) return;
|
||||
this.actions = next;
|
||||
for (const listener of this.actionListeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const notesStore = new NotesStore();
|
||||
|
||||
export function publishNotesSnapshot(snapshot: NotesSnapshot): void {
|
||||
notesStore.setSnapshot(snapshot);
|
||||
}
|
||||
|
||||
export function getNotesSnapshot(): NotesSnapshot {
|
||||
return notesStore.getSnapshot();
|
||||
}
|
||||
|
||||
export function subscribeNotes(listener: Listener): () => void {
|
||||
return notesStore.subscribe(listener);
|
||||
}
|
||||
|
||||
/** No-op subscribe for gated (hidden) panel mounts. */
|
||||
export function subscribeNotesNoop(_listener: Listener): () => void {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
export function getEmptyNotesSnapshot(): NotesSnapshot {
|
||||
return EMPTY_NOTES_SNAPSHOT;
|
||||
}
|
||||
|
||||
export function registerNotesActions(actions: NotesActions | null): void {
|
||||
notesStore.setActions(actions);
|
||||
}
|
||||
|
||||
export function getNotesActions(): NotesActions | null {
|
||||
return notesStore.getActions();
|
||||
}
|
||||
|
||||
export function subscribeNotesActions(listener: Listener): () => void {
|
||||
return notesStore.subscribeActions(listener);
|
||||
}
|
||||
|
||||
const noopUpdateNotes: NotesActions['updateNotes'] = () => {};
|
||||
const noopUpdateNoteGroups: NotesActions['updateNoteGroups'] = () => {};
|
||||
|
||||
/**
|
||||
* Subscribe to notes catalog + vault mutation actions for Notes / AI panels.
|
||||
*/
|
||||
export function useNotesStore(): {
|
||||
notes: VaultNote[];
|
||||
noteGroups: string[];
|
||||
updateNotes: NotesActions['updateNotes'];
|
||||
updateNoteGroups: NotesActions['updateNoteGroups'];
|
||||
} {
|
||||
const snapshot = useSyncExternalStore(
|
||||
subscribeNotes,
|
||||
getNotesSnapshot,
|
||||
getNotesSnapshot,
|
||||
);
|
||||
const actions = useSyncExternalStore(
|
||||
subscribeNotesActions,
|
||||
getNotesActions,
|
||||
getNotesActions,
|
||||
);
|
||||
return {
|
||||
notes: snapshot.notes as VaultNote[],
|
||||
noteGroups: snapshot.noteGroups as string[],
|
||||
updateNotes: actions?.updateNotes ?? noopUpdateNotes,
|
||||
updateNoteGroups: actions?.updateNoteGroups ?? noopUpdateNoteGroups,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type SetStateAction } from 'react';
|
||||
|
||||
import { runThemeTransition, type ThemeTransitionMode } from './themeTransition';
|
||||
import { publishAppearanceChromeSnapshot } from './appearanceChromeStore';
|
||||
import { SyncConfig, TerminalSettings, HotkeyScheme, CustomKeyBindings, DEFAULT_KEY_BINDINGS, KeyBinding, UILanguage, SessionLogFormat, normalizeTerminalSettings } from '../../domain/models';
|
||||
import {
|
||||
DEFAULT_HTTP_NETWORK_PROXY,
|
||||
@@ -967,6 +968,8 @@ export const useSettingsState = (options: { enableSettingsSync?: boolean; enable
|
||||
const persistedAppearanceChanged = previousAppearance === null
|
||||
|| hasPersistedAppearanceChanged(previousAppearance, appearanceRender);
|
||||
previousAppearanceRenderRef.current = appearanceRender;
|
||||
// Terminal leaves subscribe here so accent drag does not rebuild TerminalLayer.
|
||||
publishAppearanceChromeSnapshot({ accentMode, customAccent });
|
||||
const tokens = getUiThemeById(resolvedTheme, resolvedTheme === 'dark' ? darkUiThemeId : lightUiThemeId).tokens;
|
||||
const apply = () => applyThemeTokens(theme, resolvedTheme, tokens, accentMode, customAccent);
|
||||
const transitionMode = appearanceTransitionModeRef.current;
|
||||
|
||||
@@ -65,6 +65,10 @@ import {
|
||||
} from "../../domain/connectionLogTerminalData";
|
||||
import { getNextVaultOrder, normalizeVaultOrder } from "../../domain/vaultOrder";
|
||||
import { loadSanitizedShellHistory } from "./shellHistoryPersistence";
|
||||
import {
|
||||
publishNotesSnapshot,
|
||||
registerNotesActions,
|
||||
} from "./notesStore";
|
||||
import { publishShellHistorySnapshot } from "./shellHistoryStore";
|
||||
import { setVaultInitialized } from "./vaultInitStore";
|
||||
import {
|
||||
@@ -828,6 +832,18 @@ export const useVaultState = () => {
|
||||
publishShellHistorySnapshot(shellHistory);
|
||||
}, [shellHistory]);
|
||||
|
||||
// Notes catalog for Notes / AI side panels — keep TerminalLayer off the hot path.
|
||||
useLayoutEffect(() => {
|
||||
publishNotesSnapshot({ notes, noteGroups });
|
||||
}, [notes, noteGroups]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
registerNotesActions({ updateNotes, updateNoteGroups });
|
||||
return () => {
|
||||
registerNotesActions(null);
|
||||
};
|
||||
}, [updateNotes, updateNoteGroups]);
|
||||
|
||||
const addShellHistoryEntry = useCallback(
|
||||
(entry: Omit<ShellHistoryEntry, "id" | "timestamp">) => {
|
||||
setShellHistory((prev) => {
|
||||
|
||||
@@ -66,6 +66,7 @@ import { useStoredBoolean } from "../application/state/useStoredBoolean";
|
||||
import { readOptionalStoredStringValue, useStoredString } from "../application/state/useStoredString";
|
||||
import { useSessionLogBackend } from "../application/state/useSessionLogBackend";
|
||||
import { useTerminalLayoutSuppressActive } from "../application/state/terminalLayoutSuppressStore";
|
||||
import { useAppearanceChromeStore } from "../application/state/appearanceChromeStore";
|
||||
import {
|
||||
shouldPublishPluginTerminalSessionMountLifecycle,
|
||||
usePluginTerminalSessionLifecycle,
|
||||
@@ -260,8 +261,6 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
fontSize,
|
||||
terminalTheme,
|
||||
followAppTerminalTheme = false,
|
||||
accentMode = "theme",
|
||||
customAccent = "",
|
||||
terminalSettings,
|
||||
sessionId,
|
||||
workspaceId,
|
||||
@@ -1264,6 +1263,8 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
});
|
||||
}, [availableFonts, fontFamilyId, hasFontFamilyOverride, host.fontFamily, terminalSettings?.fallbackFont]);
|
||||
|
||||
const { accentMode, customAccent } = useAppearanceChromeStore();
|
||||
|
||||
const baseEffectiveTheme = useMemo(() => {
|
||||
if (appearanceTheme) return appearanceTheme;
|
||||
if (followAppTerminalTheme) {
|
||||
|
||||
@@ -217,6 +217,32 @@ test("TerminalLayer ignores shellHistory prop churn (reads shellHistoryStore ins
|
||||
);
|
||||
});
|
||||
|
||||
test("TerminalLayer ignores notes prop churn (reads notesStore instead)", () => {
|
||||
assert.equal(
|
||||
terminalLayerAreEqual(
|
||||
baseProps as never,
|
||||
{
|
||||
...baseProps,
|
||||
notes: [{ id: "n1", title: "One", content: "body", tags: [], createdAt: 1, updatedAt: 1, order: 1000 }],
|
||||
noteGroups: ["Ops"],
|
||||
updateNotes: () => {},
|
||||
updateNoteGroups: () => {},
|
||||
} as never,
|
||||
),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("TerminalLayer ignores customAccent prop churn (reads appearanceChromeStore instead)", () => {
|
||||
assert.equal(
|
||||
terminalLayerAreEqual(
|
||||
baseProps as never,
|
||||
{ ...baseProps, accentMode: "custom", customAccent: "#ff00aa" } as never,
|
||||
),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("TerminalLayer ignores sessions dynamicTitle-only updates", () => {
|
||||
const sessionsA = [{
|
||||
id: "s1",
|
||||
|
||||
@@ -173,8 +173,6 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
|
||||
identities,
|
||||
snippets,
|
||||
snippetPackages,
|
||||
notes,
|
||||
noteGroups,
|
||||
openNoteRequest,
|
||||
onOpenVaultNoteFromChat,
|
||||
onOpenVaultHostFromChat,
|
||||
@@ -237,8 +235,6 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
|
||||
updateHosts,
|
||||
updateSnippets,
|
||||
updateSnippetPackages,
|
||||
updateNotes,
|
||||
updateNoteGroups,
|
||||
sftpDefaultViewMode,
|
||||
sftpDoubleClickBehavior,
|
||||
sftpAutoSync,
|
||||
@@ -2165,8 +2161,6 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
|
||||
programmaticCommandLogRewriteHandlersRef,
|
||||
snippetPackages,
|
||||
snippets,
|
||||
noteGroups,
|
||||
notes,
|
||||
onOpenVaultHostFromChat,
|
||||
onOpenVaultNoteFromChat: handleOpenVaultNoteFromAiPanel,
|
||||
onOpenVaultSectionFromChat,
|
||||
@@ -2189,8 +2183,6 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
updateHosts,
|
||||
updateNoteGroups,
|
||||
updateNotes,
|
||||
updateSnippetPackages,
|
||||
updateSnippets,
|
||||
X,
|
||||
|
||||
@@ -31,8 +31,7 @@ export const terminalPropsAreEqual = (
|
||||
&& prev.fontFamilyId === next.fontFamilyId
|
||||
&& prev.fontSize === next.fontSize
|
||||
&& prev.followAppTerminalTheme === next.followAppTerminalTheme
|
||||
&& prev.accentMode === next.accentMode
|
||||
&& prev.customAccent === next.customAccent
|
||||
// accentMode / customAccent intentionally omitted — Terminal reads appearanceChromeStore.
|
||||
&& prev.terminalSettings === next.terminalSettings
|
||||
&& prev.sessionId === next.sessionId
|
||||
&& prev.restoreState === next.restoreState
|
||||
|
||||
@@ -511,8 +511,6 @@ export interface TerminalLayerProps {
|
||||
identities: Identity[];
|
||||
snippets: Snippet[];
|
||||
snippetPackages: string[];
|
||||
notes: VaultNote[];
|
||||
noteGroups: string[];
|
||||
openNoteRequest?: { tabId: string; noteId: string; requestId: number } | null;
|
||||
onOpenVaultNoteFromChat?: (noteId: string) => void;
|
||||
onOpenVaultHostFromChat?: (hostId: string) => void;
|
||||
@@ -581,8 +579,6 @@ export interface TerminalLayerProps {
|
||||
updateHosts: (hosts: Host[]) => void;
|
||||
updateSnippets?: (snippets: Snippet[]) => void;
|
||||
updateSnippetPackages?: (packages: string[]) => void;
|
||||
updateNotes: (notes: VaultNote[]) => void;
|
||||
updateNoteGroups: (groups: string[]) => void;
|
||||
sftpDefaultViewMode: 'list' | 'tree';
|
||||
sftpDoubleClickBehavior: 'open' | 'transfer';
|
||||
sftpAutoSync: boolean;
|
||||
@@ -761,8 +757,7 @@ const terminalPanePropsAreEqual = (
|
||||
prev.fontSize === next.fontSize &&
|
||||
prev.terminalTheme === next.terminalTheme &&
|
||||
prev.followAppTerminalTheme === next.followAppTerminalTheme &&
|
||||
prev.accentMode === next.accentMode &&
|
||||
prev.customAccent === next.customAccent &&
|
||||
// accentMode / customAccent intentionally omitted — Terminal reads appearanceChromeStore.
|
||||
prev.terminalSettings === next.terminalSettings &&
|
||||
prev.hotkeyScheme === next.hotkeyScheme &&
|
||||
prev.disableTerminalFontZoom === next.disableTerminalFontZoom &&
|
||||
@@ -1534,8 +1529,7 @@ const terminalPanesHostPropsAreEqual = (
|
||||
if (prev.fontSize !== next.fontSize) return false;
|
||||
if (prev.terminalTheme !== next.terminalTheme) return false;
|
||||
if (prev.followAppTerminalTheme !== next.followAppTerminalTheme) return false;
|
||||
if (prev.accentMode !== next.accentMode) return false;
|
||||
if (prev.customAccent !== next.customAccent) return false;
|
||||
// accentMode / customAccent intentionally omitted — Terminal reads appearanceChromeStore.
|
||||
if (prev.terminalSettings !== next.terminalSettings) return false;
|
||||
if (prev.hotkeyScheme !== next.hotkeyScheme) return false;
|
||||
if (prev.disableTerminalFontZoom !== next.disableTerminalFontZoom) return false;
|
||||
|
||||
@@ -523,8 +523,6 @@ export function TerminalLayerTabBridge({ stableRef }: { stableRef: StableRef })
|
||||
notesMountedTabIds: s.notesMountedTabIds,
|
||||
notesOpenNoteByTab: s.notesOpenNoteByTab,
|
||||
NotesManager: s.NotesManager,
|
||||
noteGroups: s.noteGroups,
|
||||
notes: s.notes,
|
||||
scriptsMountedTabIds: s.scriptsMountedTabIds,
|
||||
systemMountedTabIds: s.systemMountedTabIds,
|
||||
themeMountedTabIds: s.themeMountedTabIds,
|
||||
@@ -595,8 +593,6 @@ export function TerminalLayerTabBridge({ stableRef }: { stableRef: StableRef })
|
||||
snippets: s.snippets,
|
||||
updateSnippetPackages: s.updateSnippetPackages,
|
||||
updateSnippets: s.updateSnippets,
|
||||
updateNoteGroups: s.updateNoteGroups,
|
||||
updateNotes: s.updateNotes,
|
||||
splitHorizontalHandlersRef: s.splitHorizontalHandlersRef,
|
||||
splitVerticalHandlersRef: s.splitVerticalHandlersRef,
|
||||
sshDebugLogsEnabled: s.sshDebugLogsEnabled,
|
||||
@@ -641,8 +637,6 @@ export function TerminalLayerTabBridge({ stableRef }: { stableRef: StableRef })
|
||||
focusedHost,
|
||||
focusedSessionId,
|
||||
s.restoreTerminalCwd,
|
||||
s.notes,
|
||||
s.noteGroups,
|
||||
handleWorkspaceDrop,
|
||||
handleTerminalContextReaderChange,
|
||||
hibernateHiddenTabs,
|
||||
|
||||
@@ -8,6 +8,13 @@ import {
|
||||
getSidePanelLiveSnapshot,
|
||||
subscribeSidePanelLiveSnapshot,
|
||||
} from '../../application/state/sidePanelLiveStore';
|
||||
import {
|
||||
getEmptyNotesSnapshot,
|
||||
getNotesSnapshot,
|
||||
subscribeNotes,
|
||||
subscribeNotesNoop,
|
||||
useNotesStore,
|
||||
} from '../../application/state/notesStore';
|
||||
import {
|
||||
getShellHistorySnapshot,
|
||||
subscribeShellHistory,
|
||||
@@ -399,14 +406,16 @@ function SidePanelNotesSlotInner({
|
||||
isVisible: boolean;
|
||||
}) {
|
||||
const openNoteRequest = (ctx.notesOpenNoteByTab as Map<string, { noteId: string; requestId: number }>).get(tabId) ?? null;
|
||||
const {
|
||||
notes,
|
||||
noteGroups,
|
||||
updateNotes,
|
||||
updateNoteGroups,
|
||||
} = useNotesStore();
|
||||
|
||||
const {
|
||||
NotesManager,
|
||||
notes,
|
||||
noteGroups,
|
||||
hosts,
|
||||
updateNotes,
|
||||
updateNoteGroups,
|
||||
handleOpenHostFromNotes,
|
||||
} = ctx;
|
||||
|
||||
@@ -499,7 +508,6 @@ function SidePanelAiSlotInner({
|
||||
resolveAIExecutorContext,
|
||||
pendingTerminalSelectionForAI,
|
||||
handlePendingTerminalSelectionConsumed,
|
||||
notes,
|
||||
hosts,
|
||||
snippets,
|
||||
onOpenVaultNoteFromChat,
|
||||
@@ -509,6 +517,14 @@ function SidePanelAiSlotInner({
|
||||
validAIScopeTargetIds,
|
||||
} = ctx;
|
||||
|
||||
// Gate notes subscription while AI is hidden so note edits do not re-render
|
||||
// retained AI mounts (panel still mounts for fast reopen).
|
||||
const notesSnapshot = useSyncExternalStore(
|
||||
isVisible ? subscribeNotes : subscribeNotesNoop,
|
||||
isVisible ? getNotesSnapshot : getEmptyNotesSnapshot,
|
||||
isVisible ? getNotesSnapshot : getEmptyNotesSnapshot,
|
||||
);
|
||||
|
||||
if (mountedAiTabIds.length === 0) return null;
|
||||
const activeLayout = activeTabId
|
||||
? (ctx.sidePanelLayouts as Map<string, SidePanelLayout>).get(activeTabId)
|
||||
@@ -522,6 +538,9 @@ function SidePanelAiSlotInner({
|
||||
// Only the visible AI panel needs vault catalogs for artifact navigation.
|
||||
// Hidden retained panels keep session state without re-binding huge hosts/notes.
|
||||
const injectVaultCatalog = isVisible;
|
||||
const notes = injectVaultCatalog
|
||||
? (notesSnapshot.notes as import('../../domain/models').VaultNote[])
|
||||
: EMPTY_VAULT_NOTES;
|
||||
|
||||
return (
|
||||
<AISidePanelStateRoot validAIScopeTargetIds={validAIScopeTargetIds}>
|
||||
@@ -533,7 +552,7 @@ function SidePanelAiSlotInner({
|
||||
resolveExecutorContext={resolveAIExecutorContext}
|
||||
pendingTerminalSelection={pendingTerminalSelectionForAI}
|
||||
onPendingTerminalSelectionConsumed={handlePendingTerminalSelectionConsumed}
|
||||
notes={injectVaultCatalog ? notes : EMPTY_VAULT_NOTES}
|
||||
notes={notes}
|
||||
hosts={injectVaultCatalog ? hosts : EMPTY_VAULT_HOSTS}
|
||||
snippets={injectVaultCatalog ? snippets : EMPTY_VAULT_SNIPPETS}
|
||||
onOpenVaultNoteFromChat={onOpenVaultNoteFromChat}
|
||||
|
||||
@@ -320,10 +320,7 @@ const SIDE_PANEL_STABLE_CTX_KEYS = [
|
||||
'validAIScopeTargetIds',
|
||||
'AISidePanelStateRoot',
|
||||
'NotesManager',
|
||||
'notes',
|
||||
'noteGroups',
|
||||
'updateNotes',
|
||||
'updateNoteGroups',
|
||||
// notes / noteGroups / updateNotes / updateNoteGroups come from notesStore.
|
||||
'onOpenVaultNoteFromChat',
|
||||
'onOpenVaultHostFromChat',
|
||||
'onOpenVaultSectionFromChat',
|
||||
@@ -362,8 +359,7 @@ const WORKSPACE_CTX_KEYS = [
|
||||
'fontSize',
|
||||
'terminalTheme',
|
||||
'followAppTerminalTheme',
|
||||
'accentMode',
|
||||
'customAccent',
|
||||
// accentMode / customAccent come from appearanceChromeStore (Terminal leaf).
|
||||
'terminalSettings',
|
||||
'hotkeyScheme',
|
||||
'disableTerminalFontZoom',
|
||||
|
||||
@@ -12,8 +12,8 @@ export const terminalLayerAreEqual = (
|
||||
prev.keys === next.keys &&
|
||||
prev.snippets === next.snippets &&
|
||||
prev.snippetPackages === next.snippetPackages &&
|
||||
prev.notes === next.notes &&
|
||||
prev.noteGroups === next.noteGroups &&
|
||||
// notes / noteGroups / updateNotes / updateNoteGroups intentionally omitted —
|
||||
// Notes + AI panels read notesStore.
|
||||
prev.openNoteRequest === next.openNoteRequest &&
|
||||
prev.onOpenVaultNoteFromChat === next.onOpenVaultNoteFromChat &&
|
||||
prev.onOpenVaultHostFromChat === next.onOpenVaultHostFromChat &&
|
||||
@@ -32,8 +32,8 @@ export const terminalLayerAreEqual = (
|
||||
prev.followAppTerminalTheme === next.followAppTerminalTheme &&
|
||||
prev.pickTerminalTheme === next.pickTerminalTheme &&
|
||||
prev.resolveSessionAppearance === next.resolveSessionAppearance &&
|
||||
prev.accentMode === next.accentMode &&
|
||||
prev.customAccent === next.customAccent &&
|
||||
// accentMode / customAccent intentionally omitted — Terminal reads
|
||||
// appearanceChromeStore so accent drag does not rebuild TerminalLayer.
|
||||
prev.terminalSettings === next.terminalSettings &&
|
||||
prev.fontSize === next.fontSize &&
|
||||
prev.terminalFontFamilyId === next.terminalFontFamilyId &&
|
||||
@@ -75,8 +75,6 @@ export const terminalLayerAreEqual = (
|
||||
prev.onToggleBroadcast === next.onToggleBroadcast &&
|
||||
prev.updateSnippets === next.updateSnippets &&
|
||||
prev.updateSnippetPackages === next.updateSnippetPackages &&
|
||||
prev.updateNotes === next.updateNotes &&
|
||||
prev.updateNoteGroups === next.updateNoteGroups &&
|
||||
prev.toggleScriptsSidePanelRef === next.toggleScriptsSidePanelRef &&
|
||||
prev.toggleSidePanelRef === next.toggleSidePanelRef &&
|
||||
prev.identities === next.identities
|
||||
|
||||
Reference in new issue
Block a user