From f09bed06b55409fa81df7d392305706e7a80ae74 Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 14 Sep 2026 20:47:21 +0800 Subject: [PATCH] remove: drop the quick-input panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The toggle state was a global singleton while the bolt button renders in every pane's tab bar, so all panes showed the same open/close state and the single floating panel (window bottom-right, sends to the *active* session) never corresponded to the pane whose button was clicked. A true per-pane rework is a medium refactor for a feature that overlaps with the inline completion and the sidebar commands panel — removing instead. --- docs/ROADMAP.md | 2 +- .../src/workspace/QuickInputPanel.tsx | 173 ------------------ src/renderer/src/workspace/Workspace.tsx | 38 +--- src/renderer/src/workspace/workspace.css | 143 +-------------- 4 files changed, 4 insertions(+), 352 deletions(-) delete mode 100644 src/renderer/src/workspace/QuickInputPanel.tsx diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 24d5776..57aff84 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -73,7 +73,7 @@ ## M6 — 多终端协同 ✅ - [x] 广播输入(broadcastStore):选中 ≥2 个会话同步键入;标签广播标记 + 按钮计数徽标;目标 <2 自动关闭 -- [x] 快捷输入面板(Quick Input):右下角浮层 = 命令输入 + 自动执行(\r) + 快捷命令 + 最近命令;广播感知发送 +- [x] ~~快捷输入面板(Quick Input)~~(已移除:全局单例开关与面板不对一一对应,功能与行内补全/侧边栏命令面板重叠) - [x] ZMODEM(rz/sz):主进程引擎(zmodem.js,仅 SSH 会话;本地 ConPTY 不支持二进制),文件选择/保存目录对话框 + 传输面板进度 + 看门狗超时;双向 e2e(tests/zmodem-e2e.mjs) - [x] 快捷键系统:Ctrl+=/-/0 字号(持久化)、Ctrl+PgUp/PgDn 切换终端、全局唤起/隐藏(设置页可配 globalShowHide) - 验收:3 个分屏同步执行命令 ✅(活体验证:终端 3 键入扇出到终端 4);rz 传输 ✅(e2e 双向内容一致;真实服务器 rz/sz 待用户复测) diff --git a/src/renderer/src/workspace/QuickInputPanel.tsx b/src/renderer/src/workspace/QuickInputPanel.tsx deleted file mode 100644 index 6967db2..0000000 --- a/src/renderer/src/workspace/QuickInputPanel.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from 'react' -import { App, Input } from 'antd' -import { create } from 'zustand' -import type { CommandItem } from '@shared/commands' -import { writeBroadcast } from './broadcastStore' - -/** Open state shared between the tab-bar toggle button and the floating panel. */ -export const useQuickInputStore = create<{ - open: boolean - toggle: () => void - setOpen: (open: boolean) => void -}>((set) => ({ - open: window.localStorage.getItem('ot.quickInputOpen') === '1', - toggle: () => - set((state) => { - const next = !state.open - window.localStorage.setItem('ot.quickInputOpen', next ? '1' : '0') - return { open: next } - }), - setOpen: (open) => { - window.localStorage.setItem('ot.quickInputOpen', open ? '1' : '0') - set({ open }) - } -})) - -/** - * M6 quick-input panel (bottom-right floating). Three sections: - * 1. free-text command line (with auto-execute(\r) toggle) - * 2. library commands (api.listLibrary()) - * 3. recent history (api.listHistory(), top 10) - * - * Sends target = the active session via `onResolveActiveSession`; when broadcast - * is enabled and the active session is a target, the send routes through the - * broadcast fan-out so it reaches every target. Lists refresh on open. - */ -export interface QuickInputPanelProps { - /** resolve the currently active terminal session id ('' when none) */ - onResolveActiveSession: () => string -} - -export function QuickInputPanel({ onResolveActiveSession }: QuickInputPanelProps): React.JSX.Element | null { - const { message } = App.useApp() - const open = useQuickInputStore((s) => s.open) - const close = useQuickInputStore((s) => s.setOpen) - // auto-execute(\r); persisted across sessions. - const [autoEnter, setAutoEnter] = useState(() => window.localStorage.getItem('ot.quickInputEnter') !== '0') - const [value, setValue] = useState('') - const [history, setHistory] = useState([]) - const [library, setLibrary] = useState([]) - /** tracks the previous `open` so lists refresh on the false→true transition */ - const prevOpenRef = useRef(open) - - // Refresh lists every time the panel opens (and stay fresh if it was open). - const refresh = useCallback(async (): Promise => { - try { - const [h, l] = await Promise.all([window.api.listHistory(), window.api.listLibrary()]) - setHistory(h.slice(0, 10)) - setLibrary(l) - } catch { - setHistory([]) - setLibrary([]) - } - }, []) - - useEffect(() => { - if (open && !prevOpenRef.current) void refresh() - prevOpenRef.current = open - }, [open, refresh]) - - const send = useCallback( - (text: string) => { - const target = onResolveActiveSession() - if (!target) { - void message.info('没有激活的终端会话') - return - } - const data = autoEnter ? `${text}\r` : text - writeBroadcast(target, data) - // keep "最近命令" fresh immediately after a send - void window.api - .listHistory() - .then((h: CommandItem[]) => setHistory(h.slice(0, 10))) - .catch(() => undefined) - setValue('') - }, - [autoEnter, onResolveActiveSession] - ) - - const onEnter = useCallback((): void => { - const t = value.trim() - if (t) send(t) - }, [value, send]) - - const onToggleAutoEnter = useCallback((checked: boolean): void => { - setAutoEnter(checked) - window.localStorage.setItem('ot.quickInputEnter', checked ? '1' : '0') - }, []) - - if (!open) return null - - return ( -
-
- 快捷输入 - -
-
- setValue(e.target.value)} - onPressEnter={onEnter} - /> - -
- -
-
快捷命令
- {library.length === 0 ? ( -
暂无快捷命令
- ) : ( -
- {library.map((item) => ( - - ))} -
- )} -
- -
-
最近命令
- {history.length === 0 ? ( -
暂无历史记录
- ) : ( -
- {history.map((item) => ( - - ))} -
- )} -
-
- ) -} \ No newline at end of file diff --git a/src/renderer/src/workspace/Workspace.tsx b/src/renderer/src/workspace/Workspace.tsx index 85f83f0..5f0a65e 100644 --- a/src/renderer/src/workspace/Workspace.tsx +++ b/src/renderer/src/workspace/Workspace.tsx @@ -10,8 +10,7 @@ import { MenuUnfoldOutlined, PlusOutlined, SettingOutlined, - ShareAltOutlined, - ThunderboltOutlined + ShareAltOutlined } from '@ant-design/icons' import { DockviewReact } from 'dockview-react' import type { @@ -33,7 +32,6 @@ import { HostKeyModal } from './HostKeyModal' import { TerminalPanel } from './TerminalPanel' import { ApplyTemplateModal, SaveTemplateModal, TerminalTab } from './TerminalTab' import { useBroadcastStore, writeBroadcast } from './broadcastStore' -import { QuickInputPanel, useQuickInputStore } from './QuickInputPanel' import { forgetSessionCwd, getSessionCwd, onSessionCwdChange, setSessionCwd } from './sessionCwdStore' import { useSettingsStore } from '@renderer/settings/store' import type { SessionSnapshot, SessionPanelState } from '@shared/ipc' @@ -201,7 +199,6 @@ function createTabActions(mode: WorkspaceMode): (props: IDockviewHeaderActionsPr
-
) } @@ -240,7 +237,7 @@ export default function Workspace({ onOpenSettings }: WorkspaceProps): React.JSX /** Per-workspace active panel used as the split anchor. */ const activeTerminalPanelRef = useRef(undefined) const activeSshPanelRef = useRef(undefined) - /** The currently-visible mode's active panel (broadcast / quick-input target). */ + /** The currently-visible mode's active panel (broadcast target). */ const activePanelRef = useRef(undefined) /** Sessions we already asked to kill — kill once, never twice. */ @@ -961,13 +958,6 @@ export default function Workspace({ onOpenSettings }: WorkspaceProps): React.JSX } } - /** Session id of the currently active, visible terminal panel ('' when none). */ - const resolveActiveSessionId = useCallback((): string => { - const panel = activePanelRef.current - if (!panel) return '' - return sessionIdOf(panel) ?? '' - }, []) - return (
- {/* M6: floating quick-input panel (bottom-right). Sends to the active - session; broadcast target → fan-out. Sits above the sftp transfer - overlay's 14px bottom offset so the two never overlap. */} - - {/* SSH connect flow: secret prompt (cancellable) → connecting modal (not). */} {requestedConn != null && ( s.toggle) - const open = useQuickInputStore((s) => s.open) - return ( - - - - ) -} - /** * XTerminal-style slim icon rail on the left edge — replaces the top toolbar. * Top: workspace actions; bottom: settings. diff --git a/src/renderer/src/workspace/workspace.css b/src/renderer/src/workspace/workspace.css index 28bd0c0..3c24128 100644 --- a/src/renderer/src/workspace/workspace.css +++ b/src/renderer/src/workspace/workspace.css @@ -446,7 +446,7 @@ body.ssh-resizing .ssh-bottom-divider { color: var(--tab-accent-fg, #3fd06a); } -/* ---- M6 tab-bar workspace actions (broadcast + quick-input) ---- */ +/* ---- M6 tab-bar workspace actions (broadcast) ---- */ .workspace-tabbar-actions { display: flex; align-items: center; @@ -574,147 +574,6 @@ body.ssh-resizing .ssh-bottom-divider { user-select: none; } -/* ---- M6 quick-input floating panel (bottom-right) ---- - bottom: 44px so it clears the sftp transfer overlay (right/bottom 14px). */ -.quick-input-panel { - position: fixed; - right: 14px; - bottom: 44px; - z-index: 950; - width: 320px; - max-height: 60vh; - display: flex; - flex-direction: column; - gap: 8px; - padding: 10px; - background: #1f1f1f; - border: 1px solid var(--chrome-border); - border-radius: 8px; - box-shadow: 0 6px 24px rgba(0, 0, 0, 0.5); - color: var(--chrome-fg); -} - -.quick-input-headbar { - display: flex; - align-items: center; - justify-content: space-between; -} - -.quick-input-headbar-title { - font-size: 12px; - font-weight: 600; - color: var(--chrome-fg); -} - -.quick-input-close { - border: none; - background: none; - color: #8a8a8a; - font-size: 15px; - line-height: 1; - cursor: pointer; - padding: 0 2px; -} - -.quick-input-close:hover { - color: #ffffff; -} - -.quick-input-row { - display: flex; - flex-direction: column; - gap: 6px; -} - -.quick-input-field .ant-input { - background: var(--chrome-bg); - border-color: var(--chrome-border); - color: var(--chrome-fg); -} - -.quick-input-autoenter { - display: flex; - align-items: center; - gap: 5px; - font-size: 11px; - color: #9a9a9a; - cursor: pointer; - user-select: none; -} - -.quick-input-autoenter input { - accent-color: var(--chrome-accent); -} - -.quick-input-section { - display: flex; - flex-direction: column; - gap: 4px; - min-height: 0; -} - -.quick-input-head { - font-size: 11px; - color: #8a8a8a; - text-transform: uppercase; - letter-spacing: 0.4px; -} - -.quick-input-list { - display: flex; - flex-direction: column; - gap: 1px; - max-height: 140px; - overflow-y: auto; -} - -.quick-input-item { - display: flex; - align-items: center; - gap: 6px; - border: none; - background: transparent; - color: var(--chrome-fg); - font: inherit; - font-size: 12px; - text-align: left; - padding: 3px 6px; - border-radius: 4px; - cursor: pointer; - min-width: 0; -} - -.quick-input-item:hover { - background: rgba(59, 142, 234, 0.18); - color: #ffffff; -} - -.quick-input-item-cmd { - flex: 1 1 auto; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-family: 'Cascadia Mono', 'Consolas', 'SFMono-Regular', monospace; -} - -.quick-input-item-tag { - flex: 0 0 auto; - font-size: 10px; - line-height: 1; - padding: 2px 5px; - border-radius: 4px; - color: #9a9a9a; - background: rgba(255, 255, 255, 0.08); -} - -.quick-input-empty { - padding: 8px 4px; - color: #8a8a8a; - font-size: 12px; - text-align: center; -} - /* ---- custom tab (title + close + context menu) ---- */ .workspace-terminal-tab { position: relative;