feat(send-command-panel): add support for "allWindows" target in session selection

This commit is contained in:
Kang committed 2026-08-19 00:10:15 +08:00
1 parent 147b0f7a51
commit 9ff4685c4e
4 files changed
+142 -44

No files matched your search

+53 -35
View File
@@ -3403,45 +3403,62 @@ function App() {
[tabsById, terminalWindows],
);
const sendCommandSessionTargets = useMemo(() => {
if (!terminalWindows) return [];
const targets: {
id: string;
name: string;
tabName: string;
type: SessionType;
}[] = [];
const seen = new Set<string>();
const visit = (node: TerminalWindowNode) => {
if (node.kind === "split") {
visit(node.first);
visit(node.second);
return;
const currentWindowLabel = getOwnerMainWindowLabel();
const targetsById = new Map<
string,
{
id: string;
name: string;
tabName: string;
type: SessionType;
ownerWindowLabel?: string | null;
}
>();
for (const tabId of node.tabIds) {
const tab = tabsById.get(tabId);
if (!tab) continue;
for (const pane of collectSessionPanes(tab.root)) {
if (pane.paneKind !== "terminal" || !hasLiveSession(pane) || seen.has(pane.sessionId)) {
continue;
}
seen.add(pane.sessionId);
targets.push({
id: pane.sessionId,
name: pane.name,
tabName: getTabDisplayName(tab),
type: pane.type,
});
if (terminalWindows) {
const visit = (node: TerminalWindowNode) => {
if (node.kind === "split") {
visit(node.first);
visit(node.second);
return;
}
}
};
visit(terminalWindows);
return targets;
}, [tabsById, terminalWindows]);
for (const tabId of node.tabIds) {
const tab = tabsById.get(tabId);
if (!tab) continue;
for (const pane of collectSessionPanes(tab.root)) {
if (pane.paneKind !== "terminal" || !hasLiveSession(pane)) {
continue;
}
targetsById.set(pane.sessionId, {
id: pane.sessionId,
name: pane.name,
tabName: getTabDisplayName(tab),
type: pane.type,
ownerWindowLabel: currentWindowLabel,
});
}
}
};
visit(terminalWindows);
}
for (const session of liveSessionsById?.values() ?? []) {
if (!["SSH", "Local", "Telnet", "Serial"].includes(session.session_type)) continue;
if (targetsById.has(session.id)) continue;
targetsById.set(session.id, {
id: session.id,
name: session.name,
tabName: session.name,
type: session.session_type as SessionType,
ownerWindowLabel: session.owner_window_label ?? null,
});
}
return [...targetsById.values()];
}, [liveSessionsById, tabsById, terminalWindows]);
const activeBottomPanel = uiConfig.show_serial_send_panel
? "serialSend"
@@ -3833,6 +3850,7 @@ function App() {
activeNonSerialSessionId,
activeNonSerialSessionIds,
syncGroups,
currentWindowLabel: getOwnerMainWindowLabel(),
sessionTargets: sendCommandSessionTargets,
sendCommandDraft,
onSendCommandDraftConsumed: handleSendCommandDraftConsumed,
+3
View File
@@ -110,11 +110,13 @@ interface AppLayoutProps {
activeNonSerialSessionId: string | null;
activeNonSerialSessionIds: string[];
syncGroups: SyncGroup[];
currentWindowLabel: string;
sessionTargets: {
id: string;
name: string;
tabName: string;
type: SessionType;
ownerWindowLabel?: string | null;
}[];
sendCommandDraft: SendCommandPanelDraft | null;
onSendCommandDraftConsumed: () => void;
@@ -470,6 +472,7 @@ export default function AppLayout({
currentShellSessionId={bottomPanel.activeNonSerialSessionId}
shellSessionIds={bottomPanel.activeNonSerialSessionIds}
syncGroups={bottomPanel.syncGroups}
currentWindowLabel={bottomPanel.currentWindowLabel}
sessionTargets={bottomPanel.sessionTargets}
draft={bottomPanel.sendCommandDraft}
onDraftConsumed={bottomPanel.onSendCommandDraftConsumed}
+85 -8
View File
@@ -1,5 +1,6 @@
import {
type ChangeEvent,
Fragment,
type KeyboardEvent,
type ReactNode,
type UIEvent,
@@ -17,6 +18,8 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
@@ -49,6 +52,7 @@ interface SendCommandPanelProps {
currentShellSessionId: string | null;
shellSessionIds: string[];
syncGroups: SyncGroup[];
currentWindowLabel: string;
sessionTargets: SendCommandSessionTarget[];
draft?: SendCommandPanelDraft | null;
onDraftConsumed?: () => void;
@@ -71,6 +75,7 @@ interface SendCommandSessionTarget {
name: string;
tabName: string;
type: SessionType;
ownerWindowLabel?: string | null;
}
interface SendUnit {
@@ -261,6 +266,7 @@ export default function SendCommandPanel({
currentShellSessionId,
shellSessionIds,
syncGroups,
currentWindowLabel,
sessionTargets,
draft,
onDraftConsumed,
@@ -331,6 +337,47 @@ export default function SendCommandPanel({
[sessionTargetOptions],
);
const allWindowSessionIds = useMemo(
() => sessionTargetOptions.map((option) => option.session.id),
[sessionTargetOptions],
);
const sessionTargetGroups = useMemo(() => {
const groups = new Map<
string,
{
key: string;
ownerWindowLabel: string | null;
label: string;
options: typeof sessionTargetOptions;
}
>();
for (const option of sessionTargetOptions) {
const ownerWindowLabel = option.session.ownerWindowLabel ?? null;
const isCurrentWindow = ownerWindowLabel === currentWindowLabel;
const key = isCurrentWindow ? "__current__" : (ownerWindowLabel ?? "__unknown__");
const label = isCurrentWindow
? t("serialSend.currentWindowSessions", "Current window sessions")
: t("serialSend.windowGroup", "Window: {{label}}", {
label: ownerWindowLabel ?? t("serialSend.unknownWindow", "Unknown window"),
});
const group = groups.get(key);
if (group) {
group.options.push(option);
} else {
groups.set(key, { key, ownerWindowLabel, label, options: [option] });
}
}
return [...groups.values()].sort((a, b) => {
if (a.ownerWindowLabel === currentWindowLabel) return -1;
if (b.ownerWindowLabel === currentWindowLabel) return 1;
return a.label.localeCompare(b.label);
});
}, [currentWindowLabel, sessionTargetOptions, t]);
const groupTargetOptions = useMemo(() => {
return syncGroups
.filter((group) => group.enabled)
@@ -388,6 +435,10 @@ export default function SendCommandPanel({
return session ? getSessionTargetLabel(session) : t("serialSend.targetSessions", "Sessions");
}
if (target === "allWindows") {
return t("serialSend.allWindowSessions", "All window sessions");
}
return target === "all"
? t("serialSend.allSessions", "All sessions")
: t("serialSend.currentSession", "Current session");
@@ -402,6 +453,10 @@ export default function SendCommandPanel({
return sessionTargetByValue.has(target) ? [getSessionTargetId(target)] : [];
}
if (target === "allWindows") {
return allWindowSessionIds;
}
if (targetKind === "serial") {
return serialSessionId ? [serialSessionId] : [];
}
@@ -412,6 +467,7 @@ export default function SendCommandPanel({
: []
: shellSessionIds;
}, [
allWindowSessionIds,
currentTargetSessionId,
groupTargetByValue,
serialSessionId,
@@ -460,6 +516,11 @@ export default function SendCommandPanel({
return;
}
if (target === "allWindows" && allWindowSessionIds.length === 0) {
setTarget(fallbackTarget);
return;
}
if (targetKind === "serial" && target === "all") {
setTarget("current");
return;
@@ -471,6 +532,7 @@ export default function SendCommandPanel({
setTarget("all");
}
}, [
allWindowSessionIds.length,
currentTargetSessionId,
groupTargetByValue,
sessionTargetByValue,
@@ -906,6 +968,13 @@ export default function SendCommandPanel({
{t("serialSend.allSessions", "All sessions")}
</TargetMenuItem>
)}
<TargetMenuItem
checked={target === "allWindows"}
disabled={allWindowSessionIds.length === 0}
onSelect={() => setTarget("allWindows")}
>
{t("serialSend.allWindowSessions", "All window sessions")}
</TargetMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger
disabled={sessionTargetOptions.length === 0}
@@ -914,14 +983,22 @@ export default function SendCommandPanel({
<span className="truncate">{t("serialSend.targetSessions", "Sessions")}</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="max-h-[70vh] min-w-[14rem] max-w-[22rem] overflow-y-auto">
{sessionTargetOptions.map((option) => (
<TargetMenuItem
key={option.session.id}
checked={target === option.value}
onSelect={() => setTarget(option.value)}
>
{getSessionTargetLabel(option.session)}
</TargetMenuItem>
{sessionTargetGroups.map((group, groupIndex) => (
<Fragment key={group.key}>
{groupIndex > 0 && <DropdownMenuSeparator />}
<DropdownMenuLabel className="max-w-[20rem] truncate text-[0.625rem] text-muted-foreground">
{group.label}
</DropdownMenuLabel>
{group.options.map((option) => (
<TargetMenuItem
key={option.session.id}
checked={target === option.value}
onSelect={() => setTarget(option.value)}
>
{getSessionTargetLabel(option.session)}
</TargetMenuItem>
))}
</Fragment>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
+1 -1
View File
@@ -4,7 +4,7 @@ export const OPEN_SEND_COMMAND_PANEL_EVENT = "nyaterm:open-send-command-panel";
export type SendCommandDataType = "text" | "hex";
export type SendCommandMode = "line" | "character" | "byte" | "packet";
export type SendCommandTarget = "current" | "all";
export type SendCommandTarget = "current" | "all" | "allWindows";
export type SendCommandCount = number | null;
export interface SendCommandPanelDraft {