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
161 lines
5.1 KiB
TypeScript
161 lines
5.1 KiB
TypeScript
import { useEffect, useMemo } from 'react';
|
|
|
|
import {
|
|
fromEditorTabId,
|
|
isEditorTabId,
|
|
useActiveTabId,
|
|
} from '../state/activeTabStore';
|
|
import { updateActiveChromeThemeDeps } from '../state/activeChromeThemeSync';
|
|
import { useActiveChromeTheme } from '../state/useActiveChromeTheme';
|
|
import { useAppearanceChromeStore } from '../state/appearanceChromeStore';
|
|
import { netcattyBridge } from '../../infrastructure/services/netcattyBridge';
|
|
import { resolveActiveChromeTheme } from './activeChromeTheme';
|
|
import type { TerminalAppearanceHostScope, ResolvedAppearance } from '../../domain/terminalAppearanceRuntime';
|
|
import type {
|
|
Host,
|
|
TerminalSession,
|
|
TerminalTheme,
|
|
Workspace,
|
|
} from '../../types';
|
|
import type { LogView } from '../state/logViewState';
|
|
import type { EditorTabChrome } from '../state/editorTabStore';
|
|
|
|
export interface AppActiveTabChromeProps {
|
|
showSftpTab: boolean;
|
|
setActiveTabId: (id: string) => void;
|
|
applyAppTheme: () => void;
|
|
hostById: Map<string, Host>;
|
|
sessionById: Map<string, TerminalSession>;
|
|
themeById: Map<string, TerminalTheme>;
|
|
workspaceById: Map<string, Workspace>;
|
|
currentTerminalTheme: TerminalTheme;
|
|
followAppTerminalTheme: boolean;
|
|
editorTabs: readonly EditorTabChrome[];
|
|
logViews: readonly LogView[];
|
|
resolveSessionAppearance?: (hostScope: TerminalAppearanceHostScope) => ResolvedAppearance;
|
|
t: (key: string) => string;
|
|
}
|
|
|
|
/**
|
|
* Owns the `activeTabId` subscription and the purely side-effectful "chrome"
|
|
* work derived from it: window title and the SFTP-tab guard.
|
|
* Extracted out of <App> so that switching top tabs only
|
|
* re-renders this null-rendering component (and the self-subscribing leaves)
|
|
* instead of forcing the entire App tree (which holds all vault/session/
|
|
* settings state and rebuilds the giant AppView ctx) to re-render.
|
|
*
|
|
* Accent comes from appearanceChromeStore so color-picker drag does not
|
|
* rebuild AppShell chrome props.
|
|
*/
|
|
export function AppActiveTabChrome({
|
|
showSftpTab,
|
|
setActiveTabId,
|
|
applyAppTheme,
|
|
hostById,
|
|
sessionById,
|
|
themeById,
|
|
workspaceById,
|
|
currentTerminalTheme,
|
|
followAppTerminalTheme,
|
|
editorTabs,
|
|
logViews,
|
|
resolveSessionAppearance,
|
|
t,
|
|
}: AppActiveTabChromeProps) {
|
|
const activeTabId = useActiveTabId();
|
|
const { accentMode, customAccent } = useAppearanceChromeStore();
|
|
|
|
useEffect(() => {
|
|
if (!showSftpTab && activeTabId === 'sftp') {
|
|
setActiveTabId('vault');
|
|
}
|
|
}, [showSftpTab, activeTabId, setActiveTabId]);
|
|
|
|
const chromeThemeDeps = useMemo(() => ({
|
|
accentMode,
|
|
applyAppTheme,
|
|
currentTerminalTheme,
|
|
customAccent,
|
|
editorTabs,
|
|
followAppTerminalTheme,
|
|
hostById,
|
|
logViews,
|
|
resolveSessionAppearance,
|
|
sessionById,
|
|
themeById,
|
|
workspaceById,
|
|
}), [
|
|
accentMode,
|
|
applyAppTheme,
|
|
currentTerminalTheme,
|
|
customAccent,
|
|
editorTabs,
|
|
followAppTerminalTheme,
|
|
hostById,
|
|
logViews,
|
|
resolveSessionAppearance,
|
|
sessionById,
|
|
themeById,
|
|
workspaceById,
|
|
]);
|
|
|
|
updateActiveChromeThemeDeps(chromeThemeDeps);
|
|
|
|
const activeChromeTheme = useMemo(() => resolveActiveChromeTheme({
|
|
...chromeThemeDeps,
|
|
activeTabId,
|
|
}), [chromeThemeDeps, activeTabId]);
|
|
|
|
useActiveChromeTheme({
|
|
activeTheme: activeChromeTheme,
|
|
applyAppTheme,
|
|
});
|
|
|
|
const editorTabFileNameCounts = useMemo(() => {
|
|
const counts = new Map<string, number>();
|
|
for (const tab of editorTabs) counts.set(tab.fileName, (counts.get(tab.fileName) ?? 0) + 1);
|
|
return counts;
|
|
}, [editorTabs]);
|
|
|
|
const activeWindowTitle = useMemo(() => {
|
|
if (activeTabId === 'vault') return 'Vaults';
|
|
if (activeTabId === 'sftp') return 'SFTP';
|
|
if (isEditorTabId(activeTabId)) {
|
|
const editorTab = editorTabs.find((tab) => tab.id === fromEditorTabId(activeTabId));
|
|
if (!editorTab) return 'Editor';
|
|
const suffix = (editorTabFileNameCounts.get(editorTab.fileName) ?? 0) > 1
|
|
? ` · ${editorTab.remotePath.split('/').slice(-2, -1)[0] || '/'}`
|
|
: '';
|
|
return `${editorTab.fileName}${suffix}`;
|
|
}
|
|
const workspace = workspaceById.get(activeTabId);
|
|
if (workspace) return workspace.title;
|
|
const session = sessionById.get(activeTabId);
|
|
if (session) return session.hostLabel;
|
|
const logView = logViews.find((item) => item.id === activeTabId);
|
|
if (logView) {
|
|
const isLocal = logView.log.protocol === 'local' || logView.log.hostname === 'localhost';
|
|
return `${t('tabs.logPrefix')} ${isLocal ? t('tabs.logLocal') : logView.log.hostname}`;
|
|
}
|
|
return 'Netcatty';
|
|
}, [activeTabId, editorTabFileNameCounts, editorTabs, logViews, sessionById, t, workspaceById]);
|
|
|
|
useEffect(() => {
|
|
// Title is already memoized by activeTabId; skip redundant IPC when the
|
|
// string did not change (e.g. two tabs sharing the same host label).
|
|
let cancelled = false;
|
|
const bridge = netcattyBridge.get();
|
|
if (!bridge?.setWindowTitle) return;
|
|
// Defer slightly so the title write does not compete with tab-switch paint.
|
|
const timer = window.setTimeout(() => {
|
|
if (!cancelled) void bridge.setWindowTitle?.(activeWindowTitle);
|
|
}, 0);
|
|
return () => {
|
|
cancelled = true;
|
|
window.clearTimeout(timer);
|
|
};
|
|
}, [activeWindowTitle]);
|
|
|
|
return null;
|
|
}
|