From f784e5c88c6c8edb86ccf592cc1cf35dd2fbf42b Mon Sep 17 00:00:00 2001 From: fengx Date: Sat, 17 Oct 2026 19:19:25 +0800 Subject: [PATCH 1/9] feat(layout): native floating ball with swipe gestures Replaces the Tampermonkey userscript, which could not work on the phone (Chrome has no extensions there) and whose stop action matched a button label codeg never renders. The gesture vocabulary is MindFS's blue-ring menu, kept in a pure module so the tracking rules are unit-tested: tap/double tap/long press plus eight cardinal and diagonal strokes, with two-stage folds anchored at each turn. Actions dispatch through the workspace contexts this controller already owns (tabs, panels, zoom, the active ACP connection), so nothing has to synthesize keyboard events. Off by default; enabled from Appearance settings, which also resets the stored position. --- src/components/appearance-provider.tsx | 120 +++++ src/components/layout/floating-ball.tsx | 508 ++++++++++++++++++ .../layout/workspace-chrome-controller.tsx | 215 ++++++-- .../settings/appearance-settings.tsx | 103 ++++ src/hooks/use-appearance.ts | 14 + src/i18n/messages/ar.json | 48 ++ src/i18n/messages/de.json | 48 ++ src/i18n/messages/en.json | 48 ++ src/i18n/messages/es.json | 48 ++ src/i18n/messages/fr.json | 48 ++ src/i18n/messages/ja.json | 48 ++ src/i18n/messages/ko.json | 48 ++ src/i18n/messages/pt.json | 48 ++ src/i18n/messages/zh-CN.json | 48 ++ src/i18n/messages/zh-TW.json | 48 ++ src/lib/appearance-script.ts | 6 + src/lib/floating-ball-gestures.test.ts | 183 +++++++ src/lib/floating-ball-gestures.ts | 296 ++++++++++ 18 files changed, 1870 insertions(+), 55 deletions(-) create mode 100644 src/components/layout/floating-ball.tsx create mode 100644 src/lib/floating-ball-gestures.test.ts create mode 100644 src/lib/floating-ball-gestures.ts diff --git a/src/components/appearance-provider.tsx b/src/components/appearance-provider.tsx index c2c6c3e0f4..d33257b30c 100644 --- a/src/components/appearance-provider.tsx +++ b/src/components/appearance-provider.tsx @@ -38,6 +38,9 @@ import { STORAGE_KEY_ZOOM_LEVEL, STORAGE_KEY_CHAT_ANIMATIONS, STORAGE_KEY_CHAT_CONTENT_WIDTH, + STORAGE_KEY_MESSAGE_NAV_POSITION, + STORAGE_KEY_FLOATING_BALL_ENABLED, + STORAGE_KEY_FLOATING_BALL_POSITION, STORAGE_KEY_WELCOME_QUICK_ACTIONS, STORAGE_KEY_UI_FONT, STORAGE_KEY_UI_FONT_CUSTOM, @@ -116,6 +119,18 @@ function syncAppearanceMode(mode: string) { export type FontSelection = { id: string; custom: string } +/** 消息导航卡位置。`start` = 逻辑起始边(LTR 左上,历史行为); + * `bottom-end` = 右下角紧凑卡片。 */ +export type MessageNavPosition = "start" | "bottom-end" + +export const DEFAULT_MESSAGE_NAV_POSITION: MessageNavPosition = "start" + +/** 悬浮球停靠位置(视口坐标 px,左上角原点)。 */ +export type FloatingBallPosition = { x: number; y: number } + +/** 缺省停靠:右下角,距右边与底边各 18px(底边让出安全区)。 */ +export const FLOATING_BALL_INSET = 18 + type AppearanceContextValue = { themeColor: ThemeColor setThemeColor: (color: ThemeColor) => void @@ -127,6 +142,15 @@ type AppearanceContextValue = { /** 聊天区域动画总开关,默认开启;关闭时 带 data-chat-animations="off" */ chatAnimations: boolean setChatAnimations: (on: boolean) => void + /** 消息导航卡位置:起始边(默认,左上)或右下角紧凑卡片。 */ + messageNavPosition: MessageNavPosition + setMessageNavPosition: (position: MessageNavPosition) => void + /** 悬浮球(手势入口)是否显示,默认关闭。 */ + floatingBallEnabled: boolean + setFloatingBallEnabled: (on: boolean) => void + /** 悬浮球停靠位置;null = 右下角缺省位。 */ + floatingBallPosition: FloatingBallPosition | null + setFloatingBallPosition: (position: FloatingBallPosition | null) => void /** * 聊天内容宽度(100% 缩放下的 px,落到 CSS 时换成 rem,随缩放变化), * null = 内置默认(48rem)。拖拽手柄与外观设置页共用;set 会持久化并落到 @@ -277,6 +301,40 @@ function readBool(key: string, def: boolean): boolean { } } +function readMessageNavPosition(): MessageNavPosition { + if (typeof document === "undefined") return DEFAULT_MESSAGE_NAV_POSITION + try { + return localStorage.getItem(STORAGE_KEY_MESSAGE_NAV_POSITION) === + "bottom-end" + ? "bottom-end" + : DEFAULT_MESSAGE_NAV_POSITION + } catch { + return DEFAULT_MESSAGE_NAV_POSITION + } +} + +function readFloatingBallPosition(): FloatingBallPosition | null { + if (typeof document === "undefined") return null + try { + const raw = localStorage.getItem(STORAGE_KEY_FLOATING_BALL_POSITION) + if (!raw) return null + const parsed: unknown = JSON.parse(raw) + if (typeof parsed !== "object" || parsed === null) return null + const { x, y } = parsed as Record + if ( + typeof x !== "number" || + typeof y !== "number" || + !Number.isFinite(x) || + !Number.isFinite(y) + ) { + return null + } + return { x, y } + } catch { + return null + } +} + function readNumber( key: string, def: number, @@ -418,6 +476,17 @@ export function AppearanceProvider({ } }, [chatAnimations]) + // 消息导航卡位置。默认起始边(历史行为),仅在用户显式选右下角时才变。 + const [messageNavPosition, setMessageNavPositionState] = + useState(() => readMessageNavPosition()) + + // 悬浮球:默认不显示,显式开启后才挂载;位置缺省为右下角。 + const [floatingBallEnabled, setFloatingBallEnabledState] = useState( + () => readBool(STORAGE_KEY_FLOATING_BALL_ENABLED, false) + ) + const [floatingBallPosition, setFloatingBallPositionState] = + useState(() => readFloatingBallPosition()) + // 聊天内容宽度:DOM 变量由 inline 脚本预置,这里持有状态供设置页回显。 const [chatContentWidth, setChatContentWidthState] = useState( () => readChatContentWidth() @@ -582,6 +651,38 @@ export function AppearanceProvider({ persist(STORAGE_KEY_CHAT_ANIMATIONS, on ? "1" : "0") }, []) + const setMessageNavPosition = useCallback((position: MessageNavPosition) => { + setMessageNavPositionState(position) + persist( + STORAGE_KEY_MESSAGE_NAV_POSITION, + position === "bottom-end" ? "bottom-end" : "start" + ) + }, []) + + const setFloatingBallEnabled = useCallback((on: boolean) => { + setFloatingBallEnabledState(on) + persist(STORAGE_KEY_FLOATING_BALL_ENABLED, on ? "1" : "0") + }, []) + + const setFloatingBallPosition = useCallback( + (position: FloatingBallPosition | null) => { + setFloatingBallPositionState(position) + if (position === null) { + try { + localStorage.removeItem(STORAGE_KEY_FLOATING_BALL_POSITION) + } catch { + // 存不进去也不影响本窗口——位置只是便利,不是数据。 + } + return + } + persist( + STORAGE_KEY_FLOATING_BALL_POSITION, + JSON.stringify({ x: Math.round(position.x), y: Math.round(position.y) }) + ) + }, + [] + ) + const setChatContentWidth = useCallback((px: number | null) => { chatContentWidthRef.current = px setChatContentWidthState(px) @@ -1033,6 +1134,19 @@ export function AppearanceProvider({ if (e.key === STORAGE_KEY_CHAT_ANIMATIONS) { setChatAnimationsState(readBool(STORAGE_KEY_CHAT_ANIMATIONS, true)) } + // 外观设置是独立窗口:那边改了导航卡位置,工作区窗口要跟着搬。 + if (e.key === STORAGE_KEY_MESSAGE_NAV_POSITION) { + setMessageNavPositionState(readMessageNavPosition()) + } + // 悬浮球同样在外观设置里开关,那边的改动要立刻反映到工作区窗口。 + if (e.key === STORAGE_KEY_FLOATING_BALL_ENABLED) { + setFloatingBallEnabledState( + readBool(STORAGE_KEY_FLOATING_BALL_ENABLED, false) + ) + } + if (e.key === STORAGE_KEY_FLOATING_BALL_POSITION) { + setFloatingBallPositionState(readFloatingBallPosition()) + } if (e.key && FONT_KEYS.has(e.key)) { rehydrateFonts() } @@ -1139,6 +1253,12 @@ export function AppearanceProvider({ setShowWelcomeQuickActions, chatAnimations, setChatAnimations, + messageNavPosition, + setMessageNavPosition, + floatingBallEnabled, + setFloatingBallEnabled, + floatingBallPosition, + setFloatingBallPosition, chatContentWidth, setChatContentWidth, previewChatContentWidth: applyChatContentWidth, diff --git a/src/components/layout/floating-ball.tsx b/src/components/layout/floating-ball.tsx new file mode 100644 index 0000000000..990e96b57b --- /dev/null +++ b/src/components/layout/floating-ball.tsx @@ -0,0 +1,508 @@ +"use client" + +import { Command, X } from "lucide-react" +import { useCallback, useEffect, useRef, useState } from "react" +import { useTranslations } from "next-intl" + +import { + FLOATING_BALL_INSET, + type FloatingBallPosition, +} from "@/components/appearance-provider" +import { useFloatingBall } from "@/hooks/use-appearance" +import { + createFloatingBallTracker, + DEFAULT_FLOATING_BALL_BINDINGS, + DOUBLE_TAP_MS, + GESTURE_ONSET_MS, + LONG_PRESS_MS, + MOVE_PX, + resolveTapGesture, + type FloatingBallAction, + type FloatingBallGesture, +} from "@/lib/floating-ball-gestures" +import { cn } from "@/lib/utils" + +const BALL_SIZE = 46 +const PANEL_WIDTH = 208 + +/** The rows the tap panel offers, in reading order. */ +const PANEL_ACTIONS: readonly Exclude[] = [ + "newConversation", + "toggleSidebar", + "toggleSearch", + "toggleTerminal", + "nextTab", + "prevTab", + "reopenClosedTab", + "openFolder", + "zoomIn", + "zoomOut", + "zoomReset", + "stopGenerating", + "openSettings", +] + +/** Gestures shown in the cheat sheet, with the action they fire. */ +const CHEAT_SHEET = [ + "tap", + "doubleTap", + "longPress", + "left", + "right", + "up", + "upLeft", + "upRight", + "upThenLeft", + "upThenRight", + "upDown", + "leftThenRight", + "rightThenLeft", + "upLeftThenDown", + "upRightThenDown", +] as const satisfies readonly FloatingBallGesture[] + +interface FloatingBallProps { + /** Runs an action against the workspace (tab, panel, zoom, agent turn). */ + onAction: (action: FloatingBallAction) => void +} + +function clampPosition( + position: FloatingBallPosition, + viewport: { width: number; height: number } +): FloatingBallPosition { + const maxX = Math.max(0, viewport.width - BALL_SIZE - 4) + const maxY = Math.max(0, viewport.height - BALL_SIZE - 4) + return { + x: Math.min(Math.max(4, position.x), maxX), + y: Math.min(Math.max(4, position.y), maxY), + } +} + +function defaultPosition(viewport: { + width: number + height: number +}): FloatingBallPosition { + return { + x: viewport.width - BALL_SIZE - FLOATING_BALL_INSET, + y: viewport.height - BALL_SIZE - FLOATING_BALL_INSET, + } +} + +function readViewport(): { width: number; height: number } { + if (typeof window === "undefined") return { width: 0, height: 0 } + // `visualViewport` is what shrinks when the on-screen keyboard opens; the + // layout viewport does not, so anchoring to it would park the ball behind + // the keyboard. + const vv = window.visualViewport + return { + width: Math.round(vv?.width ?? window.innerWidth), + height: Math.round(vv?.height ?? window.innerHeight), + } +} + +/** + * A draggable ball over the workspace that turns swipes into the actions the + * keyboard shortcuts already perform — the touch counterpart of those + * shortcuts, for the phone, where there is no keyboard. + * + * The gesture vocabulary comes from MindFS's blue-ring menu so the muscle + * memory carries over; see `floating-ball-gestures.ts`. Actions are dispatched + * by the caller, which owns the workspace contexts. + */ +export function FloatingBall({ onAction }: FloatingBallProps) { + const t = useTranslations("FloatingBall") + const { floatingBallEnabled, floatingBallPosition, setFloatingBallPosition } = + useFloatingBall() + + const [viewport, setViewport] = useState(readViewport) + // The stored position is the only source of truth: deriving the on-screen + // position from it keeps rotation, window resizing and a reset from another + // window consistent without copying it into local state. + const position = floatingBallPosition + ? clampPosition(floatingBallPosition, viewport) + : defaultPosition(viewport) + const [panelOpen, setPanelOpen] = useState(false) + const [settingsOpen, setSettingsOpen] = useState(false) + + const ballRef = useRef(null) + const trackerRef = useRef(createFloatingBallTracker()) + const dragRef = useRef<{ + pointerId: number + /** Where the ball sat when the press began, in viewport px. */ + origin: FloatingBallPosition + /** Where the finger went down. */ + startX: number + startY: number + startedAt: number + mode: "undecided" | "gesture" | "drag" + } | null>(null) + const lastTapAtRef = useRef(null) + const tapTimerRef = useRef | null>(null) + const frameRef = useRef(null) + + // Keep the viewport current: it decides both the default corner and where an + // off-screen ball gets clamped back to. + useEffect(() => { + const onResize = () => setViewport(readViewport()) + window.addEventListener("resize", onResize) + window.visualViewport?.addEventListener("resize", onResize) + return () => { + window.removeEventListener("resize", onResize) + window.visualViewport?.removeEventListener("resize", onResize) + } + }, []) + + const clearTapTimer = useCallback(() => { + if (tapTimerRef.current !== null) { + clearTimeout(tapTimerRef.current) + tapTimerRef.current = null + } + }, []) + + useEffect(() => clearTapTimer, [clearTapTimer]) + + const fire = useCallback( + (gesture: FloatingBallGesture) => { + const action = DEFAULT_FLOATING_BALL_BINDINGS[gesture] + if (!action) return + if (action === "panel") { + setSettingsOpen(false) + setPanelOpen((open) => !open) + return + } + if (action === "openSettings") { + setPanelOpen(false) + setSettingsOpen((open) => !open) + return + } + setPanelOpen(false) + setSettingsOpen(false) + onAction(action) + }, + [onAction] + ) + + const onPointerDown = useCallback( + (event: React.PointerEvent) => { + if (event.button !== 0 && event.pointerType === "mouse") return + const now = Date.now() + dragRef.current = { + pointerId: event.pointerId, + origin: position, + startX: event.clientX, + startY: event.clientY, + startedAt: now, + mode: "undecided", + } + trackerRef.current.start(event.clientX, event.clientY) + event.currentTarget.setPointerCapture(event.pointerId) + }, + [position] + ) + + const onPointerMove = useCallback( + (event: React.PointerEvent) => { + const drag = dragRef.current + if (!drag || drag.pointerId !== event.pointerId) return + const dx = event.clientX - drag.startX + const dy = event.clientY - drag.startY + const distance = Math.hypot(dx, dy) + + if (drag.mode === "undecided" && distance > MOVE_PX) { + // Fast movement is a gesture; a press that only starts moving after + // the onset window is the user repositioning the ball. + drag.mode = + Date.now() - drag.startedAt < GESTURE_ONSET_MS ? "gesture" : "drag" + } + + if (drag.mode === "drag") { + event.preventDefault() + const next = clampPosition( + { x: drag.origin.x + dx, y: drag.origin.y + dy }, + readViewport() + ) + // Write the frame directly: a state update per pointer sample would + // re-render the whole overlay tree on every move. + if (frameRef.current !== null) cancelAnimationFrame(frameRef.current) + frameRef.current = requestAnimationFrame(() => { + if (ballRef.current) { + ballRef.current.style.left = `${next.x}px` + ballRef.current.style.top = `${next.y}px` + } + frameRef.current = null + }) + return + } + + if (drag.mode === "gesture") { + event.preventDefault() + trackerRef.current.move(event.clientX, event.clientY) + } + }, + [] + ) + + const onPointerUp = useCallback( + (event: React.PointerEvent) => { + const drag = dragRef.current + if (!drag || drag.pointerId !== event.pointerId) return + dragRef.current = null + if (frameRef.current !== null) { + cancelAnimationFrame(frameRef.current) + frameRef.current = null + } + + if (drag.mode === "drag") { + const dx = event.clientX - drag.startX + const dy = event.clientY - drag.startY + setFloatingBallPosition( + clampPosition( + { x: drag.origin.x + dx, y: drag.origin.y + dy }, + readViewport() + ) + ) + return + } + + if (drag.mode === "gesture") { + const gesture = trackerRef.current.end(event.clientX, event.clientY) + trackerRef.current.reset() + if (gesture) fire(gesture) + return + } + + // A press that never moved: tap, double tap or long press. + const now = Date.now() + const heldMs = now - drag.startedAt + const sinceLastTap = + lastTapAtRef.current === null ? null : now - lastTapAtRef.current + const gesture = resolveTapGesture(heldMs, sinceLastTap) + + if (gesture === "longPress") { + clearTapTimer() + lastTapAtRef.current = null + fire("longPress") + return + } + if (gesture === "doubleTap") { + clearTapTimer() + lastTapAtRef.current = null + fire("doubleTap") + return + } + + // A single tap is only known to be single once the double-tap window + // has passed, so it waits — otherwise the panel would flash open before + // a double tap could replace it. + lastTapAtRef.current = now + clearTapTimer() + tapTimerRef.current = setTimeout(() => { + tapTimerRef.current = null + lastTapAtRef.current = null + fire("tap") + }, DOUBLE_TAP_MS) + }, + [clearTapTimer, fire, setFloatingBallPosition] + ) + + const onPointerCancel = useCallback( + (event: React.PointerEvent) => { + const drag = dragRef.current + if (!drag || drag.pointerId !== event.pointerId) return + dragRef.current = null + trackerRef.current.reset() + if (frameRef.current !== null) { + cancelAnimationFrame(frameRef.current) + frameRef.current = null + } + }, + [] + ) + + // Dismiss either surface on an outside press or Escape. + useEffect(() => { + if (!panelOpen && !settingsOpen) return + const onPointerDown = (event: PointerEvent) => { + const target = event.target as Node | null + if (!target) return + if (ballRef.current?.contains(target)) return + if ((target as Element).closest?.("[data-floating-ball-surface]")) return + setPanelOpen(false) + setSettingsOpen(false) + } + const onKeyDown = (event: KeyboardEvent) => { + if (event.key !== "Escape") return + setPanelOpen(false) + setSettingsOpen(false) + } + document.addEventListener("pointerdown", onPointerDown, true) + document.addEventListener("keydown", onKeyDown) + return () => { + document.removeEventListener("pointerdown", onPointerDown, true) + document.removeEventListener("keydown", onKeyDown) + } + }, [panelOpen, settingsOpen]) + + if (!floatingBallEnabled) return null + + // The panel opens toward the middle of the screen: above the ball when it + // sits in the lower half, below it otherwise. + const opensUpward = position.y > viewport.height / 2 + const panelBottom = opensUpward ? viewport.height - position.y + 8 : undefined + const panelTop = opensUpward ? undefined : position.y + BALL_SIZE + 8 + const panelLeft = Math.min( + Math.max(8, position.x + BALL_SIZE - PANEL_WIDTH), + Math.max(8, viewport.width - PANEL_WIDTH - 8) + ) + + return ( + <> + + + {panelOpen && ( +
+
+ {t("panelTitle")} + +
+
+ {PANEL_ACTIONS.map((action) => ( + + ))} +
+
+ )} + + {settingsOpen && ( +
+
+ {t("settingsTitle")} + +
+
+ +
+

+ {t("cheatSheetTitle")} +

+
    + {CHEAT_SHEET.map((gesture) => { + const action = DEFAULT_FLOATING_BALL_BINDINGS[gesture] + if (!action) return null + return ( +
  • + + {t(`gestures.${gesture}`)} + + + {action === "panel" + ? t("panelTitle") + : t(`actions.${action}`)} + +
  • + ) + })} +
+
+

+ {t("hint")} +

+
+
+ )} + + ) +} + +export { LONG_PRESS_MS } diff --git a/src/components/layout/workspace-chrome-controller.tsx b/src/components/layout/workspace-chrome-controller.tsx index 02e8669997..3155eea62c 100644 --- a/src/components/layout/workspace-chrome-controller.tsx +++ b/src/components/layout/workspace-chrome-controller.tsx @@ -2,6 +2,11 @@ import { useCallback, useEffect, useRef, useState } from "react" import { openSettingsWindow } from "@/lib/api" +import { + useAcpActions, + useConnectionStore, +} from "@/contexts/acp-connections-context" +import { useZoomLevel } from "@/hooks/use-appearance" import { useActiveFolder } from "@/contexts/active-folder-context" import { useIsActiveChatMode } from "@/hooks/use-is-active-chat-mode" import { useSidebarContext } from "@/contexts/sidebar-context" @@ -39,7 +44,10 @@ import { getCurrentWindowLabel } from "@/lib/browser/window-label" import { closeCurrentWindow } from "@/lib/platform" import { getShellTransport, isDesktop } from "@/lib/transport" import { SearchCommandDialog } from "@/components/conversations/search-command-dialog" +import { FloatingBall } from "@/components/layout/floating-ball" import { WorkspaceFolderDialog } from "@/components/layout/workspace-folder-dialog" +import { stepZoom, DEFAULT_ZOOM_LEVEL } from "@/lib/theme-presets" +import type { FloatingBallAction } from "@/lib/floating-ball-gestures" /** * Headless owner of the workspace's global keyboard shortcuts and the two @@ -75,6 +83,13 @@ export function WorkspaceChromeController() { } = useWorkspaceActions() const { openConversations } = useWorkbenchRoute() const { shortcuts } = useShortcutSettings() + const { zoomLevel, setZoomLevel } = useZoomLevel() + // Stopping a turn is the one floating-ball action with no shortcut: the + // composer's stop button is the only other entry point, and it belongs to + // whichever conversation happens to be mounted. The active connection is + // what the workspace considers "the turn in front of the user". + const { cancel } = useAcpActions() + const { getActiveKey } = useConnectionStore() // Search open-state is shared (see search-dialog-context): the trigger lives // in the sidebar, but this always-mounted controller owns the dialog and the // ⌘K shortcut so search works even when the sidebar is collapsed. @@ -206,6 +221,70 @@ export function WorkspaceChromeController() { } }, []) + // Reopen the most recently closed tab, at the slot it was closed from. Shared + // by the shortcut and the floating ball so the two cannot drift. + const reopenClosedTab = useCallback(() => { + // Every entry carries the slot it was closed from, and each opener + // puts the tab back there (clamped to the strip) rather than at the + // end. + while (true) { + const closed = popClosedTab() + if (!closed) return + if (closed.kind === "file") { + void openFilePreview(closed.path, { + folderId: closed.folderId ?? undefined, + index: closed.index, + }) + return + } + if (closed.kind === "browser") { + // Back at the page it was showing; a tab already on that page is + // activated instead (the usual one-tab-per-URL rule). + openBrowserTab(closed.url, { + folderId: closed.folderId ?? undefined, + index: closed.index, + profile: closed.profile, + // Said only when it was remote: otherwise the address decides, + // as it does when the tab comes back after a restart. + ...(closed.remote === true ? { remote: true } : {}), + ...(closed.device ? { device: closed.device } : {}), + }) + return + } + if (closed.conversationId != null) { + // A deletion seen at any point wins. `applyConversationRemove` + // purges what is on the stack when it runs, but a tab that was + // still open then gets recorded afterwards, when the user closes + // it by hand — that entry can only be caught here. + if (isConversationDeleted(closed.conversationId)) continue + openConversations() + openTab( + closed.folderId, + closed.conversationId, + closed.agentType, + closed.isPinned, + closed.title, + { index: closed.index } + ) + return + } + const folder = useAppWorkspaceStore.getState().getFolder(closed.folderId) + const workingDir = closed.workingDir ?? folder?.path + if (!workingDir) continue + openConversations() + openNewConversationTab(closed.folderId, workingDir, { + index: closed.index, + }) + return + } + }, [ + openBrowserTab, + openConversations, + openFilePreview, + openNewConversationTab, + openTab, + ]) + useEffect(() => { function handleKeyDown(e: KeyboardEvent) { if (matchShortcutEvent(e, shortcuts.toggle_search)) { @@ -332,61 +411,8 @@ export function WorkspaceChromeController() { if (matchShortcutEvent(e, shortcuts.reopen_last_closed_tab)) { e.preventDefault() - // Every entry carries the slot it was closed from, and each opener - // puts the tab back there (clamped to the strip) rather than at the - // end. - while (true) { - const closed = popClosedTab() - if (!closed) return - if (closed.kind === "file") { - void openFilePreview(closed.path, { - folderId: closed.folderId ?? undefined, - index: closed.index, - }) - return - } - if (closed.kind === "browser") { - // Back at the page it was showing; a tab already on that page is - // activated instead (the usual one-tab-per-URL rule). - openBrowserTab(closed.url, { - folderId: closed.folderId ?? undefined, - index: closed.index, - profile: closed.profile, - // Said only when it was remote: otherwise the address decides, - // as it does when the tab comes back after a restart. - ...(closed.remote === true ? { remote: true } : {}), - ...(closed.device ? { device: closed.device } : {}), - }) - return - } - if (closed.conversationId != null) { - // A deletion seen at any point wins. `applyConversationRemove` - // purges what is on the stack when it runs, but a tab that was - // still open then gets recorded afterwards, when the user closes - // it by hand — that entry can only be caught here. - if (isConversationDeleted(closed.conversationId)) continue - openConversations() - openTab( - closed.folderId, - closed.conversationId, - closed.agentType, - closed.isPinned, - closed.title, - { index: closed.index } - ) - return - } - const folder = useAppWorkspaceStore - .getState() - .getFolder(closed.folderId) - const workingDir = closed.workingDir ?? folder?.path - if (!workingDir) continue - openConversations() - openNewConversationTab(closed.folderId, workingDir, { - index: closed.index, - }) - return - } + reopenClosedTab() + return } } document.addEventListener("keydown", handleKeyDown) @@ -410,6 +436,7 @@ export function WorkspaceChromeController() { activeTabId, switchTab, closeCurrentTab, + reopenClosedTab, mode, activePane, filesMaximized, @@ -419,10 +446,88 @@ export function WorkspaceChromeController() { switchFileTab, ]) + const handleFloatingBallAction = useCallback( + (action: FloatingBallAction) => { + switch (action) { + case "newConversation": + if (!activeFolder) return + openConversations() + openNewConversationTab(activeFolder.id, activeFolder.path) + return + case "toggleSidebar": + toggle() + return + case "toggleSearch": + setSearchOpen((prev) => !prev) + return + case "toggleTerminal": + toggleTerminal() + return + case "nextTab": + case "prevTab": { + if (tabs.length < 2 || !activeTabId) return + const currentIndex = tabs.findIndex((tab) => tab.id === activeTabId) + if (currentIndex === -1) return + const offset = action === "nextTab" ? 1 : -1 + const nextIndex = (currentIndex + offset + tabs.length) % tabs.length + switchTab(tabs[nextIndex].id) + return + } + case "reopenClosedTab": + reopenClosedTab() + return + case "openFolder": + void handleOpenFolder() + return + case "zoomIn": + setZoomLevel(stepZoom(zoomLevel, 1)) + return + case "zoomOut": + setZoomLevel(stepZoom(zoomLevel, -1)) + return + case "zoomReset": + setZoomLevel(DEFAULT_ZOOM_LEVEL) + return + case "stopGenerating": { + const key = getActiveKey() + if (!key) return + void cancel(key).catch((err) => { + console.error("[WorkspaceChromeController] stop failed:", err) + }) + return + } + case "openSettings": + handleOpenSettings() + return + case "panel": + return + } + }, + [ + activeFolder, + activeTabId, + cancel, + getActiveKey, + handleOpenFolder, + handleOpenSettings, + openConversations, + openNewConversationTab, + reopenClosedTab, + setSearchOpen, + setZoomLevel, + switchTab, + tabs, + toggle, + toggleTerminal, + zoomLevel, + ] + ) + return ( <> + ) } diff --git a/src/components/settings/appearance-settings.tsx b/src/components/settings/appearance-settings.tsx index 405409c3e0..024688263c 100644 --- a/src/components/settings/appearance-settings.tsx +++ b/src/components/settings/appearance-settings.tsx @@ -5,6 +5,7 @@ import { Monitor, Moon, MoveHorizontal, + Orbit, Sparkles, Sun, Type, @@ -29,6 +30,7 @@ import { useWelcomeQuickActions, useChatAnimationsSetting, useChatContentWidth, + useFloatingBall, } from "@/hooks/use-appearance" import { CHAT_CONTENT_DEFAULT, @@ -68,6 +70,12 @@ export function AppearanceSettings() { const { showWelcomeQuickActions, setShowWelcomeQuickActions } = useWelcomeQuickActions() const { chatAnimations, setChatAnimations } = useChatAnimationsSetting() + const { + floatingBallEnabled, + setFloatingBallEnabled, + floatingBallPosition, + setFloatingBallPosition, + } = useFloatingBall() const { chatContentWidth, setChatContentWidth } = useChatContentWidth() const [screenWidth] = useState(readScreenWidth) // 宽度以 100% 缩放下的 px 计(随缩放一起放大),所以同一块屏幕在高缩放下能放下 @@ -295,7 +303,102 @@ export function AppearanceSettings() { +

+ {t("floatingBall.sectionDescription")} +

+ + + +
+

+ {t("floatingBall.hint")} +

+ +
+ {/* ===== Chat content width ===== */} +

+ {t("messageNav.sectionDescription")} +

+ +
+ +

+ {t("messageNav.hint")} +

+
+ + {/* ===== Floating ball ===== */} +
+
+ +

+ {t("floatingBall.sectionTitle")} +

+
+ +

+ {t("floatingBall.sectionDescription")} +

+ + + +
+

+ {t("floatingBall.hint")} +

+ +
+
+ {/* ===== Chat content width ===== */}{" "} +>>>>>>> aebc706f (feat(layout): native floating ball with swipe gestures)
diff --git a/src/hooks/use-appearance.ts b/src/hooks/use-appearance.ts index cb354492cc..7d08bece29 100644 --- a/src/hooks/use-appearance.ts +++ b/src/hooks/use-appearance.ts @@ -45,6 +45,20 @@ export function useChatAnimationsSetting() { return { chatAnimations, setChatAnimations } } +/** + * 悬浮球开关与停靠位置。同样允许在 Provider 之外降级:球只在工作区里渲染, + * 设置页是另一个窗口,那里只读开关、不渲染球。 + */ +export function useFloatingBall() { + const ctx = useContext(AppearanceContext) + return { + floatingBallEnabled: ctx?.floatingBallEnabled ?? false, + setFloatingBallEnabled: ctx?.setFloatingBallEnabled ?? (() => {}), + floatingBallPosition: ctx?.floatingBallPosition ?? null, + setFloatingBallPosition: ctx?.setFloatingBallPosition ?? (() => {}), + } +} + /** * 聊天区域是否播放动画。供 JS 驱动的动画(Shimmer、平滑滚动)读取; * 在 Provider 之外(测试、独立窗口)回退为开启,不抛错。读的是只装这一个布尔值的 diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 9064265440..670eea527d 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -239,6 +239,13 @@ "appliedToast": "تم تطبيق الخلفية", "credit": "الصور من wallhaven.cc · محتوى آمن فقط" } + }, + "floatingBall": { + "sectionTitle": "الكرة العائمة", + "sectionDescription": "كرة قابلة للسحب فوق مساحة العمل تحوّل السحب إلى اختصارات، مصممة للهاتف حيث لا توجد لوحة مفاتيح.", + "enable": "إظهار الكرة العائمة", + "hint": "اسحب للتحريك، اسحب بسرعة لإيماءة، اضغط مطوّلاً للوحة وقائمة الإيماءات.", + "resetPosition": "إعادة تعيين الموضع" } }, "SystemSettings": { @@ -7328,5 +7335,46 @@ "probeFailed": "تعذّر على المتصفح المدمج التأكد من أنه يصل إلى المضيف البعيد عبر النفق. حاول مرة أخرى بعد قليل." } } + }, + "FloatingBall": { + "label": "الكرة العائمة", + "panelTitle": "إجراءات سريعة", + "settingsTitle": "الكرة العائمة", + "close": "إغلاق", + "resetPosition": "إعادة تعيين الموضع", + "cheatSheetTitle": "الإيماءات", + "hint": "اسحب الكرة لتحريكها. السحب السريع يشغّل إيماءة، والضغط المطوّل يفتح هذه اللوحة.", + "actions": { + "newConversation": "محادثة جديدة", + "toggleSidebar": "تبديل الشريط الجانبي", + "toggleSearch": "بحث", + "toggleTerminal": "تبديل الطرفية", + "nextTab": "التبويب التالي", + "prevTab": "التبويب السابق", + "reopenClosedTab": "إعادة فتح التبويب المغلق", + "openFolder": "فتح مجلد", + "zoomIn": "تكبير", + "zoomOut": "تصغير", + "zoomReset": "إعادة تعيين التكبير", + "stopGenerating": "إيقاف التوليد", + "openSettings": "الإعدادات" + }, + "gestures": { + "tap": "نقرة", + "doubleTap": "نقرة مزدوجة", + "longPress": "ضغط مطوّل", + "left": "سحب لليسار", + "right": "سحب لليمين", + "up": "سحب للأعلى", + "upLeft": "أعلى اليسار", + "upRight": "أعلى اليمين", + "upThenLeft": "أعلى ثم يسار", + "upThenRight": "أعلى ثم يمين", + "upDown": "أعلى ثم أسفل", + "leftThenRight": "يسار ثم يمين", + "rightThenLeft": "يمين ثم يسار", + "upLeftThenDown": "أعلى اليسار ثم أسفل", + "upRightThenDown": "أعلى اليمين ثم أسفل" + } } } diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 88d009f706..b761c3d26f 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -239,6 +239,13 @@ "appliedToast": "Hintergrund angewendet", "credit": "Bilder von wallhaven.cc · nur SFW" } + }, + "floatingBall": { + "sectionTitle": "Schwebekugel", + "sectionDescription": "Eine verschiebbare Kugel über dem Arbeitsbereich, die Wischgesten in Tastenkürzel verwandelt – gedacht für das Telefon ohne Tastatur.", + "enable": "Schwebekugel anzeigen", + "hint": "Ziehen zum Verschieben, schnelles Wischen für eine Geste, langes Drücken für Panel und Gestenübersicht.", + "resetPosition": "Position zurücksetzen" } }, "SystemSettings": { @@ -7328,5 +7335,46 @@ "probeFailed": "Der integrierte Browser konnte nicht bestätigen, dass er den entfernten Host über den Tunnel erreicht. Versuche es gleich noch einmal." } } + }, + "FloatingBall": { + "label": "Schwebekugel", + "panelTitle": "Schnellaktionen", + "settingsTitle": "Schwebekugel", + "close": "Schließen", + "resetPosition": "Position zurücksetzen", + "cheatSheetTitle": "Gesten", + "hint": "Ziehe die Kugel, um sie zu verschieben. Schnelles Wischen löst eine Geste aus, langes Drücken öffnet dieses Panel.", + "actions": { + "newConversation": "Neue Unterhaltung", + "toggleSidebar": "Seitenleiste umschalten", + "toggleSearch": "Suchen", + "toggleTerminal": "Terminal umschalten", + "nextTab": "Nächster Tab", + "prevTab": "Vorheriger Tab", + "reopenClosedTab": "Geschlossenen Tab wieder öffnen", + "openFolder": "Ordner öffnen", + "zoomIn": "Vergrößern", + "zoomOut": "Verkleinern", + "zoomReset": "Zoom zurücksetzen", + "stopGenerating": "Generierung stoppen", + "openSettings": "Einstellungen" + }, + "gestures": { + "tap": "Tippen", + "doubleTap": "Doppeltippen", + "longPress": "Langes Drücken", + "left": "Nach links wischen", + "right": "Nach rechts wischen", + "up": "Nach oben wischen", + "upLeft": "Oben links", + "upRight": "Oben rechts", + "upThenLeft": "Oben, dann links", + "upThenRight": "Oben, dann rechts", + "upDown": "Oben, dann unten", + "leftThenRight": "Links, dann rechts", + "rightThenLeft": "Rechts, dann links", + "upLeftThenDown": "Oben links, dann unten", + "upRightThenDown": "Oben rechts, dann unten" + } } } diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index dd1aa0c2cd..40dddea163 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -239,6 +239,13 @@ "appliedToast": "Wallpaper applied", "credit": "Images from wallhaven.cc · SFW only" } + }, + "floatingBall": { + "sectionTitle": "Floating Ball", + "sectionDescription": "A draggable ball over the workspace that turns swipes into shortcuts — built for the phone, where there is no keyboard.", + "enable": "Show floating ball", + "hint": "Drag to move; swipe quickly for a gesture; long press for the panel and cheat sheet.", + "resetPosition": "Reset position" } }, "SystemSettings": { @@ -7328,5 +7335,46 @@ "probeFailed": "The built-in browser couldn't confirm that it reaches the remote host through the tunnel. Try again in a moment." } } + }, + "FloatingBall": { + "label": "Floating ball", + "panelTitle": "Quick actions", + "settingsTitle": "Floating ball", + "close": "Close", + "resetPosition": "Reset position", + "cheatSheetTitle": "Gestures", + "hint": "Drag the ball to move it. Swipe quickly for a gesture; press and hold to open this panel.", + "actions": { + "newConversation": "New conversation", + "toggleSidebar": "Toggle sidebar", + "toggleSearch": "Search", + "toggleTerminal": "Toggle terminal", + "nextTab": "Next tab", + "prevTab": "Previous tab", + "reopenClosedTab": "Reopen closed tab", + "openFolder": "Open folder", + "zoomIn": "Zoom in", + "zoomOut": "Zoom out", + "zoomReset": "Reset zoom", + "stopGenerating": "Stop generating", + "openSettings": "Settings" + }, + "gestures": { + "tap": "Tap", + "doubleTap": "Double tap", + "longPress": "Long press", + "left": "Swipe left", + "right": "Swipe right", + "up": "Swipe up", + "upLeft": "Up-left", + "upRight": "Up-right", + "upThenLeft": "Up, then left", + "upThenRight": "Up, then right", + "upDown": "Up, then down", + "leftThenRight": "Left, then right", + "rightThenLeft": "Right, then left", + "upLeftThenDown": "Up-left, then down", + "upRightThenDown": "Up-right, then down" + } } } diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 2b5b20f4a2..5a552bfd2e 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -239,6 +239,13 @@ "appliedToast": "Fondo aplicado", "credit": "Imágenes de wallhaven.cc · solo SFW" } + }, + "floatingBall": { + "sectionTitle": "Bola flotante", + "sectionDescription": "Una bola arrastrable sobre el espacio de trabajo que convierte los deslizamientos en atajos, pensada para el teléfono, donde no hay teclado.", + "enable": "Mostrar bola flotante", + "hint": "Arrastra para mover, desliza rápido para un gesto, mantén pulsado para el panel y la lista de gestos.", + "resetPosition": "Restablecer posición" } }, "SystemSettings": { @@ -7328,5 +7335,46 @@ "probeFailed": "El navegador integrado no pudo confirmar que llega al host remoto a través del túnel. Vuelve a intentarlo en un momento." } } + }, + "FloatingBall": { + "label": "Bola flotante", + "panelTitle": "Acciones rápidas", + "settingsTitle": "Bola flotante", + "close": "Cerrar", + "resetPosition": "Restablecer posición", + "cheatSheetTitle": "Gestos", + "hint": "Arrastra la bola para moverla. Un deslizamiento rápido activa un gesto; una pulsación larga abre este panel.", + "actions": { + "newConversation": "Nueva conversación", + "toggleSidebar": "Mostrar u ocultar la barra lateral", + "toggleSearch": "Buscar", + "toggleTerminal": "Mostrar u ocultar la terminal", + "nextTab": "Pestaña siguiente", + "prevTab": "Pestaña anterior", + "reopenClosedTab": "Reabrir pestaña cerrada", + "openFolder": "Abrir carpeta", + "zoomIn": "Acercar", + "zoomOut": "Alejar", + "zoomReset": "Restablecer zoom", + "stopGenerating": "Detener la generación", + "openSettings": "Ajustes" + }, + "gestures": { + "tap": "Toque", + "doubleTap": "Doble toque", + "longPress": "Pulsación larga", + "left": "Deslizar a la izquierda", + "right": "Deslizar a la derecha", + "up": "Deslizar hacia arriba", + "upLeft": "Arriba a la izquierda", + "upRight": "Arriba a la derecha", + "upThenLeft": "Arriba y luego a la izquierda", + "upThenRight": "Arriba y luego a la derecha", + "upDown": "Arriba y luego abajo", + "leftThenRight": "Izquierda y luego derecha", + "rightThenLeft": "Derecha y luego izquierda", + "upLeftThenDown": "Arriba a la izquierda y luego abajo", + "upRightThenDown": "Arriba a la derecha y luego abajo" + } } } diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 81e0e01077..cfe3b4a4ce 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -239,6 +239,13 @@ "appliedToast": "Fond d'écran appliqué", "credit": "Images de wallhaven.cc · SFW uniquement" } + }, + "floatingBall": { + "sectionTitle": "Boule flottante", + "sectionDescription": "Une boule déplaçable au-dessus de l’espace de travail qui transforme les balayages en raccourcis — pensée pour le téléphone, où il n’y a pas de clavier.", + "enable": "Afficher la boule flottante", + "hint": "Faites glisser pour déplacer, balayez rapidement pour un geste, appuyez longuement pour le panneau et la liste des gestes.", + "resetPosition": "Réinitialiser la position" } }, "SystemSettings": { @@ -7328,5 +7335,46 @@ "probeFailed": "Le navigateur intégré n'a pas pu confirmer qu'il atteint l'hôte distant par le tunnel. Réessayez dans un instant." } } + }, + "FloatingBall": { + "label": "Boule flottante", + "panelTitle": "Actions rapides", + "settingsTitle": "Boule flottante", + "close": "Fermer", + "resetPosition": "Réinitialiser la position", + "cheatSheetTitle": "Gestes", + "hint": "Faites glisser la boule pour la déplacer. Un balayage rapide déclenche un geste, un appui long ouvre ce panneau.", + "actions": { + "newConversation": "Nouvelle conversation", + "toggleSidebar": "Afficher ou masquer la barre latérale", + "toggleSearch": "Rechercher", + "toggleTerminal": "Afficher ou masquer le terminal", + "nextTab": "Onglet suivant", + "prevTab": "Onglet précédent", + "reopenClosedTab": "Rouvrir l’onglet fermé", + "openFolder": "Ouvrir un dossier", + "zoomIn": "Zoom avant", + "zoomOut": "Zoom arrière", + "zoomReset": "Réinitialiser le zoom", + "stopGenerating": "Arrêter la génération", + "openSettings": "Paramètres" + }, + "gestures": { + "tap": "Appui", + "doubleTap": "Double appui", + "longPress": "Appui long", + "left": "Balayage à gauche", + "right": "Balayage à droite", + "up": "Balayage vers le haut", + "upLeft": "Haut-gauche", + "upRight": "Haut-droite", + "upThenLeft": "Haut, puis gauche", + "upThenRight": "Haut, puis droite", + "upDown": "Haut, puis bas", + "leftThenRight": "Gauche, puis droite", + "rightThenLeft": "Droite, puis gauche", + "upLeftThenDown": "Haut-gauche, puis bas", + "upRightThenDown": "Haut-droite, puis bas" + } } } diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index 89ff45c68a..a915251370 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -239,6 +239,13 @@ "appliedToast": "壁紙を適用しました", "credit": "画像提供 wallhaven.cc · SFW のみ" } + }, + "floatingBall": { + "sectionTitle": "フローティングボール", + "sectionDescription": "ワークスペース上に置くドラッグ可能なボール。スワイプをショートカットに変えます。キーボードのないスマートフォン向けです。", + "enable": "フローティングボールを表示", + "hint": "ドラッグで移動、素早いスワイプでジェスチャー、長押しでパネルとジェスチャー一覧を開きます。", + "resetPosition": "位置をリセット" } }, "SystemSettings": { @@ -7328,5 +7335,46 @@ "probeFailed": "内蔵ブラウザーがトンネル経由でリモートホストに届くことを確認できませんでした。しばらくしてからもう一度お試しください。" } } + }, + "FloatingBall": { + "label": "フローティングボール", + "panelTitle": "クイック操作", + "settingsTitle": "フローティングボール", + "close": "閉じる", + "resetPosition": "位置をリセット", + "cheatSheetTitle": "ジェスチャー", + "hint": "ボールをドラッグして移動します。素早くスワイプするとジェスチャー、長押しでこのパネルが開きます。", + "actions": { + "newConversation": "新しい会話", + "toggleSidebar": "サイドバーの切り替え", + "toggleSearch": "検索", + "toggleTerminal": "ターミナルの切り替え", + "nextTab": "次のタブ", + "prevTab": "前のタブ", + "reopenClosedTab": "閉じたタブを開き直す", + "openFolder": "フォルダーを開く", + "zoomIn": "拡大", + "zoomOut": "縮小", + "zoomReset": "ズームをリセット", + "stopGenerating": "生成を停止", + "openSettings": "設定" + }, + "gestures": { + "tap": "タップ", + "doubleTap": "ダブルタップ", + "longPress": "長押し", + "left": "左スワイプ", + "right": "右スワイプ", + "up": "上スワイプ", + "upLeft": "左上スワイプ", + "upRight": "右上スワイプ", + "upThenLeft": "上→左", + "upThenRight": "上→右", + "upDown": "上→下", + "leftThenRight": "左→右", + "rightThenLeft": "右→左", + "upLeftThenDown": "左上→下", + "upRightThenDown": "右上→下" + } } } diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 7ca0474a3f..01214e8f7c 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -239,6 +239,13 @@ "appliedToast": "배경화면이 적용되었습니다", "credit": "이미지 제공 wallhaven.cc · SFW만 표시" } + }, + "floatingBall": { + "sectionTitle": "플로팅 볼", + "sectionDescription": "작업 공간 위의 드래그 가능한 볼로, 스와이프를 단축키로 바꿉니다. 키보드가 없는 휴대폰을 위한 기능입니다.", + "enable": "플로팅 볼 표시", + "hint": "드래그로 이동, 빠른 스와이프로 제스처, 길게 눌러 패널과 제스처 목록을 엽니다.", + "resetPosition": "위치 초기화" } }, "SystemSettings": { @@ -7328,5 +7335,46 @@ "probeFailed": "내장 브라우저가 터널을 통해 원격 호스트에 도달하는지 확인할 수 없습니다. 잠시 후 다시 시도하세요." } } + }, + "FloatingBall": { + "label": "플로팅 볼", + "panelTitle": "빠른 작업", + "settingsTitle": "플로팅 볼", + "close": "닫기", + "resetPosition": "위치 초기화", + "cheatSheetTitle": "제스처", + "hint": "볼을 드래그해 이동하세요. 빠르게 스와이프하면 제스처, 길게 누르면 이 패널이 열립니다.", + "actions": { + "newConversation": "새 대화", + "toggleSidebar": "사이드바 전환", + "toggleSearch": "검색", + "toggleTerminal": "터미널 전환", + "nextTab": "다음 탭", + "prevTab": "이전 탭", + "reopenClosedTab": "닫은 탭 다시 열기", + "openFolder": "폴더 열기", + "zoomIn": "확대", + "zoomOut": "축소", + "zoomReset": "확대/축소 초기화", + "stopGenerating": "생성 중지", + "openSettings": "설정" + }, + "gestures": { + "tap": "탭", + "doubleTap": "더블 탭", + "longPress": "길게 누르기", + "left": "왼쪽 스와이프", + "right": "오른쪽 스와이프", + "up": "위로 스와이프", + "upLeft": "왼쪽 위", + "upRight": "오른쪽 위", + "upThenLeft": "위 → 왼쪽", + "upThenRight": "위 → 오른쪽", + "upDown": "위 → 아래", + "leftThenRight": "왼쪽 → 오른쪽", + "rightThenLeft": "오른쪽 → 왼쪽", + "upLeftThenDown": "왼쪽 위 → 아래", + "upRightThenDown": "오른쪽 위 → 아래" + } } } diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 2b251da735..3d99db6a5a 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -239,6 +239,13 @@ "appliedToast": "Papel de parede aplicado", "credit": "Imagens de wallhaven.cc · apenas SFW" } + }, + "floatingBall": { + "sectionTitle": "Bola flutuante", + "sectionDescription": "Uma bola arrastável sobre o espaço de trabalho que transforma deslizes em atalhos — feita para o telefone, onde não há teclado.", + "enable": "Mostrar bola flutuante", + "hint": "Arraste para mover, deslize rápido para um gesto, toque e segure para o painel e a lista de gestos.", + "resetPosition": "Redefinir posição" } }, "SystemSettings": { @@ -7328,5 +7335,46 @@ "probeFailed": "O navegador integrado não conseguiu confirmar que alcança o host remoto pelo túnel. Tente novamente em instantes." } } + }, + "FloatingBall": { + "label": "Bola flutuante", + "panelTitle": "Ações rápidas", + "settingsTitle": "Bola flutuante", + "close": "Fechar", + "resetPosition": "Redefinir posição", + "cheatSheetTitle": "Gestos", + "hint": "Arraste a bola para movê-la. Um deslize rápido dispara um gesto; um toque longo abre este painel.", + "actions": { + "newConversation": "Nova conversa", + "toggleSidebar": "Alternar barra lateral", + "toggleSearch": "Pesquisar", + "toggleTerminal": "Alternar terminal", + "nextTab": "Próxima aba", + "prevTab": "Aba anterior", + "reopenClosedTab": "Reabrir aba fechada", + "openFolder": "Abrir pasta", + "zoomIn": "Aumentar zoom", + "zoomOut": "Diminuir zoom", + "zoomReset": "Redefinir zoom", + "stopGenerating": "Parar geração", + "openSettings": "Configurações" + }, + "gestures": { + "tap": "Toque", + "doubleTap": "Toque duplo", + "longPress": "Toque longo", + "left": "Deslizar para a esquerda", + "right": "Deslizar para a direita", + "up": "Deslizar para cima", + "upLeft": "Cima-esquerda", + "upRight": "Cima-direita", + "upThenLeft": "Cima, depois esquerda", + "upThenRight": "Cima, depois direita", + "upDown": "Cima, depois baixo", + "leftThenRight": "Esquerda, depois direita", + "rightThenLeft": "Direita, depois esquerda", + "upLeftThenDown": "Cima-esquerda, depois baixo", + "upRightThenDown": "Cima-direita, depois baixo" + } } } diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 4c6566cf89..6155947c3e 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -239,6 +239,13 @@ "appliedToast": "壁纸已应用", "credit": "图片来源 wallhaven.cc · 仅 SFW 内容" } + }, + "floatingBall": { + "sectionTitle": "悬浮球", + "sectionDescription": "工作区上的可拖动悬浮球,把滑动手势变成快捷键——为没有键盘的手机而设。", + "enable": "显示悬浮球", + "hint": "拖动可移动;快速滑动触发手势;长按打开面板与手势表。", + "resetPosition": "重置位置" } }, "SystemSettings": { @@ -7328,5 +7335,46 @@ "probeFailed": "内置浏览器无法确认它能通过隧道访问远端主机。请稍后重试。" } } + }, + "FloatingBall": { + "label": "悬浮球", + "panelTitle": "快捷操作", + "settingsTitle": "悬浮球", + "close": "关闭", + "resetPosition": "重置位置", + "cheatSheetTitle": "手势", + "hint": "拖动悬浮球可移动位置。快速滑动触发手势,长按打开此面板。", + "actions": { + "newConversation": "新建会话", + "toggleSidebar": "切换侧栏", + "toggleSearch": "搜索", + "toggleTerminal": "切换终端", + "nextTab": "下一个标签", + "prevTab": "上一个标签", + "reopenClosedTab": "恢复关闭的标签", + "openFolder": "打开文件夹", + "zoomIn": "放大", + "zoomOut": "缩小", + "zoomReset": "重置缩放", + "stopGenerating": "停止生成", + "openSettings": "设置" + }, + "gestures": { + "tap": "单击", + "doubleTap": "双击", + "longPress": "长按", + "left": "左滑", + "right": "右滑", + "up": "上滑", + "upLeft": "左上滑", + "upRight": "右上滑", + "upThenLeft": "上滑后左滑", + "upThenRight": "上滑后右滑", + "upDown": "上滑后下滑", + "leftThenRight": "左滑后右滑", + "rightThenLeft": "右滑后左滑", + "upLeftThenDown": "左上滑后下滑", + "upRightThenDown": "右上滑后下滑" + } } } diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index f87c51e60c..edb89163ab 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -239,6 +239,13 @@ "appliedToast": "桌布已套用", "credit": "圖片來源 wallhaven.cc · 僅 SFW 內容" } + }, + "floatingBall": { + "sectionTitle": "懸浮球", + "sectionDescription": "工作區上的可拖動懸浮球,把滑動手勢變成快捷鍵——為沒有鍵盤的手機而設。", + "enable": "顯示懸浮球", + "hint": "拖動可移動;快速滑動觸發手勢;長按打開面板與手勢表。", + "resetPosition": "重設位置" } }, "SystemSettings": { @@ -7328,5 +7335,46 @@ "probeFailed": "內建瀏覽器無法確認它能透過通道連到遠端主機。請稍後再試。" } } + }, + "FloatingBall": { + "label": "懸浮球", + "panelTitle": "快捷操作", + "settingsTitle": "懸浮球", + "close": "關閉", + "resetPosition": "重設位置", + "cheatSheetTitle": "手勢", + "hint": "拖動懸浮球可移動位置。快速滑動觸發手勢,長按打開此面板。", + "actions": { + "newConversation": "新增對話", + "toggleSidebar": "切換側欄", + "toggleSearch": "搜尋", + "toggleTerminal": "切換終端機", + "nextTab": "下一個分頁", + "prevTab": "上一個分頁", + "reopenClosedTab": "恢復關閉的分頁", + "openFolder": "開啟資料夾", + "zoomIn": "放大", + "zoomOut": "縮小", + "zoomReset": "重設縮放", + "stopGenerating": "停止生成", + "openSettings": "設定" + }, + "gestures": { + "tap": "單擊", + "doubleTap": "雙擊", + "longPress": "長按", + "left": "左滑", + "right": "右滑", + "up": "上滑", + "upLeft": "左上滑", + "upRight": "右上滑", + "upThenLeft": "上滑後左滑", + "upThenRight": "上滑後右滑", + "upDown": "上滑後下滑", + "leftThenRight": "左滑後右滑", + "rightThenLeft": "右滑後左滑", + "upLeftThenDown": "左上滑後下滑", + "upRightThenDown": "右上滑後下滑" + } } } diff --git a/src/lib/appearance-script.ts b/src/lib/appearance-script.ts index 996bd35424..de1ce4836c 100644 --- a/src/lib/appearance-script.ts +++ b/src/lib/appearance-script.ts @@ -29,6 +29,12 @@ export const STORAGE_KEY_CHAT_ANIMATIONS = "codeg-chat-animations" // (48rem)。需预水合,避免会话首屏先按默认宽度排版再跳变。 export const STORAGE_KEY_CHAT_CONTENT_WIDTH = "codeg-chat-content-width" +// 悬浮球(手势入口)开关与位置。默认关闭:它是常驻在会话之上的控件,没有显式 +// 开启就不该出现。位置存相对视口左上角的 px,缺省表示停在右下角。两者都无需 +// 预水合——水合前根本不渲染球。 +export const STORAGE_KEY_FLOATING_BALL_ENABLED = "codeg-floating-ball-enabled" +export const STORAGE_KEY_FLOATING_BALL_POSITION = "codeg-floating-ball-position" + // 字体偏好(界面 / 编辑器 / 终端)。 // 只有界面字体需要 *_STACK(已解析的 CSS font-family 栈),供 inline 脚本零依赖地 // 预水合写入 --font-sans;编辑器/终端字体只走各自的 Monaco/xterm 选项,水合后才挂载, diff --git a/src/lib/floating-ball-gestures.test.ts b/src/lib/floating-ball-gestures.test.ts new file mode 100644 index 0000000000..b86a4cab0c --- /dev/null +++ b/src/lib/floating-ball-gestures.test.ts @@ -0,0 +1,183 @@ +import { describe, expect, it } from "vitest" + +import { + createFloatingBallTracker, + DEFAULT_FLOATING_BALL_BINDINGS, + FLOATING_BALL_GESTURES, + LONG_PRESS_MS, + MOVE_PX, + resolveTapGesture, + SWIPE_PX, + type FloatingBallGesture, +} from "./floating-ball-gestures" + +/** Walk a straight line in `n` samples, the way a finger reports positions. */ +function steps( + from: [number, number], + to: [number, number], + n = 8 +): [number, number][] { + const points: [number, number][] = [] + for (let i = 1; i <= n; i += 1) { + points.push([ + from[0] + ((to[0] - from[0]) * i) / n, + from[1] + ((to[1] - from[1]) * i) / n, + ]) + } + return points +} + +/** Run a trail of legs through the tracker and return what it names. */ +function trail(...legs: [number, number][]): FloatingBallGesture | null { + const points: [number, number][] = [[0, 0]] + for (const leg of legs) { + points.push(...steps(points[points.length - 1], leg)) + } + return run(points) +} + +function run(points: [number, number][]): FloatingBallGesture | null { + const tracker = createFloatingBallTracker() + tracker.start(points[0][0], points[0][1]) + for (const point of points.slice(1)) tracker.move(point[0], point[1]) + const last = points[points.length - 1] + return tracker.end(last[0], last[1]) +} + +const S = SWIPE_PX + +describe("single-stroke gestures", () => { + it("names each cardinal and upward diagonal", () => { + expect( + run([ + [0, 0], + [-(S + 20), 0], + ]) + ).toBe("left") + expect( + run([ + [0, 0], + [S + 20, 0], + ]) + ).toBe("right") + expect( + run([ + [0, 0], + [0, -(S + 20)], + ]) + ).toBe("up") + expect( + run([ + [0, 0], + [-(S + 10), -(S + 10)], + ]) + ).toBe("upLeft") + expect( + run([ + [0, 0], + [S + 10, -(S + 10)], + ]) + ).toBe("upRight") + }) + + it("leaves downward strokes unbound", () => { + expect( + run([ + [0, 0], + [0, S + 20], + ]) + ).toBeNull() + expect( + run([ + [0, 0], + [-(S + 10), S + 10], + ]) + ).toBeNull() + }) + + it("ignores movement below the threshold", () => { + expect( + run([ + [0, 0], + [MOVE_PX - 3, 3], + ]) + ).toBeNull() + }) + + it("needs one axis to dominate before naming a direction", () => { + // 45 across vs 20 down is 2.25x: clearly a horizontal stroke. + expect( + run([ + [0, 0], + [S + 15, 20], + ]) + ).toBe("right") + // 30 vs 20 is only 1.5x and neither axis reaches the diagonal threshold, + // so the stroke stays ambiguous and fires nothing rather than guessing. + expect( + run([ + [0, 0], + [S, 20], + ]) + ).toBeNull() + }) +}) + +describe("two-stage trails", () => { + it("folds a return stroke into its round-trip gesture", () => { + expect(trail([-50, 0], [50, 0])).toBe("leftThenRight") + expect(trail([50, 0], [-50, 0])).toBe("rightThenLeft") + expect(trail([0, -50], [0, 50])).toBe("upDown") + }) + + it("folds an upward start plus a turn", () => { + expect(trail([0, -50], [-60, -50])).toBe("upThenLeft") + expect(trail([0, -50], [60, -50])).toBe("upThenRight") + expect(trail([-40, -40], [-40, 40])).toBe("upLeftThenDown") + expect(trail([40, -40], [40, 40])).toBe("upRightThenDown") + }) + + it("does not read a horizontal round trip as a stop gesture", () => { + // Regression: summing displacement from the origin cancelled ←→ into a + // diagonal, which fired the stop binding. + expect(trail([-50, 0], [50, 0])).not.toBe("upRightThenDown") + }) + + it("records the stages it saw", () => { + const tracker = createFloatingBallTracker() + tracker.start(0, 0) + for (const point of steps([0, 0], [0, -60])) tracker.move(...point) + for (const point of steps([0, -60], [0, 60])) tracker.move(...point) + expect(tracker.trail).toEqual(["U", "D"]) + }) +}) + +describe("click-scale gestures", () => { + it("separates a tap, a double tap and a long press", () => { + expect(resolveTapGesture(80, null)).toBe("tap") + expect(resolveTapGesture(80, 120)).toBe("doubleTap") + expect(resolveTapGesture(LONG_PRESS_MS, null)).toBe("longPress") + expect(resolveTapGesture(LONG_PRESS_MS, 50)).toBe("longPress") + expect(resolveTapGesture(80, 400)).toBe("tap") + }) +}) + +describe("default bindings", () => { + it("binds every gesture, allowing deliberate gaps", () => { + const unbound = FLOATING_BALL_GESTURES.filter( + (gesture) => DEFAULT_FLOATING_BALL_BINDINGS[gesture] === null + ) + // The two long corner continuations are the only intentional gaps: a + // trail passing through a two-stage binding would otherwise fire twice. + expect(unbound.sort()).toEqual(["upLeftThenRight", "upRightThenLeft"]) + }) + + it("keeps the MindFS muscle memory for the headline gestures", () => { + expect(DEFAULT_FLOATING_BALL_BINDINGS.left).toBe("newConversation") + expect(DEFAULT_FLOATING_BALL_BINDINGS.tap).toBe("panel") + expect(DEFAULT_FLOATING_BALL_BINDINGS.longPress).toBe("openSettings") + expect(DEFAULT_FLOATING_BALL_BINDINGS.upRightThenDown).toBe( + "stopGenerating" + ) + }) +}) diff --git a/src/lib/floating-ball-gestures.ts b/src/lib/floating-ball-gestures.ts new file mode 100644 index 0000000000..288b64e528 --- /dev/null +++ b/src/lib/floating-ball-gestures.ts @@ -0,0 +1,296 @@ +/** + * Gesture vocabulary for the workspace floating ball. + * + * Pure — no DOM, no timers — so the recogniser can be exercised directly and + * the component above it stays a thin view. The vocabulary and thresholds are + * carried over from the MindFS blue-ring menu (`quickSwitch.ts`) so the muscle + * memory of that app transfers unchanged. + * + * Two families: + * - click-scale: tap / doubleTap / longPress, decided on duration alone. + * - path gestures: one-stroke swipes (left/right/up and the two upward + * diagonals), two-stage trails that start upward and turn, and round-trip + * trails that come back on themselves. + * + * Path tracking is anchor-based, not displacement-from-origin based: each time + * the direction changes, the anchor moves to the finger. Summing displacement + * from the origin makes a ↑ then ↓ trail cancel out into a single straight + * stroke, which is how the original implementation mistook "upDown" for a + * diagonal. + */ + +export type FloatingBallGesture = + | "tap" + | "doubleTap" + | "longPress" + | "left" + | "right" + | "up" + | "upLeft" + | "upRight" + | "upThenLeft" + | "upThenRight" + | "upLeftThenRight" + | "upRightThenLeft" + | "upRightThenDown" + | "upLeftThenDown" + | "upDown" + | "rightThenLeft" + | "leftThenRight" + +export type FloatingBallAction = + | "panel" + | "newConversation" + | "toggleSidebar" + | "toggleSearch" + | "toggleTerminal" + | "nextTab" + | "prevTab" + | "reopenClosedTab" + | "openFolder" + | "zoomIn" + | "zoomOut" + | "zoomReset" + | "stopGenerating" + | "openSettings" + +export const FLOATING_BALL_GESTURES: readonly FloatingBallGesture[] = [ + "tap", + "doubleTap", + "longPress", + "left", + "right", + "up", + "upLeft", + "upRight", + "upThenLeft", + "upThenRight", + "upLeftThenRight", + "upRightThenLeft", + "upRightThenDown", + "upLeftThenDown", + "upDown", + "rightThenLeft", + "leftThenRight", +] + +/** + * What each gesture does out of the box. + * + * `upLeftThenRight` and `upRightThenLeft` are deliberately unbound: the trails + * that reach them pass through the two-stage bindings above, so a stray + * continuation would fire a second action on the way. + */ +export const DEFAULT_FLOATING_BALL_BINDINGS: Readonly< + Record +> = { + tap: "panel", + doubleTap: "toggleTerminal", + longPress: "openSettings", + left: "newConversation", + right: "toggleSidebar", + up: "panel", + upLeft: "toggleSearch", + upRight: "nextTab", + upThenLeft: "prevTab", + upThenRight: "reopenClosedTab", + upLeftThenRight: null, + upRightThenLeft: null, + upRightThenDown: "stopGenerating", + upLeftThenDown: "zoomOut", + upDown: "openFolder", + rightThenLeft: "zoomReset", + leftThenRight: "zoomIn", +} + +/** A press held at least this long is a long press, not a tap. */ +export const LONG_PRESS_MS = 600 +/** A second press within this window of the first makes it a double tap. */ +export const DOUBLE_TAP_MS = 300 +/** + * Movement faster than this after touchdown is a gesture; slower is a drag of + * the ball itself. Both start with the same pointer, so time is what tells + * them apart. + */ +export const GESTURE_ONSET_MS = 250 +/** Below this displacement nothing counts as movement at all. */ +export const MOVE_PX = 8 +/** A single stroke must reach this far on an axis to name a direction. */ +export const SWIPE_PX = 30 +/** Reverse travel within one stage that starts a new stage. */ +export const SEGMENT_GAP_PX = 22 + +type Segment = "L" | "R" | "U" | "D" | "UL" | "UR" | "DL" | "DR" + +const OPPOSITE: Record = { + L: "R", + R: "L", + U: "D", + D: "U", + UL: "DR", + UR: "DL", + DL: "UR", + DR: "UL", +} + +function isOpposite(a: Segment | null, b: Segment): boolean { + return a !== null && OPPOSITE[a] === b +} + +/** + * Name the direction of one stage, or null while it is still too short. + * + * A stage that is long on both axes is a diagonal; otherwise the longer axis + * wins, and only when it dominates by 1.6x — a nearly square 30x28 stroke is + * still "up", not "upLeft". + */ +function classifySegment(dx: number, dy: number): Segment | null { + const adx = Math.abs(dx) + const ady = Math.abs(dy) + if (adx >= SWIPE_PX && ady >= SWIPE_PX) { + if (dy < 0) return dx < 0 ? "UL" : "UR" + return dx < 0 ? "DL" : "DR" + } + if (adx >= SWIPE_PX && adx > ady * 1.6) return dx < 0 ? "L" : "R" + if (ady >= SWIPE_PX && ady > adx * 1.6) return dy < 0 ? "U" : "D" + return null +} + +/** The last two stages of a trail, folded into the gesture they spell. */ +function foldTrail(path: readonly Segment[]): FloatingBallGesture | null { + if (path.length < 2) return null + const a = path[path.length - 2] + const b = path[path.length - 1] + if (a === "L" && b === "R") return "leftThenRight" + if (a === "R" && b === "L") return "rightThenLeft" + if (a === "U" && b === "D") return "upDown" + if (a === "U" && b === "L") return "upThenLeft" + if (a === "U" && b === "R") return "upThenRight" + if (a === "UL" && b === "R") return "upLeftThenRight" + if (a === "UR" && b === "L") return "upRightThenLeft" + if (a === "UR" && b === "D") return "upRightThenDown" + if (a === "UL" && b === "D") return "upLeftThenDown" + return null +} + +/** A single stage as a gesture. Downward strokes are unbound by default. */ +function singleStage(stage: Segment): FloatingBallGesture | null { + switch (stage) { + case "L": + return "left" + case "R": + return "right" + case "U": + return "up" + case "UL": + return "upLeft" + case "UR": + return "upRight" + default: + return null + } +} + +export interface FloatingBallTracker { + /** Touchdown. */ + start(x: number, y: number): void + /** Every pointer sample while the press is down. */ + move(x: number, y: number): void + /** Lift-off: the path gesture, or null when this press was not one. */ + end(x: number, y: number): FloatingBallGesture | null + /** Whether the press has travelled far enough to be a gesture at all. */ + readonly moved: boolean + /** Stages recognised so far, oldest first. Exposed for tests and debugging. */ + readonly trail: readonly Segment[] + reset(): void +} + +export function createFloatingBallTracker(): FloatingBallTracker { + let originX = 0 + let originY = 0 + let anchorX = 0 + let anchorY = 0 + let moved = false + let trail: Segment[] = [] + + const reset = () => { + originX = 0 + originY = 0 + anchorX = 0 + anchorY = 0 + moved = false + trail = [] + } + + return { + get moved() { + return moved + }, + get trail() { + return trail + }, + reset, + start(x, y) { + reset() + originX = x + originY = y + anchorX = x + anchorY = y + }, + move(x, y) { + const total = Math.hypot(x - originX, y - originY) + if (!moved && total <= MOVE_PX) return + moved = true + + const stage = classifySegment(x - anchorX, y - anchorY) + if (!stage) return + const last = trail.length > 0 ? trail[trail.length - 1] : null + if (stage === last) return + + if (isOpposite(last, stage) || trail.length === 0) { + // A reversal always starts a new stage; so does the first one. + trail.push(stage) + } else if (trail.length === 1 && foldTrail([trail[0], stage]) === null) { + // Jitter at the start of a stroke (U that drifts into UL) replaces the + // opening stage — unless the pair spells a binding, in which case the + // second stage must be kept or the gesture is lost. + trail[0] = stage + } else { + trail.push(stage) + } + // The next stage is measured from here, so it does not inherit the + // travel that already named this one. + anchorX = x + anchorY = y + }, + end(x, y) { + const dx = x - originX + const dy = y - originY + if (!moved || Math.hypot(dx, dy) <= MOVE_PX) return null + + const folded = foldTrail(trail) + if (folded) return folded + + if (trail.length === 1) return singleStage(trail[0]) + return null + }, + } +} + +/** + * Which click-scale gesture a completed press is. + * + * `msSinceLastTap` is the gap to the previous tap on the ball, or null when + * there is none pending. A press that produced a long press or a double tap + * clears that pending tap; a plain tap starts the window. + */ +export function resolveTapGesture( + heldMs: number, + msSinceLastTap: number | null +): "longPress" | "doubleTap" | "tap" { + if (heldMs >= LONG_PRESS_MS) return "longPress" + if (msSinceLastTap !== null && msSinceLastTap <= DOUBLE_TAP_MS) { + return "doubleTap" + } + return "tap" +} From 5455df4395b8d05e4d9959d20ede3a8a8472caac Mon Sep 17 00:00:00 2001 From: fengx Date: Sun, 18 Oct 2026 15:48:07 +0800 Subject: [PATCH 2/9] feat(layout): native floating ball with swipe gestures (conflict resolution cleanups) --- src/components/appearance-provider.tsx | 40 ------------- .../settings/appearance-settings.tsx | 60 +------------------ 2 files changed, 1 insertion(+), 99 deletions(-) diff --git a/src/components/appearance-provider.tsx b/src/components/appearance-provider.tsx index d33257b30c..0a69b71260 100644 --- a/src/components/appearance-provider.tsx +++ b/src/components/appearance-provider.tsx @@ -38,7 +38,6 @@ import { STORAGE_KEY_ZOOM_LEVEL, STORAGE_KEY_CHAT_ANIMATIONS, STORAGE_KEY_CHAT_CONTENT_WIDTH, - STORAGE_KEY_MESSAGE_NAV_POSITION, STORAGE_KEY_FLOATING_BALL_ENABLED, STORAGE_KEY_FLOATING_BALL_POSITION, STORAGE_KEY_WELCOME_QUICK_ACTIONS, @@ -119,12 +118,6 @@ function syncAppearanceMode(mode: string) { export type FontSelection = { id: string; custom: string } -/** 消息导航卡位置。`start` = 逻辑起始边(LTR 左上,历史行为); - * `bottom-end` = 右下角紧凑卡片。 */ -export type MessageNavPosition = "start" | "bottom-end" - -export const DEFAULT_MESSAGE_NAV_POSITION: MessageNavPosition = "start" - /** 悬浮球停靠位置(视口坐标 px,左上角原点)。 */ export type FloatingBallPosition = { x: number; y: number } @@ -142,9 +135,6 @@ type AppearanceContextValue = { /** 聊天区域动画总开关,默认开启;关闭时 带 data-chat-animations="off" */ chatAnimations: boolean setChatAnimations: (on: boolean) => void - /** 消息导航卡位置:起始边(默认,左上)或右下角紧凑卡片。 */ - messageNavPosition: MessageNavPosition - setMessageNavPosition: (position: MessageNavPosition) => void /** 悬浮球(手势入口)是否显示,默认关闭。 */ floatingBallEnabled: boolean setFloatingBallEnabled: (on: boolean) => void @@ -301,18 +291,6 @@ function readBool(key: string, def: boolean): boolean { } } -function readMessageNavPosition(): MessageNavPosition { - if (typeof document === "undefined") return DEFAULT_MESSAGE_NAV_POSITION - try { - return localStorage.getItem(STORAGE_KEY_MESSAGE_NAV_POSITION) === - "bottom-end" - ? "bottom-end" - : DEFAULT_MESSAGE_NAV_POSITION - } catch { - return DEFAULT_MESSAGE_NAV_POSITION - } -} - function readFloatingBallPosition(): FloatingBallPosition | null { if (typeof document === "undefined") return null try { @@ -476,10 +454,6 @@ export function AppearanceProvider({ } }, [chatAnimations]) - // 消息导航卡位置。默认起始边(历史行为),仅在用户显式选右下角时才变。 - const [messageNavPosition, setMessageNavPositionState] = - useState(() => readMessageNavPosition()) - // 悬浮球:默认不显示,显式开启后才挂载;位置缺省为右下角。 const [floatingBallEnabled, setFloatingBallEnabledState] = useState( () => readBool(STORAGE_KEY_FLOATING_BALL_ENABLED, false) @@ -651,14 +625,6 @@ export function AppearanceProvider({ persist(STORAGE_KEY_CHAT_ANIMATIONS, on ? "1" : "0") }, []) - const setMessageNavPosition = useCallback((position: MessageNavPosition) => { - setMessageNavPositionState(position) - persist( - STORAGE_KEY_MESSAGE_NAV_POSITION, - position === "bottom-end" ? "bottom-end" : "start" - ) - }, []) - const setFloatingBallEnabled = useCallback((on: boolean) => { setFloatingBallEnabledState(on) persist(STORAGE_KEY_FLOATING_BALL_ENABLED, on ? "1" : "0") @@ -1134,10 +1100,6 @@ export function AppearanceProvider({ if (e.key === STORAGE_KEY_CHAT_ANIMATIONS) { setChatAnimationsState(readBool(STORAGE_KEY_CHAT_ANIMATIONS, true)) } - // 外观设置是独立窗口:那边改了导航卡位置,工作区窗口要跟着搬。 - if (e.key === STORAGE_KEY_MESSAGE_NAV_POSITION) { - setMessageNavPositionState(readMessageNavPosition()) - } // 悬浮球同样在外观设置里开关,那边的改动要立刻反映到工作区窗口。 if (e.key === STORAGE_KEY_FLOATING_BALL_ENABLED) { setFloatingBallEnabledState( @@ -1253,8 +1215,6 @@ export function AppearanceProvider({ setShowWelcomeQuickActions, chatAnimations, setChatAnimations, - messageNavPosition, - setMessageNavPosition, floatingBallEnabled, setFloatingBallEnabled, floatingBallPosition, diff --git a/src/components/settings/appearance-settings.tsx b/src/components/settings/appearance-settings.tsx index 024688263c..b56653fbdc 100644 --- a/src/components/settings/appearance-settings.tsx +++ b/src/components/settings/appearance-settings.tsx @@ -303,63 +303,6 @@ export function AppearanceSettings() {
-

- {t("floatingBall.sectionDescription")} -

- - - -
-

- {t("floatingBall.hint")} -

- -
- - {/* ===== Chat content width ===== */} -

- {t("messageNav.sectionDescription")} -

- -
- -

- {t("messageNav.hint")} -

-
- {/* ===== Floating ball ===== */}
@@ -397,8 +340,7 @@ export function AppearanceSettings() {
- {/* ===== Chat content width ===== */}{" "} ->>>>>>> aebc706f (feat(layout): native floating ball with swipe gestures) + {/* ===== Chat content width ===== */}
From bb6dfcf20ce69b66c1e8688e7de90bf7cde9bbd6 Mon Sep 17 00:00:00 2001 From: fengx Date: Sat, 17 Oct 2026 19:39:46 +0800 Subject: [PATCH 3/9] test(layout): cover the floating ball's interactions Adds a component suite for the ball (hidden until enabled, tap/double tap/long press, swipe and two-stage trails, drag persistence, Escape) and stubs the providers it now reaches in the chrome-controller test, which was failing to import the ACP context through the new imports. jsdom has no PointerEvent coordinates, so the helpers dispatch MouseEvents carrying the pointer event's type instead. --- src/components/layout/floating-ball.test.tsx | 203 ++++++++++++++++++ .../workspace-chrome-controller.test.tsx | 15 ++ 2 files changed, 218 insertions(+) create mode 100644 src/components/layout/floating-ball.test.tsx diff --git a/src/components/layout/floating-ball.test.tsx b/src/components/layout/floating-ball.test.tsx new file mode 100644 index 0000000000..828520f9aa --- /dev/null +++ b/src/components/layout/floating-ball.test.tsx @@ -0,0 +1,203 @@ +import { act, cleanup, fireEvent, render, screen } from "@testing-library/react" +import { NextIntlClientProvider } from "next-intl" +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" + +import enMessages from "@/i18n/messages/en.json" + +const appearance = vi.hoisted(() => ({ + enabled: false, + position: null as { x: number; y: number } | null, + setPosition: vi.fn(), +})) + +vi.mock("@/hooks/use-appearance", () => ({ + useFloatingBall: () => ({ + floatingBallEnabled: appearance.enabled, + setFloatingBallEnabled: vi.fn(), + floatingBallPosition: appearance.position, + setFloatingBallPosition: appearance.setPosition, + }), +})) + +import { FloatingBall } from "./floating-ball" + +function renderBall(onAction = vi.fn()) { + render( + + + + ) + return onAction +} + +/** + * jsdom implements neither `PointerEvent` nor coordinates on it, so + * `fireEvent.pointerDown(el, { clientX })` arrives with nulls. A `MouseEvent` + * carrying the pointer event's type does reach React's handler with real + * coordinates, which is all the component reads. + */ +function pointer( + el: Element, + type: "pointerdown" | "pointermove" | "pointerup", + x: number, + y: number, + pointerId = 1 +) { + const event = new MouseEvent(type, { + bubbles: true, + cancelable: true, + clientX: x, + clientY: y, + button: 0, + }) + Object.defineProperty(event, "pointerId", { value: pointerId }) + Object.defineProperty(event, "pointerType", { value: "touch" }) + fireEvent(el, event) +} + +/** A press that never moves, held for `heldMs`. */ +function tap(heldMs = 50) { + const ball = screen.getByLabelText("Floating ball") + pointer(ball, "pointerdown", 100, 100) + vi.advanceTimersByTime(heldMs) + pointer(ball, "pointerup", 100, 100) + return ball +} + +/** A press that moves in a straight line and releases. */ +function swipe(from: [number, number], to: [number, number]) { + const ball = screen.getByLabelText("Floating ball") + pointer(ball, "pointerdown", from[0], from[1]) + // Within GESTURE_ONSET_MS, so this is a gesture and not a drag. + pointer( + ball, + "pointermove", + from[0] + (to[0] - from[0]) / 2, + from[1] + (to[1] - from[1]) / 2 + ) + pointer(ball, "pointermove", to[0], to[1]) + pointer(ball, "pointerup", to[0], to[1]) +} + +beforeEach(() => { + localStorage.clear() + appearance.enabled = false + appearance.position = null + appearance.setPosition.mockClear() + vi.useFakeTimers() +}) + +afterEach(() => { + vi.useRealTimers() + cleanup() +}) + +describe("FloatingBall", () => { + it("renders nothing until the preference is enabled", () => { + renderBall() + expect(screen.queryByLabelText("Floating ball")).toBeNull() + }) + + it("shows the ball when enabled", () => { + appearance.enabled = true + renderBall() + expect(screen.getByLabelText("Floating ball")).toBeInTheDocument() + }) + + it("opens the action panel on a single tap, after the double-tap window", () => { + appearance.enabled = true + const onAction = renderBall() + tap() + // Deferred so a double tap does not flash the panel first. + expect(screen.queryByRole("dialog")).toBeNull() + act(() => { + vi.advanceTimersByTime(300) + }) + expect(screen.getByRole("dialog")).toBeInTheDocument() + expect(screen.getByText("New conversation")).toBeInTheDocument() + expect(onAction).not.toHaveBeenCalled() + }) + + it("treats a second tap inside the window as a double tap", () => { + appearance.enabled = true + const onAction = renderBall() + tap() + act(() => { + vi.advanceTimersByTime(100) + }) + tap() + expect(onAction).toHaveBeenCalledWith("toggleTerminal") + // The pending single tap must not fire as well. + act(() => { + vi.advanceTimersByTime(500) + }) + expect(onAction).toHaveBeenCalledTimes(1) + }) + + it("opens the in-ball settings on a long press", () => { + appearance.enabled = true + const onAction = renderBall() + tap(700) + expect(screen.getByRole("dialog")).toBeInTheDocument() + expect(screen.getByText("Reset position")).toBeInTheDocument() + expect(onAction).not.toHaveBeenCalled() + }) + + it("fires the bound action for a swipe", () => { + appearance.enabled = true + const onAction = renderBall() + // Left = new conversation. + swipe([200, 300], [120, 300]) + expect(onAction).toHaveBeenCalledWith("newConversation") + }) + + it("recognises a two-stage trail", () => { + appearance.enabled = true + const onAction = renderBall() + const ball = screen.getByLabelText("Floating ball") + pointer(ball, "pointerdown", 200, 400) + pointer(ball, "pointermove", 200, 360) + pointer(ball, "pointermove", 200, 320) + pointer(ball, "pointermove", 200, 400) + pointer(ball, "pointermove", 200, 460) + pointer(ball, "pointerup", 200, 460) + // Up, then down = open folder. + expect(onAction).toHaveBeenCalledWith("openFolder") + }) + + it("stores the new position after a drag", () => { + appearance.enabled = true + renderBall() + const ball = screen.getByLabelText("Floating ball") as HTMLElement + // The ball's own corner is what moves, not the pointer's, so the assertion + // is on the delta from where it started. + const startX = parseFloat(ball.style.left) + const startY = parseFloat(ball.style.top) + pointer(ball, "pointerdown", 300, 500) + // Moving only after the onset window makes this a repositioning drag. + act(() => { + vi.advanceTimersByTime(400) + }) + pointer(ball, "pointermove", 260, 440) + pointer(ball, "pointerup", 260, 440) + expect(appearance.setPosition).toHaveBeenCalled() + const stored = appearance.setPosition.mock.calls[0][0] as { + x: number + y: number + } + expect(stored.x).toBe(startX - 40) + expect(stored.y).toBe(startY - 60) + }) + + it("closes the panel on Escape", () => { + appearance.enabled = true + renderBall() + tap() + act(() => { + vi.advanceTimersByTime(300) + }) + expect(screen.getByRole("dialog")).toBeInTheDocument() + fireEvent.keyDown(document, { key: "Escape" }) + expect(screen.queryByRole("dialog")).toBeNull() + }) +}) diff --git a/src/components/layout/workspace-chrome-controller.test.tsx b/src/components/layout/workspace-chrome-controller.test.tsx index 6805b13ffa..5b1a377fb2 100644 --- a/src/components/layout/workspace-chrome-controller.test.tsx +++ b/src/components/layout/workspace-chrome-controller.test.tsx @@ -39,6 +39,21 @@ const spies = vi.hoisted(() => ({ })) vi.mock("@/lib/api", () => ({ openSettingsWindow: vi.fn() })) +// The floating ball reads the active ACP connection (to stop a turn) and the +// zoom level. Both pull provider modules this test does not exercise, and the +// ACP context imports the workspace store in a shape the stub below does not +// provide; the ⌘W behaviour under test touches neither. +vi.mock("@/contexts/acp-connections-context", () => ({ + useAcpActions: () => ({ cancel: vi.fn() }), + // Not a zustand store: it returns the context value directly. + useConnectionStore: () => ({ getActiveKey: () => null }), +})) +vi.mock("@/hooks/use-appearance", () => ({ + useZoomLevel: () => ({ zoomLevel: 100, setZoomLevel: vi.fn() }), +})) +vi.mock("@/components/layout/floating-ball", () => ({ + FloatingBall: () => null, +})) vi.mock("@/contexts/active-folder-context", () => ({ useActiveFolder: () => ({ activeFolder: { id: 1, path: "/repo" } }), })) From 0c06445d2c0c27b75469bc426c11915b5af83e88 Mon Sep 17 00:00:00 2001 From: fengx Date: Sat, 17 Oct 2026 20:16:58 +0800 Subject: [PATCH 4/9] fix(layout): keep the floating ball clear of the composer and status bar The bottom-right default landed on the status bar's MCP light and alerts badge (measured at 390x844 and 1280x800), and the composer's send button sits just above it. Default to the right edge at 55% height, the one band empty on both phone and desktop, and cap the panel height to the space left on its side so the action list scrolls instead of running off-screen. --- src/components/layout/floating-ball.test.tsx | 14 +++++++++ src/components/layout/floating-ball.tsx | 30 +++++++++++++++++--- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/components/layout/floating-ball.test.tsx b/src/components/layout/floating-ball.test.tsx index 828520f9aa..7d9eff38fe 100644 --- a/src/components/layout/floating-ball.test.tsx +++ b/src/components/layout/floating-ball.test.tsx @@ -104,6 +104,20 @@ describe("FloatingBall", () => { expect(screen.getByLabelText("Floating ball")).toBeInTheDocument() }) + it("defaults to the right edge, clear of the composer and status bar", () => { + appearance.enabled = true + renderBall() + const ball = screen.getByLabelText("Floating ball") as HTMLElement + const x = parseFloat(ball.style.left) + const y = parseFloat(ball.style.top) + // Flush to the trailing edge... + expect(x).toBe(window.innerWidth - 46 - 18) + // ...but well above the bottom, where the composer's send button and the + // status bar's alerts/MCP badges sit. + expect(y).toBe(Math.round(window.innerHeight * 0.55)) + expect(y).toBeLessThan(window.innerHeight - 46 - 18) + }) + it("opens the action panel on a single tap, after the double-tap window", () => { appearance.enabled = true const onAction = renderBall() diff --git a/src/components/layout/floating-ball.tsx b/src/components/layout/floating-ball.tsx index 990e96b57b..84aedcfe59 100644 --- a/src/components/layout/floating-ball.tsx +++ b/src/components/layout/floating-ball.tsx @@ -78,13 +78,25 @@ function clampPosition( } } +/** + * Where a freshly enabled ball lands: the right edge, a little below the + * vertical middle. + * + * The bottom-right corner is not free in this workspace. The composer's send + * button occupies the corner above the status bar, and the status bar itself + * carries the MCP health light and the alerts badge — measured at 390×844 and + * 1280×800, both covered by the old corner default. The right edge around 55% + * is the one band that is empty on both phone and desktop. + */ +const DEFAULT_Y_RATIO = 0.55 + function defaultPosition(viewport: { width: number height: number }): FloatingBallPosition { return { x: viewport.width - BALL_SIZE - FLOATING_BALL_INSET, - y: viewport.height - BALL_SIZE - FLOATING_BALL_INSET, + y: Math.round(viewport.height * DEFAULT_Y_RATIO), } } @@ -346,10 +358,18 @@ export function FloatingBall({ onAction }: FloatingBallProps) { if (!floatingBallEnabled) return null // The panel opens toward the middle of the screen: above the ball when it - // sits in the lower half, below it otherwise. + // sits in the lower half, below it otherwise. Its height is capped to the + // space actually left on that side, so a long action list scrolls instead of + // pushing the header off-screen. const opensUpward = position.y > viewport.height / 2 const panelBottom = opensUpward ? viewport.height - position.y + 8 : undefined const panelTop = opensUpward ? undefined : position.y + BALL_SIZE + 8 + const panelMaxHeight = Math.max( + 120, + (opensUpward + ? position.y - 8 + : viewport.height - position.y - BALL_SIZE - 8) - 8 + ) const panelLeft = Math.min( Math.max(8, position.x + BALL_SIZE - PANEL_WIDTH), Math.max(8, viewport.width - PANEL_WIDTH - 8) @@ -396,9 +416,10 @@ export function FloatingBall({ onAction }: FloatingBallProps) { top: panelTop, bottom: panelBottom, width: PANEL_WIDTH, + maxHeight: panelMaxHeight, }} className={cn( - "fixed z-40 flex max-h-[60vh] flex-col overflow-hidden rounded-xl", + "fixed z-40 flex flex-col overflow-hidden rounded-xl", "border bg-popover text-popover-foreground shadow-2xl" )} > @@ -441,9 +462,10 @@ export function FloatingBall({ onAction }: FloatingBallProps) { top: panelTop, bottom: panelBottom, width: PANEL_WIDTH, + maxHeight: panelMaxHeight, }} className={cn( - "fixed z-40 flex max-h-[60vh] flex-col overflow-hidden rounded-xl", + "fixed z-40 flex flex-col overflow-hidden rounded-xl", "border bg-popover text-popover-foreground shadow-2xl" )} > From a8a194046dbe96015cd86311587907739a72106c Mon Sep 17 00:00:00 2001 From: fengx Date: Sat, 17 Oct 2026 23:56:40 +0800 Subject: [PATCH 5/9] =?UTF-8?q?feat(floating-ball):=20batch=20v82=20P0=20?= =?UTF-8?q?=E2=80=94=20expand=20gesture=20vocabulary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 12 new path gestures: down family single strokes (down/downLeft/downRight now first-class) and the compound pairs from the owner's list (downUp, upThenDownRight, downThenUpRight, rightThenUp/Down, downThenRight/Left, downRightThenUp, downLeftThenUp) - foldTrail gains all 9 missing folds: without them the jitter-replacement rule silently turned 8 of the 12 requested compounds into other gestures - end() no longer requires origin displacement: a round trip drawn cleanly back to its start used to be dropped - 10 new actions in the vocabulary (todos/canvas/agents/rename/cycle...) with default bindings; stopGenerating stays on an inconspicuous compound - tests: 13 passing, downward-stroke semantics reversed per new table --- src/lib/floating-ball-gestures.test.ts | 61 ++++++++++++++++++-- src/lib/floating-ball-gestures.ts | 77 ++++++++++++++++++++++++-- 2 files changed, 128 insertions(+), 10 deletions(-) diff --git a/src/lib/floating-ball-gestures.test.ts b/src/lib/floating-ball-gestures.test.ts index b86a4cab0c..3b0f5345bc 100644 --- a/src/lib/floating-ball-gestures.test.ts +++ b/src/lib/floating-ball-gestures.test.ts @@ -80,19 +80,27 @@ describe("single-stroke gestures", () => { ).toBe("upRight") }) - it("leaves downward strokes unbound", () => { + it("names downward strokes as first-class gestures", () => { + // Batch v82: down/downLeft/downRight are recognisable now (binding them is + // the bindings table's job, not the recogniser's). expect( run([ [0, 0], [0, S + 20], ]) - ).toBeNull() + ).toBe("down") expect( run([ [0, 0], [-(S + 10), S + 10], ]) - ).toBeNull() + ).toBe("downLeft") + expect( + run([ + [0, 0], + [S + 10, S + 10], + ]) + ).toBe("downRight") }) it("ignores movement below the threshold", () => { @@ -128,6 +136,7 @@ describe("two-stage trails", () => { expect(trail([-50, 0], [50, 0])).toBe("leftThenRight") expect(trail([50, 0], [-50, 0])).toBe("rightThenLeft") expect(trail([0, -50], [0, 50])).toBe("upDown") + expect(trail([0, 50], [0, -50])).toBe("downUp") }) it("folds an upward start plus a turn", () => { @@ -135,6 +144,38 @@ describe("two-stage trails", () => { expect(trail([0, -50], [60, -50])).toBe("upThenRight") expect(trail([-40, -40], [-40, 40])).toBe("upLeftThenDown") expect(trail([40, -40], [40, 40])).toBe("upRightThenDown") + expect(trail([0, -50], [(S + 10), (S + 10)])).toBe("upThenDownRight") + }) + + it("folds the batch-v82 downward and horizontal starts", () => { + // Each of these pairs previously fell through the jitter-replacement rule + // and silently became a different single-stage gesture. + expect(trail([0, 50], [S + 10, -(S + 10)])).toBe("downThenUpRight") + expect(trail([50, 0], [50, -(S + 20)])).toBe("rightThenUp") + expect(trail([50, 0], [50, S + 20])).toBe("rightThenDown") + expect(trail([0, 50], [S + 20, 50])).toBe("downThenRight") + expect(trail([0, 50], [-(S + 20), 50])).toBe("downThenLeft") + expect(trail([S + 10, S + 10], [S + 10, -(S + 20)])).toBe( + "downRightThenUp" + ) + expect(trail([-(S + 10), S + 10], [-(S + 10), -(S + 20)])).toBe( + "downLeftThenUp" + ) + }) + + it("keeps a round trip whose finger lands back at the origin", () => { + // Regression (batch v82): end() used to require displacement from the + // origin, which dropped a ↓↑ drawn cleanly back to its start. + const points: [number, number][] = [ + ...steps([0, 0], [0, 60]), + ...steps([0, 60], [0, 0]), + ] + expect(run(points)).toBe("downUp") + const upDown: [number, number][] = [ + ...steps([0, 0], [0, -60]), + ...steps([0, -60], [0, 0]), + ] + expect(run(upDown)).toBe("upDown") }) it("does not read a horizontal round trip as a stop gesture", () => { @@ -167,9 +208,17 @@ describe("default bindings", () => { const unbound = FLOATING_BALL_GESTURES.filter( (gesture) => DEFAULT_FLOATING_BALL_BINDINGS[gesture] === null ) - // The two long corner continuations are the only intentional gaps: a - // trail passing through a two-stage binding would otherwise fire twice. - expect(unbound.sort()).toEqual(["upLeftThenRight", "upRightThenLeft"]) + // The long corner continuations are the only intentional gaps: a trail + // passing through a two-stage binding would otherwise fire twice. The + // straight down family stays recognisable but unbound by default — the + // settings UI lets owners bind them. + expect(unbound.sort()).toEqual([ + "down", + "downLeft", + "downRight", + "upLeftThenRight", + "upRightThenLeft", + ]) }) it("keeps the MindFS muscle memory for the headline gestures", () => { diff --git a/src/lib/floating-ball-gestures.ts b/src/lib/floating-ball-gestures.ts index 288b64e528..6e58f98522 100644 --- a/src/lib/floating-ball-gestures.ts +++ b/src/lib/floating-ball-gestures.ts @@ -26,8 +26,11 @@ export type FloatingBallGesture = | "left" | "right" | "up" + | "down" | "upLeft" | "upRight" + | "downLeft" + | "downRight" | "upThenLeft" | "upThenRight" | "upLeftThenRight" @@ -35,6 +38,15 @@ export type FloatingBallGesture = | "upRightThenDown" | "upLeftThenDown" | "upDown" + | "downUp" + | "upThenDownRight" + | "downThenUpRight" + | "rightThenUp" + | "rightThenDown" + | "downThenRight" + | "downThenLeft" + | "downRightThenUp" + | "downLeftThenUp" | "rightThenLeft" | "leftThenRight" @@ -53,6 +65,16 @@ export type FloatingBallAction = | "zoomReset" | "stopGenerating" | "openSettings" + | "openTodos" + | "openTodosNew" + | "openAutomations" + | "openCanvas" + | "openAgents" + | "openSystemAdmin" + | "renameConversation" + | "openSessionFolder" + | "cycleAgent" + | "cycleFavoriteAgent" export const FLOATING_BALL_GESTURES: readonly FloatingBallGesture[] = [ "tap", @@ -61,8 +83,11 @@ export const FLOATING_BALL_GESTURES: readonly FloatingBallGesture[] = [ "left", "right", "up", + "down", "upLeft", "upRight", + "downLeft", + "downRight", "upThenLeft", "upThenRight", "upLeftThenRight", @@ -70,6 +95,15 @@ export const FLOATING_BALL_GESTURES: readonly FloatingBallGesture[] = [ "upRightThenDown", "upLeftThenDown", "upDown", + "downUp", + "upThenDownRight", + "downThenUpRight", + "rightThenUp", + "rightThenDown", + "downThenRight", + "downThenLeft", + "downRightThenUp", + "downLeftThenUp", "rightThenLeft", "leftThenRight", ] @@ -80,6 +114,11 @@ export const FLOATING_BALL_GESTURES: readonly FloatingBallGesture[] = [ * `upLeftThenRight` and `upRightThenLeft` are deliberately unbound: the trails * that reach them pass through the two-stage bindings above, so a stray * continuation would fire a second action on the way. + * + * The ten batch-v82 actions spread across the new downward / compound strokes; + * `stopGenerating` stays on an inconspicuous compound on purpose — a gesture + * that aborts a running answer must not live next to the ones fingers find + * first. */ export const DEFAULT_FLOATING_BALL_BINDINGS: Readonly< Record @@ -90,8 +129,11 @@ export const DEFAULT_FLOATING_BALL_BINDINGS: Readonly< left: "newConversation", right: "toggleSidebar", up: "panel", + down: null, upLeft: "toggleSearch", upRight: "nextTab", + downLeft: null, + downRight: null, upThenLeft: "prevTab", upThenRight: "reopenClosedTab", upLeftThenRight: null, @@ -99,6 +141,15 @@ export const DEFAULT_FLOATING_BALL_BINDINGS: Readonly< upRightThenDown: "stopGenerating", upLeftThenDown: "zoomOut", upDown: "openFolder", + downUp: "cycleAgent", + upThenDownRight: "openCanvas", + downThenUpRight: "cycleFavoriteAgent", + rightThenUp: "openTodos", + rightThenDown: "openTodosNew", + downThenRight: "openAutomations", + downThenLeft: "openSessionFolder", + downRightThenUp: "openAgents", + downLeftThenUp: "openSystemAdmin", rightThenLeft: "zoomReset", leftThenRight: "zoomIn", } @@ -164,8 +215,17 @@ function foldTrail(path: readonly Segment[]): FloatingBallGesture | null { if (a === "L" && b === "R") return "leftThenRight" if (a === "R" && b === "L") return "rightThenLeft" if (a === "U" && b === "D") return "upDown" + if (a === "D" && b === "U") return "downUp" if (a === "U" && b === "L") return "upThenLeft" if (a === "U" && b === "R") return "upThenRight" + if (a === "U" && b === "DR") return "upThenDownRight" + if (a === "D" && b === "UR") return "downThenUpRight" + if (a === "R" && b === "U") return "rightThenUp" + if (a === "R" && b === "D") return "rightThenDown" + if (a === "D" && b === "R") return "downThenRight" + if (a === "D" && b === "L") return "downThenLeft" + if (a === "DR" && b === "U") return "downRightThenUp" + if (a === "DL" && b === "U") return "downLeftThenUp" if (a === "UL" && b === "R") return "upLeftThenRight" if (a === "UR" && b === "L") return "upRightThenLeft" if (a === "UR" && b === "D") return "upRightThenDown" @@ -173,7 +233,7 @@ function foldTrail(path: readonly Segment[]): FloatingBallGesture | null { return null } -/** A single stage as a gesture. Downward strokes are unbound by default. */ +/** A single stage as a gesture. Downward strokes are bound via the table. */ function singleStage(stage: Segment): FloatingBallGesture | null { switch (stage) { case "L": @@ -182,10 +242,16 @@ function singleStage(stage: Segment): FloatingBallGesture | null { return "right" case "U": return "up" + case "D": + return "down" case "UL": return "upLeft" case "UR": return "upRight" + case "DL": + return "downLeft" + case "DR": + return "downRight" default: return null } @@ -264,9 +330,12 @@ export function createFloatingBallTracker(): FloatingBallTracker { anchorY = y }, end(x, y) { - const dx = x - originX - const dy = y - originY - if (!moved || Math.hypot(dx, dy) <= MOVE_PX) return null + if (!moved) return null + // A completed trail is judged on its stages, not on how far the finger + // ended from the origin: round trips (↓↑, ↑↓) naturally land back where + // they started, and an origin-displacement check would drop precisely + // the strokes drawn most cleanly. + if (trail.length === 0) return null const folded = foldTrail(trail) if (folded) return folded From 1c8c25072ed426a9b636b70762df1a66a8835d82 Mon Sep 17 00:00:00 2001 From: fengx Date: Sun, 18 Oct 2026 00:25:04 +0800 Subject: [PATCH 6/9] =?UTF-8?q?feat(floating-ball):=20batch=20v82=20P1+P2+?= =?UTF-8?q?P2b=20=E2=80=94=20configurable=20bindings,=20gesture=20hint=20p?= =?UTF-8?q?ill,=20three=20ball=20sizes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bindings overrides: per-gesture action map in appearance settings (null = user-cleared, missing key = default), persisted per window, cross-window synced; fire() and the cheat sheet read the override - gesture feedback: the ball follows the finger during a swipe and a hint pill names the action the trail would fire (previewGesture in the engine, so pill and commit can never disagree); snap-back via transform - three size presets (small 36 / medium 40 / large 46, default large) with icon scaling, plus a 6px transparent hit-slop halo so the small preset still meets the 44px touch minimum - settings UI: size select, per-gesture binding rows, favorite-agent chips (cap 3) — all under the existing floating-ball section - i18n: 12 new gesture names + 10 new action names + settings copy across all ten locales --- src/components/appearance-provider.tsx | 127 ++++++++++ src/components/layout/floating-ball.test.tsx | 9 + src/components/layout/floating-ball.tsx | 211 +++++++++++++---- .../settings/appearance-settings.tsx | 216 ++++++++++++++++++ src/hooks/use-appearance.ts | 8 + src/i18n/messages/ar.json | 99 +++++++- src/i18n/messages/de.json | 99 +++++++- src/i18n/messages/en.json | 99 +++++++- src/i18n/messages/es.json | 99 +++++++- src/i18n/messages/fr.json | 99 +++++++- src/i18n/messages/ja.json | 99 +++++++- src/i18n/messages/ko.json | 99 +++++++- src/i18n/messages/pt.json | 99 +++++++- src/i18n/messages/zh-CN.json | 99 +++++++- src/i18n/messages/zh-TW.json | 99 +++++++- src/lib/appearance-script.ts | 6 + src/lib/floating-ball-gestures.test.ts | 9 +- src/lib/floating-ball-gestures.ts | 19 +- 18 files changed, 1503 insertions(+), 92 deletions(-) diff --git a/src/components/appearance-provider.tsx b/src/components/appearance-provider.tsx index 0a69b71260..654edbd4fe 100644 --- a/src/components/appearance-provider.tsx +++ b/src/components/appearance-provider.tsx @@ -40,6 +40,9 @@ import { STORAGE_KEY_CHAT_CONTENT_WIDTH, STORAGE_KEY_FLOATING_BALL_ENABLED, STORAGE_KEY_FLOATING_BALL_POSITION, + STORAGE_KEY_FLOATING_BALL_BINDINGS, + STORAGE_KEY_FLOATING_BALL_SIZE, + STORAGE_KEY_FLOATING_BALL_FAVORITE_AGENTS, STORAGE_KEY_WELCOME_QUICK_ACTIONS, STORAGE_KEY_UI_FONT, STORAGE_KEY_UI_FONT_CUSTOM, @@ -124,6 +127,29 @@ export type FloatingBallPosition = { x: number; y: number } /** 缺省停靠:右下角,距右边与底边各 18px(底边让出安全区)。 */ export const FLOATING_BALL_INSET = 18 +/** + * 手势→动作绑定覆盖表。value 为 null 表示用户显式清除该手势(必须与"缺 key + * 回退默认"区分,否则清除功能失效);未出现的 key 一律取当前默认表。 + */ +export type FloatingBallBindingsOverride = Partial< + Record +> + +/** 球体三档大小:默认大(46px),中 40、小 36。 */ +export type FloatingBallSize = "small" | "medium" | "large" + +export const FLOATING_BALL_SIZE_PX: Record = { + small: 36, + medium: 40, + large: 46, +} + +export const DEFAULT_FLOATING_BALL_SIZE: FloatingBallSize = "large" + +function isFloatingBallSize(v: unknown): v is FloatingBallSize { + return v === "small" || v === "medium" || v === "large" +} + type AppearanceContextValue = { themeColor: ThemeColor setThemeColor: (color: ThemeColor) => void @@ -141,6 +167,15 @@ type AppearanceContextValue = { /** 悬浮球停靠位置;null = 右下角缺省位。 */ floatingBallPosition: FloatingBallPosition | null setFloatingBallPosition: (position: FloatingBallPosition | null) => void + /** 手势绑定覆盖表(null = 用户清除);缺 key 回退默认表。 */ + floatingBallBindings: FloatingBallBindingsOverride + setFloatingBallBindings: (bindings: FloatingBallBindingsOverride) => void + /** 球体三档大小。 */ + floatingBallSize: FloatingBallSize + setFloatingBallSize: (size: FloatingBallSize) => void + /** 常用 agent 快切列表(agentType 数组,2-3 个)。 */ + floatingBallFavoriteAgents: string[] + setFloatingBallFavoriteAgents: (agents: string[]) => void /** * 聊天内容宽度(100% 缩放下的 px,落到 CSS 时换成 rem,随缩放变化), * null = 内置默认(48rem)。拖拽手柄与外观设置页共用;set 会持久化并落到 @@ -313,6 +348,47 @@ function readFloatingBallPosition(): FloatingBallPosition | null { } } +function readFloatingBallBindings(): FloatingBallBindingsOverride { + if (typeof document === "undefined") return {} + try { + const raw = localStorage.getItem(STORAGE_KEY_FLOATING_BALL_BINDINGS) + if (!raw) return {} + const parsed: unknown = JSON.parse(raw) + if (typeof parsed !== "object" || parsed === null) return {} + const out: FloatingBallBindingsOverride = {} + for (const [k, v] of Object.entries(parsed as Record)) { + // null survives on purpose: it means "user cleared this gesture". + if (v === null || typeof v === "string") out[k] = v + } + return out + } catch { + return {} + } +} + +function readFloatingBallSize(): FloatingBallSize { + if (typeof document === "undefined") return DEFAULT_FLOATING_BALL_SIZE + try { + const raw = localStorage.getItem(STORAGE_KEY_FLOATING_BALL_SIZE) + return isFloatingBallSize(raw) ? raw : DEFAULT_FLOATING_BALL_SIZE + } catch { + return DEFAULT_FLOATING_BALL_SIZE + } +} + +function readFloatingBallFavoriteAgents(): string[] { + if (typeof document === "undefined") return [] + try { + const raw = localStorage.getItem(STORAGE_KEY_FLOATING_BALL_FAVORITE_AGENTS) + if (!raw) return [] + const parsed: unknown = JSON.parse(raw) + if (!Array.isArray(parsed)) return [] + return parsed.filter((v): v is string => typeof v === "string").slice(0, 5) + } catch { + return [] + } +} + function readNumber( key: string, def: number, @@ -460,6 +536,12 @@ export function AppearanceProvider({ ) const [floatingBallPosition, setFloatingBallPositionState] = useState(() => readFloatingBallPosition()) + const [floatingBallBindings, setFloatingBallBindingsState] = + useState(() => readFloatingBallBindings()) + const [floatingBallSize, setFloatingBallSizeState] = + useState(() => readFloatingBallSize()) + const [floatingBallFavoriteAgents, setFloatingBallFavoriteAgentsState] = + useState(() => readFloatingBallFavoriteAgents()) // 聊天内容宽度:DOM 变量由 inline 脚本预置,这里持有状态供设置页回显。 const [chatContentWidth, setChatContentWidthState] = useState( @@ -649,6 +731,36 @@ export function AppearanceProvider({ [] ) + const setFloatingBallBindings = useCallback( + (bindings: FloatingBallBindingsOverride) => { + setFloatingBallBindingsState(bindings) + const hasKeys = Object.keys(bindings).length > 0 + if (!hasKeys) { + try { + localStorage.removeItem(STORAGE_KEY_FLOATING_BALL_BINDINGS) + } catch { + // 存不进去也不影响本窗口——绑定只是便利,不是数据。 + } + return + } + persist(STORAGE_KEY_FLOATING_BALL_BINDINGS, JSON.stringify(bindings)) + }, + [] + ) + + const setFloatingBallSize = useCallback((size: FloatingBallSize) => { + setFloatingBallSizeState(size) + persist(STORAGE_KEY_FLOATING_BALL_SIZE, size) + }, []) + + const setFloatingBallFavoriteAgents = useCallback((agents: string[]) => { + setFloatingBallFavoriteAgentsState(agents) + persist( + STORAGE_KEY_FLOATING_BALL_FAVORITE_AGENTS, + JSON.stringify(agents.slice(0, 5)) + ) + }, []) + const setChatContentWidth = useCallback((px: number | null) => { chatContentWidthRef.current = px setChatContentWidthState(px) @@ -1109,6 +1221,15 @@ export function AppearanceProvider({ if (e.key === STORAGE_KEY_FLOATING_BALL_POSITION) { setFloatingBallPositionState(readFloatingBallPosition()) } + if (e.key === STORAGE_KEY_FLOATING_BALL_BINDINGS) { + setFloatingBallBindingsState(readFloatingBallBindings()) + } + if (e.key === STORAGE_KEY_FLOATING_BALL_SIZE) { + setFloatingBallSizeState(readFloatingBallSize()) + } + if (e.key === STORAGE_KEY_FLOATING_BALL_FAVORITE_AGENTS) { + setFloatingBallFavoriteAgentsState(readFloatingBallFavoriteAgents()) + } if (e.key && FONT_KEYS.has(e.key)) { rehydrateFonts() } @@ -1219,6 +1340,12 @@ export function AppearanceProvider({ setFloatingBallEnabled, floatingBallPosition, setFloatingBallPosition, + floatingBallBindings, + setFloatingBallBindings, + floatingBallSize, + setFloatingBallSize, + floatingBallFavoriteAgents, + setFloatingBallFavoriteAgents, chatContentWidth, setChatContentWidth, previewChatContentWidth: applyChatContentWidth, diff --git a/src/components/layout/floating-ball.test.tsx b/src/components/layout/floating-ball.test.tsx index 7d9eff38fe..81ce6c7ce1 100644 --- a/src/components/layout/floating-ball.test.tsx +++ b/src/components/layout/floating-ball.test.tsx @@ -8,6 +8,9 @@ const appearance = vi.hoisted(() => ({ enabled: false, position: null as { x: number; y: number } | null, setPosition: vi.fn(), + bindings: {} as Record, + size: "large" as "small" | "medium" | "large", + favoriteAgents: [] as string[], })) vi.mock("@/hooks/use-appearance", () => ({ @@ -16,6 +19,12 @@ vi.mock("@/hooks/use-appearance", () => ({ setFloatingBallEnabled: vi.fn(), floatingBallPosition: appearance.position, setFloatingBallPosition: appearance.setPosition, + floatingBallBindings: appearance.bindings, + setFloatingBallBindings: vi.fn(), + floatingBallSize: appearance.size, + setFloatingBallSize: vi.fn(), + floatingBallFavoriteAgents: appearance.favoriteAgents, + setFloatingBallFavoriteAgents: vi.fn(), }), })) diff --git a/src/components/layout/floating-ball.tsx b/src/components/layout/floating-ball.tsx index 84aedcfe59..9e55480260 100644 --- a/src/components/layout/floating-ball.tsx +++ b/src/components/layout/floating-ball.tsx @@ -6,7 +6,9 @@ import { useTranslations } from "next-intl" import { FLOATING_BALL_INSET, + FLOATING_BALL_SIZE_PX, type FloatingBallPosition, + type FloatingBallSize, } from "@/components/appearance-provider" import { useFloatingBall } from "@/hooks/use-appearance" import { @@ -16,15 +18,28 @@ import { GESTURE_ONSET_MS, LONG_PRESS_MS, MOVE_PX, + previewGesture, resolveTapGesture, type FloatingBallAction, type FloatingBallGesture, } from "@/lib/floating-ball-gestures" import { cn } from "@/lib/utils" -const BALL_SIZE = 46 const PANEL_WIDTH = 208 +/** + * How far the ball follows the finger during a gesture, as a fraction of the + * viewport. MindFS clamps its ring to ±64px because it lives inside an input + * row; ours is free-floating, so the follow distance scales with the screen. + */ +const GESTURE_FOLLOW_RATIO = 0.18 + +const ICON_CLASS: Record = { + small: "size-4", + medium: "size-[18px]", + large: "size-5", +} + /** The rows the tap panel offers, in reading order. */ const PANEL_ACTIONS: readonly Exclude[] = [ "newConversation", @@ -35,6 +50,16 @@ const PANEL_ACTIONS: readonly Exclude[] = [ "prevTab", "reopenClosedTab", "openFolder", + "openTodos", + "openTodosNew", + "openAutomations", + "openCanvas", + "openAgents", + "openSystemAdmin", + "renameConversation", + "openSessionFolder", + "cycleAgent", + "cycleFavoriteAgent", "zoomIn", "zoomOut", "zoomReset", @@ -43,23 +68,35 @@ const PANEL_ACTIONS: readonly Exclude[] = [ ] /** Gestures shown in the cheat sheet, with the action they fire. */ -const CHEAT_SHEET = [ +const CHEAT_SHEET: readonly FloatingBallGesture[] = [ "tap", "doubleTap", "longPress", "left", "right", "up", + "down", "upLeft", "upRight", + "downLeft", + "downRight", "upThenLeft", "upThenRight", "upDown", + "downUp", + "rightThenUp", + "rightThenDown", + "downThenRight", + "downThenLeft", + "upThenDownRight", + "downThenUpRight", + "downRightThenUp", + "downLeftThenUp", "leftThenRight", "rightThenLeft", "upLeftThenDown", "upRightThenDown", -] as const satisfies readonly FloatingBallGesture[] +] interface FloatingBallProps { /** Runs an action against the workspace (tab, panel, zoom, agent turn). */ @@ -68,10 +105,11 @@ interface FloatingBallProps { function clampPosition( position: FloatingBallPosition, - viewport: { width: number; height: number } + viewport: { width: number; height: number }, + ballSize: number ): FloatingBallPosition { - const maxX = Math.max(0, viewport.width - BALL_SIZE - 4) - const maxY = Math.max(0, viewport.height - BALL_SIZE - 4) + const maxX = Math.max(0, viewport.width - ballSize - 4) + const maxY = Math.max(0, viewport.height - ballSize - 4) return { x: Math.min(Math.max(4, position.x), maxX), y: Math.min(Math.max(4, position.y), maxY), @@ -90,12 +128,12 @@ function clampPosition( */ const DEFAULT_Y_RATIO = 0.55 -function defaultPosition(viewport: { - width: number - height: number -}): FloatingBallPosition { +function defaultPosition( + viewport: { width: number; height: number }, + ballSize: number +): FloatingBallPosition { return { - x: viewport.width - BALL_SIZE - FLOATING_BALL_INSET, + x: viewport.width - ballSize - FLOATING_BALL_INSET, y: Math.round(viewport.height * DEFAULT_Y_RATIO), } } @@ -119,22 +157,35 @@ function readViewport(): { width: number; height: number } { * * The gesture vocabulary comes from MindFS's blue-ring menu so the muscle * memory carries over; see `floating-ball-gestures.ts`. Actions are dispatched - * by the caller, which owns the workspace contexts. + * by the caller, which owns the workspace contexts. Bindings and the ball's + * size are owner-configurable (appearance settings); a swipe shows a live hint + * pill naming the action the current trail would fire. */ export function FloatingBall({ onAction }: FloatingBallProps) { const t = useTranslations("FloatingBall") - const { floatingBallEnabled, floatingBallPosition, setFloatingBallPosition } = - useFloatingBall() + const { + floatingBallEnabled, + floatingBallPosition, + setFloatingBallPosition, + floatingBallBindings, + floatingBallSize, + } = useFloatingBall() + + const BALL_SIZE = FLOATING_BALL_SIZE_PX[floatingBallSize] const [viewport, setViewport] = useState(readViewport) // The stored position is the only source of truth: deriving the on-screen // position from it keeps rotation, window resizing and a reset from another // window consistent without copying it into local state. const position = floatingBallPosition - ? clampPosition(floatingBallPosition, viewport) - : defaultPosition(viewport) + ? clampPosition(floatingBallPosition, viewport, BALL_SIZE) + : defaultPosition(viewport, BALL_SIZE) const [panelOpen, setPanelOpen] = useState(false) const [settingsOpen, setSettingsOpen] = useState(false) + // Live gesture feedback: the ball follows the finger and a pill names the + // action the trail would commit. Both reset on lift-off. + const [follow, setFollow] = useState<{ x: number; y: number } | null>(null) + const [hint, setHint] = useState(null) const ballRef = useRef(null) const trackerRef = useRef(createFloatingBallTracker()) @@ -152,6 +203,18 @@ export function FloatingBall({ onAction }: FloatingBallProps) { const tapTimerRef = useRef | null>(null) const frameRef = useRef(null) + const resolveAction = useCallback( + (gesture: FloatingBallGesture): FloatingBallAction | null => { + const override = floatingBallBindings[gesture] as + | FloatingBallAction + | null + | undefined + if (override !== undefined) return override + return DEFAULT_FLOATING_BALL_BINDINGS[gesture] + }, + [floatingBallBindings] + ) + // Keep the viewport current: it decides both the default corner and where an // off-screen ball gets clamped back to. useEffect(() => { @@ -175,7 +238,7 @@ export function FloatingBall({ onAction }: FloatingBallProps) { const fire = useCallback( (gesture: FloatingBallGesture) => { - const action = DEFAULT_FLOATING_BALL_BINDINGS[gesture] + const action = resolveAction(gesture) if (!action) return if (action === "panel") { setSettingsOpen(false) @@ -191,7 +254,7 @@ export function FloatingBall({ onAction }: FloatingBallProps) { setSettingsOpen(false) onAction(action) }, - [onAction] + [onAction, resolveAction] ) const onPointerDown = useCallback( @@ -231,7 +294,8 @@ export function FloatingBall({ onAction }: FloatingBallProps) { event.preventDefault() const next = clampPosition( { x: drag.origin.x + dx, y: drag.origin.y + dy }, - readViewport() + readViewport(), + BALL_SIZE ) // Write the frame directly: a state update per pointer sample would // re-render the whole overlay tree on every move. @@ -249,9 +313,26 @@ export function FloatingBall({ onAction }: FloatingBallProps) { if (drag.mode === "gesture") { event.preventDefault() trackerRef.current.move(event.clientX, event.clientY) + // The ball trails the finger, clamped to a fraction of the viewport so + // a long swipe cannot throw it off-screen; both reset on lift-off. The + // trail offset is gesture-only visual feedback — the stored position + // never moves during a gesture. + const followLimit = Math.round( + Math.min(viewport.width, viewport.height) * GESTURE_FOLLOW_RATIO + ) + setFollow({ + x: Math.max(-followLimit, Math.min(followLimit, dx)), + y: Math.max(-followLimit, Math.min(followLimit, dy)), + }) + setHint( + (() => { + const gesture = previewGesture(trackerRef.current.trail) + return gesture ? resolveAction(gesture) : null + })() + ) } }, - [] + [BALL_SIZE, resolveAction, viewport] ) const onPointerUp = useCallback( @@ -259,6 +340,8 @@ export function FloatingBall({ onAction }: FloatingBallProps) { const drag = dragRef.current if (!drag || drag.pointerId !== event.pointerId) return dragRef.current = null + setFollow(null) + setHint(null) if (frameRef.current !== null) { cancelAnimationFrame(frameRef.current) frameRef.current = null @@ -270,14 +353,15 @@ export function FloatingBall({ onAction }: FloatingBallProps) { setFloatingBallPosition( clampPosition( { x: drag.origin.x + dx, y: drag.origin.y + dy }, - readViewport() + readViewport(), + BALL_SIZE ) ) return } if (drag.mode === "gesture") { - const gesture = trackerRef.current.end(event.clientX, event.clientY) + const gesture = trackerRef.current.end() trackerRef.current.reset() if (gesture) fire(gesture) return @@ -314,7 +398,7 @@ export function FloatingBall({ onAction }: FloatingBallProps) { fire("tap") }, DOUBLE_TAP_MS) }, - [clearTapTimer, fire, setFloatingBallPosition] + [BALL_SIZE, clearTapTimer, fire, setFloatingBallPosition] ) const onPointerCancel = useCallback( @@ -323,6 +407,8 @@ export function FloatingBall({ onAction }: FloatingBallProps) { if (!drag || drag.pointerId !== event.pointerId) return dragRef.current = null trackerRef.current.reset() + setFollow(null) + setHint(null) if (frameRef.current !== null) { cancelAnimationFrame(frameRef.current) frameRef.current = null @@ -375,6 +461,16 @@ export function FloatingBall({ onAction }: FloatingBallProps) { Math.max(8, viewport.width - PANEL_WIDTH - 8) ) + const cheatRows = CHEAT_SHEET.map((gesture) => ({ + gesture, + action: resolveAction(gesture), + })).filter( + ( + row + ): row is { gesture: FloatingBallGesture; action: FloatingBallAction } => + row.action !== null + ) + return ( <> + {hint && ( +
+ {hint === "panel" ? t("panelTitle") : t(`actions.${hint}`)} +
+ )} + {panelOpen && (
    - {CHEAT_SHEET.map((gesture) => { - const action = DEFAULT_FLOATING_BALL_BINDINGS[gesture] - if (!action) return null - return ( -
  • - - {t(`gestures.${gesture}`)} - - - {action === "panel" - ? t("panelTitle") - : t(`actions.${action}`)} - -
  • - ) - })} + {cheatRows.map(({ gesture, action }) => ( +
  • + + {t(`gestures.${gesture}`)} + + + {action === "panel" + ? t("panelTitle") + : t(`actions.${action}`)} + +
  • + ))}

diff --git a/src/components/settings/appearance-settings.tsx b/src/components/settings/appearance-settings.tsx index b56653fbdc..85c3f33a32 100644 --- a/src/components/settings/appearance-settings.tsx +++ b/src/components/settings/appearance-settings.tsx @@ -32,6 +32,13 @@ import { useChatContentWidth, useFloatingBall, } from "@/hooks/use-appearance" +import { useAcpAgents } from "@/hooks/use-acp-agents" +import { + DEFAULT_FLOATING_BALL_BINDINGS, + FLOATING_BALL_GESTURES, + type FloatingBallAction, +} from "@/lib/floating-ball-gestures" +import { type FloatingBallSize } from "@/components/appearance-provider" import { CHAT_CONTENT_DEFAULT, CHAT_CONTENT_MIN, @@ -62,6 +69,100 @@ function readScreenWidth(): number { type ThemeMode = "system" | "light" | "dark" +/** Actions the binding dropdown offers (everything but the panel toggle). */ +const BALL_BINDABLE_ACTIONS: readonly FloatingBallAction[] = [ + "panel", + "newConversation", + "toggleSidebar", + "toggleSearch", + "toggleTerminal", + "nextTab", + "prevTab", + "reopenClosedTab", + "openFolder", + "openTodos", + "openTodosNew", + "openAutomations", + "openCanvas", + "openAgents", + "openSystemAdmin", + "renameConversation", + "openSessionFolder", + "cycleAgent", + "cycleFavoriteAgent", + "zoomIn", + "zoomOut", + "zoomReset", + "stopGenerating", + "openSettings", +] + +function isFloatingBallSize(v: string): v is FloatingBallSize { + return v === "small" || v === "medium" || v === "large" +} + +/** Label of a gesture's built-in default, for the placeholder row. */ +function defaultGestureLabel( + action: FloatingBallAction, + // Loosely typed: the two caller namespaces (FloatingBall / AppearanceSettings) + // both mirror the actions.* keys, and next-intl's key union would force a + // per-namespace overload for no safety gain here. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + t: any +): string { + return action === "panel" ? t("panelTitle") : t(`actions.${action}`) +} + +/** Multi-select (2-3) of agents the cycle-favorite gesture switches between. */ +function FavoriteAgentsPicker() { + const t = useTranslations("FloatingBall") + const { floatingBallFavoriteAgents, setFloatingBallFavoriteAgents } = + useFloatingBall() + const { agents } = useAcpAgents() + const usable = agents.filter((a) => a.enabled && a.available) + + const toggle = (agentType: string) => { + const has = floatingBallFavoriteAgents.includes(agentType) + if (has) { + setFloatingBallFavoriteAgents( + floatingBallFavoriteAgents.filter((a) => a !== agentType) + ) + return + } + // Cap at 3: the cycle is a thumb gesture, a longer loop defeats it. + if (floatingBallFavoriteAgents.length >= 3) return + setFloatingBallFavoriteAgents([...floatingBallFavoriteAgents, agentType]) + } + + return ( +

+ {usable.map((agent) => { + const picked = floatingBallFavoriteAgents.includes(agent.agent_type) + return ( + + ) + })} + {usable.length === 0 && ( + + {t("favoriteAgentsEmpty")} + + )} +
+ ) +} + export function AppearanceSettings() { const t = useTranslations("AppearanceSettings") const { theme, resolvedTheme, setTheme } = useTheme() @@ -75,6 +176,10 @@ export function AppearanceSettings() { setFloatingBallEnabled, floatingBallPosition, setFloatingBallPosition, + floatingBallBindings, + setFloatingBallBindings, + floatingBallSize, + setFloatingBallSize, } = useFloatingBall() const { chatContentWidth, setChatContentWidth } = useChatContentWidth() const [screenWidth] = useState(readScreenWidth) @@ -82,6 +187,11 @@ export function AppearanceSettings() { // 的宽度更小。 const chatWidthMax = chatContentWidthCeiling(screenWidth, zoomLevel / 100) + // The binding dropdown renders under the AppearanceSettings namespace, whose + // mirrored actions.* keys come from FloatingBall. + const actionLabel = (action: FloatingBallAction) => + action === "panel" ? t("floatingBall.panelTitle") : t(`actions.${action}`) + const resolvedThemeLabel = resolvedTheme === "dark" ? t("resolvedTheme.dark") @@ -339,6 +449,112 @@ export function AppearanceSettings() { {t("floatingBall.resetPosition")}
+ + {/* Ball size: three presets, defaults to large (the v81 size). */} +
+

{t("floatingBall.ballSize")}

+ +
+ + {/* Per-gesture binding overrides. An empty SelectValue means the + gesture falls back to the built-in default row below. */} +
+

+ {t("floatingBall.gestureBindings")} +

+
+ {FLOATING_BALL_GESTURES.filter( + (g) => g !== "tap" && g !== "doubleTap" && g !== "longPress" + ).map((gesture) => { + const override = floatingBallBindings[gesture] + const value = override ?? "" + return ( +
+ + {t(`gestures.${gesture}`, { + fallback: gesture, + })} + + +
+ ) + })} +
+
+ + {/* Favorite agents for the cycle-favorite gesture. */} +
+

+ {t("floatingBall.favoriteAgents")} +

+ +

+ {t("floatingBall.favoriteAgentsHint")} +

+
{/* ===== Chat content width ===== */}
diff --git a/src/hooks/use-appearance.ts b/src/hooks/use-appearance.ts index 7d08bece29..ac2730b1cf 100644 --- a/src/hooks/use-appearance.ts +++ b/src/hooks/use-appearance.ts @@ -4,6 +4,7 @@ import { useContext } from "react" import { AppearanceContext, ChatAnimationsContext, + DEFAULT_FLOATING_BALL_SIZE, } from "@/components/appearance-provider" import { applyChatContentWidth, @@ -56,6 +57,13 @@ export function useFloatingBall() { setFloatingBallEnabled: ctx?.setFloatingBallEnabled ?? (() => {}), floatingBallPosition: ctx?.floatingBallPosition ?? null, setFloatingBallPosition: ctx?.setFloatingBallPosition ?? (() => {}), + floatingBallBindings: ctx?.floatingBallBindings ?? {}, + setFloatingBallBindings: ctx?.setFloatingBallBindings ?? (() => {}), + floatingBallSize: ctx?.floatingBallSize ?? DEFAULT_FLOATING_BALL_SIZE, + setFloatingBallSize: ctx?.setFloatingBallSize ?? (() => {}), + floatingBallFavoriteAgents: ctx?.floatingBallFavoriteAgents ?? [], + setFloatingBallFavoriteAgents: + ctx?.setFloatingBallFavoriteAgents ?? (() => {}), } } diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 670eea527d..bb16902416 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -245,7 +245,73 @@ "sectionDescription": "كرة قابلة للسحب فوق مساحة العمل تحوّل السحب إلى اختصارات، مصممة للهاتف حيث لا توجد لوحة مفاتيح.", "enable": "إظهار الكرة العائمة", "hint": "اسحب للتحريك، اسحب بسرعة لإيماءة، اضغط مطوّلاً للوحة وقائمة الإيماءات.", - "resetPosition": "إعادة تعيين الموضع" + "resetPosition": "إعادة تعيين الموضع", + "ballSize": "حجم الكرة", + "sizeSmall": "صغير", + "sizeMedium": "متوسط", + "sizeLarge": "كبير", + "gestureBindings": "ربط الإيماءات", + "bindingCleared": "غير مربوط", + "favoriteAgents": "الوكلاء المفضلون", + "favoriteAgentsHint": "اختر 2-3 وكلاء للتنقل بينهم بإيماءة المفضلين.", + "favoriteAgentsEmpty": "لا يوجد وكلاء متاحون", + "panelTitle": "إجراءات سريعة" + }, + "gestures": { + "tap": "Tap", + "doubleTap": "Double tap", + "longPress": "Long press", + "left": "Swipe left", + "right": "Swipe right", + "up": "Swipe up", + "upLeft": "Up-left", + "upRight": "Up-right", + "upThenLeft": "Up, then left", + "upThenRight": "Up, then right", + "upDown": "Up, then down", + "leftThenRight": "Left, then right", + "rightThenLeft": "Right, then left", + "upLeftThenDown": "Up-left, then down", + "upRightThenDown": "Up-right, then down", + "down": "Swipe down", + "downLeft": "Down-left", + "downRight": "Down-right", + "downUp": "Down, then up", + "upThenDownRight": "Up, then down-right", + "downThenUpRight": "Down, then up-right", + "rightThenUp": "Right, then up", + "rightThenDown": "Right, then down", + "downThenRight": "Down, then right", + "downThenLeft": "Down, then left", + "downRightThenUp": "Down-right, then up", + "downLeftThenUp": "Down-left, then up", + "upLeftThenRight": "Up-left, then right", + "upRightThenLeft": "Up-right, then left" + }, + "actions": { + "newConversation": "New conversation", + "toggleSidebar": "Toggle sidebar", + "toggleSearch": "Search", + "toggleTerminal": "Toggle terminal", + "nextTab": "Next tab", + "prevTab": "Previous tab", + "reopenClosedTab": "Reopen closed tab", + "openFolder": "Open folder", + "zoomIn": "Zoom in", + "zoomOut": "Zoom out", + "zoomReset": "Reset zoom", + "stopGenerating": "Stop generating", + "openSettings": "Settings", + "openTodos": "To-dos", + "openTodosNew": "New to-do", + "openAutomations": "Automations", + "openCanvas": "Canvas", + "openAgents": "Agents", + "openSystemAdmin": "System admin", + "renameConversation": "Rename conversation", + "openSessionFolder": "Session files", + "cycleAgent": "Next agent", + "cycleFavoriteAgent": "Favorite agents" } }, "SystemSettings": { @@ -7357,7 +7423,17 @@ "zoomOut": "تصغير", "zoomReset": "إعادة تعيين التكبير", "stopGenerating": "إيقاف التوليد", - "openSettings": "الإعدادات" + "openSettings": "الإعدادات", + "openTodos": "المهام", + "openTodosNew": "مهمة جديدة", + "openAutomations": "الأتمتة", + "openCanvas": "اللوحة", + "openAgents": "الوكلاء", + "openSystemAdmin": "إدارة النظام", + "renameConversation": "إعادة تسمية المحادثة", + "openSessionFolder": "ملفات الجلسة", + "cycleAgent": "الوكيل التالي", + "cycleFavoriteAgent": "الوكلاء المفضلون" }, "gestures": { "tap": "نقرة", @@ -7374,7 +7450,22 @@ "leftThenRight": "يسار ثم يمين", "rightThenLeft": "يمين ثم يسار", "upLeftThenDown": "أعلى اليسار ثم أسفل", - "upRightThenDown": "أعلى اليمين ثم أسفل" - } + "upRightThenDown": "أعلى اليمين ثم أسفل", + "down": "اسحب لأسفل", + "downLeft": "أسفل-يسار", + "downRight": "أسفل-يمين", + "downUp": "أسفل ثم أعلى", + "upThenDownRight": "أعلى ثم أسفل-يمين", + "downThenUpRight": "أسفل ثم أعلى-يمين", + "rightThenUp": "يمين ثم أعلى", + "rightThenDown": "يمين ثم أسفل", + "downThenRight": "أسفل ثم يمين", + "downThenLeft": "أسفل ثم يسار", + "downRightThenUp": "أسفل-يمين ثم أعلى", + "downLeftThenUp": "أسفل-يسار ثم أعلى", + "upLeftThenRight": "أعلى-يسار ثم يمين", + "upRightThenLeft": "أعلى-يمين ثم يسار" + }, + "favoriteAgentsEmpty": "لا يوجد وكلاء متاحون" } } diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index b761c3d26f..f1e38eb844 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -245,7 +245,73 @@ "sectionDescription": "Eine verschiebbare Kugel über dem Arbeitsbereich, die Wischgesten in Tastenkürzel verwandelt – gedacht für das Telefon ohne Tastatur.", "enable": "Schwebekugel anzeigen", "hint": "Ziehen zum Verschieben, schnelles Wischen für eine Geste, langes Drücken für Panel und Gestenübersicht.", - "resetPosition": "Position zurücksetzen" + "resetPosition": "Position zurücksetzen", + "ballSize": "Kugelgröße", + "sizeSmall": "Klein", + "sizeMedium": "Mittel", + "sizeLarge": "Groß", + "gestureBindings": "Gestenzuordnung", + "bindingCleared": "Nicht zugewiesen", + "favoriteAgents": "Favoriten-Agenten", + "favoriteAgentsHint": "Wähle 2-3 Agenten, um sie mit dem Favoriten-Gest zu durchlaufen.", + "favoriteAgentsEmpty": "Keine Agenten verfügbar", + "panelTitle": "Schnellaktionen" + }, + "gestures": { + "tap": "Tap", + "doubleTap": "Double tap", + "longPress": "Long press", + "left": "Swipe left", + "right": "Swipe right", + "up": "Swipe up", + "upLeft": "Up-left", + "upRight": "Up-right", + "upThenLeft": "Up, then left", + "upThenRight": "Up, then right", + "upDown": "Up, then down", + "leftThenRight": "Left, then right", + "rightThenLeft": "Right, then left", + "upLeftThenDown": "Up-left, then down", + "upRightThenDown": "Up-right, then down", + "down": "Swipe down", + "downLeft": "Down-left", + "downRight": "Down-right", + "downUp": "Down, then up", + "upThenDownRight": "Up, then down-right", + "downThenUpRight": "Down, then up-right", + "rightThenUp": "Right, then up", + "rightThenDown": "Right, then down", + "downThenRight": "Down, then right", + "downThenLeft": "Down, then left", + "downRightThenUp": "Down-right, then up", + "downLeftThenUp": "Down-left, then up", + "upLeftThenRight": "Up-left, then right", + "upRightThenLeft": "Up-right, then left" + }, + "actions": { + "newConversation": "New conversation", + "toggleSidebar": "Toggle sidebar", + "toggleSearch": "Search", + "toggleTerminal": "Toggle terminal", + "nextTab": "Next tab", + "prevTab": "Previous tab", + "reopenClosedTab": "Reopen closed tab", + "openFolder": "Open folder", + "zoomIn": "Zoom in", + "zoomOut": "Zoom out", + "zoomReset": "Reset zoom", + "stopGenerating": "Stop generating", + "openSettings": "Settings", + "openTodos": "To-dos", + "openTodosNew": "New to-do", + "openAutomations": "Automations", + "openCanvas": "Canvas", + "openAgents": "Agents", + "openSystemAdmin": "System admin", + "renameConversation": "Rename conversation", + "openSessionFolder": "Session files", + "cycleAgent": "Next agent", + "cycleFavoriteAgent": "Favorite agents" } }, "SystemSettings": { @@ -7357,7 +7423,17 @@ "zoomOut": "Verkleinern", "zoomReset": "Zoom zurücksetzen", "stopGenerating": "Generierung stoppen", - "openSettings": "Einstellungen" + "openSettings": "Einstellungen", + "openTodos": "Aufgaben", + "openTodosNew": "Neue Aufgabe", + "openAutomations": "Automatisierungen", + "openCanvas": "Canvas", + "openAgents": "Agenten", + "openSystemAdmin": "Systemverwaltung", + "renameConversation": "Unterhaltung umbenennen", + "openSessionFolder": "Sitzungsdateien", + "cycleAgent": "Nächster Agent", + "cycleFavoriteAgent": "Favoriten-Agenten" }, "gestures": { "tap": "Tippen", @@ -7374,7 +7450,22 @@ "leftThenRight": "Links, dann rechts", "rightThenLeft": "Rechts, dann links", "upLeftThenDown": "Oben links, dann unten", - "upRightThenDown": "Oben rechts, dann unten" - } + "upRightThenDown": "Oben rechts, dann unten", + "down": "Nach unten", + "downLeft": "Unten-links", + "downRight": "Unten-rechts", + "downUp": "Unten, dann oben", + "upThenDownRight": "Oben, dann unten-re.", + "downThenUpRight": "Unten, dann oben-re.", + "rightThenUp": "Rechts, dann oben", + "rightThenDown": "Rechts, dann unten", + "downThenRight": "Unten, dann rechts", + "downThenLeft": "Unten, dann links", + "downRightThenUp": "Unten-re., dann oben", + "downLeftThenUp": "Unten-li., dann oben", + "upLeftThenRight": "Oben-li., dann rechts", + "upRightThenLeft": "Oben-re., dann links" + }, + "favoriteAgentsEmpty": "Keine Agenten verfügbar" } } diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 40dddea163..4d9f43bf03 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -245,7 +245,73 @@ "sectionDescription": "A draggable ball over the workspace that turns swipes into shortcuts — built for the phone, where there is no keyboard.", "enable": "Show floating ball", "hint": "Drag to move; swipe quickly for a gesture; long press for the panel and cheat sheet.", - "resetPosition": "Reset position" + "resetPosition": "Reset position", + "ballSize": "Ball size", + "sizeSmall": "Small", + "sizeMedium": "Medium", + "sizeLarge": "Large", + "gestureBindings": "Gesture bindings", + "bindingCleared": "Unbound", + "favoriteAgents": "Favorite agents", + "favoriteAgentsHint": "Pick 2-3 agents to cycle through with the favorite-agents gesture.", + "favoriteAgentsEmpty": "No available agents", + "panelTitle": "Quick actions" + }, + "gestures": { + "tap": "Tap", + "doubleTap": "Double tap", + "longPress": "Long press", + "left": "Swipe left", + "right": "Swipe right", + "up": "Swipe up", + "upLeft": "Up-left", + "upRight": "Up-right", + "upThenLeft": "Up, then left", + "upThenRight": "Up, then right", + "upDown": "Up, then down", + "leftThenRight": "Left, then right", + "rightThenLeft": "Right, then left", + "upLeftThenDown": "Up-left, then down", + "upRightThenDown": "Up-right, then down", + "down": "Swipe down", + "downLeft": "Down-left", + "downRight": "Down-right", + "downUp": "Down, then up", + "upThenDownRight": "Up, then down-right", + "downThenUpRight": "Down, then up-right", + "rightThenUp": "Right, then up", + "rightThenDown": "Right, then down", + "downThenRight": "Down, then right", + "downThenLeft": "Down, then left", + "downRightThenUp": "Down-right, then up", + "downLeftThenUp": "Down-left, then up", + "upLeftThenRight": "Up-left, then right", + "upRightThenLeft": "Up-right, then left" + }, + "actions": { + "newConversation": "New conversation", + "toggleSidebar": "Toggle sidebar", + "toggleSearch": "Search", + "toggleTerminal": "Toggle terminal", + "nextTab": "Next tab", + "prevTab": "Previous tab", + "reopenClosedTab": "Reopen closed tab", + "openFolder": "Open folder", + "zoomIn": "Zoom in", + "zoomOut": "Zoom out", + "zoomReset": "Reset zoom", + "stopGenerating": "Stop generating", + "openSettings": "Settings", + "openTodos": "To-dos", + "openTodosNew": "New to-do", + "openAutomations": "Automations", + "openCanvas": "Canvas", + "openAgents": "Agents", + "openSystemAdmin": "System admin", + "renameConversation": "Rename conversation", + "openSessionFolder": "Session files", + "cycleAgent": "Next agent", + "cycleFavoriteAgent": "Favorite agents" } }, "SystemSettings": { @@ -7357,7 +7423,17 @@ "zoomOut": "Zoom out", "zoomReset": "Reset zoom", "stopGenerating": "Stop generating", - "openSettings": "Settings" + "openSettings": "Settings", + "openTodos": "To-dos", + "openTodosNew": "New to-do", + "openAutomations": "Automations", + "openCanvas": "Canvas", + "openAgents": "Agents", + "openSystemAdmin": "System admin", + "renameConversation": "Rename conversation", + "openSessionFolder": "Session files", + "cycleAgent": "Next agent", + "cycleFavoriteAgent": "Favorite agents" }, "gestures": { "tap": "Tap", @@ -7374,7 +7450,22 @@ "leftThenRight": "Left, then right", "rightThenLeft": "Right, then left", "upLeftThenDown": "Up-left, then down", - "upRightThenDown": "Up-right, then down" - } + "upRightThenDown": "Up-right, then down", + "down": "Swipe down", + "downLeft": "Down-left", + "downRight": "Down-right", + "downUp": "Down, then up", + "upThenDownRight": "Up, then down-right", + "downThenUpRight": "Down, then up-right", + "rightThenUp": "Right, then up", + "rightThenDown": "Right, then down", + "downThenRight": "Down, then right", + "downThenLeft": "Down, then left", + "downRightThenUp": "Down-right, then up", + "downLeftThenUp": "Down-left, then up", + "upLeftThenRight": "Up-left, then right", + "upRightThenLeft": "Up-right, then left" + }, + "favoriteAgentsEmpty": "No available agents" } } diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 5a552bfd2e..1a7fdba587 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -245,7 +245,73 @@ "sectionDescription": "Una bola arrastrable sobre el espacio de trabajo que convierte los deslizamientos en atajos, pensada para el teléfono, donde no hay teclado.", "enable": "Mostrar bola flotante", "hint": "Arrastra para mover, desliza rápido para un gesto, mantén pulsado para el panel y la lista de gestos.", - "resetPosition": "Restablecer posición" + "resetPosition": "Restablecer posición", + "ballSize": "Tamaño de la bola", + "sizeSmall": "Pequeño", + "sizeMedium": "Mediano", + "sizeLarge": "Grande", + "gestureBindings": "Asignación de gestos", + "bindingCleared": "Sin asignar", + "favoriteAgents": "Agentes favoritos", + "favoriteAgentsHint": "Elige 2-3 agentes para alternar con el gesto de favoritos.", + "favoriteAgentsEmpty": "No hay agentes disponibles", + "panelTitle": "Acciones rápidas" + }, + "gestures": { + "tap": "Tap", + "doubleTap": "Double tap", + "longPress": "Long press", + "left": "Swipe left", + "right": "Swipe right", + "up": "Swipe up", + "upLeft": "Up-left", + "upRight": "Up-right", + "upThenLeft": "Up, then left", + "upThenRight": "Up, then right", + "upDown": "Up, then down", + "leftThenRight": "Left, then right", + "rightThenLeft": "Right, then left", + "upLeftThenDown": "Up-left, then down", + "upRightThenDown": "Up-right, then down", + "down": "Swipe down", + "downLeft": "Down-left", + "downRight": "Down-right", + "downUp": "Down, then up", + "upThenDownRight": "Up, then down-right", + "downThenUpRight": "Down, then up-right", + "rightThenUp": "Right, then up", + "rightThenDown": "Right, then down", + "downThenRight": "Down, then right", + "downThenLeft": "Down, then left", + "downRightThenUp": "Down-right, then up", + "downLeftThenUp": "Down-left, then up", + "upLeftThenRight": "Up-left, then right", + "upRightThenLeft": "Up-right, then left" + }, + "actions": { + "newConversation": "New conversation", + "toggleSidebar": "Toggle sidebar", + "toggleSearch": "Search", + "toggleTerminal": "Toggle terminal", + "nextTab": "Next tab", + "prevTab": "Previous tab", + "reopenClosedTab": "Reopen closed tab", + "openFolder": "Open folder", + "zoomIn": "Zoom in", + "zoomOut": "Zoom out", + "zoomReset": "Reset zoom", + "stopGenerating": "Stop generating", + "openSettings": "Settings", + "openTodos": "To-dos", + "openTodosNew": "New to-do", + "openAutomations": "Automations", + "openCanvas": "Canvas", + "openAgents": "Agents", + "openSystemAdmin": "System admin", + "renameConversation": "Rename conversation", + "openSessionFolder": "Session files", + "cycleAgent": "Next agent", + "cycleFavoriteAgent": "Favorite agents" } }, "SystemSettings": { @@ -7357,7 +7423,17 @@ "zoomOut": "Alejar", "zoomReset": "Restablecer zoom", "stopGenerating": "Detener la generación", - "openSettings": "Ajustes" + "openSettings": "Ajustes", + "openTodos": "Tareas", + "openTodosNew": "Nueva tarea", + "openAutomations": "Automatizaciones", + "openCanvas": "Lienzo", + "openAgents": "Agentes", + "openSystemAdmin": "Administración", + "renameConversation": "Renombrar conversación", + "openSessionFolder": "Archivos de sesión", + "cycleAgent": "Siguiente agente", + "cycleFavoriteAgent": "Agentes favoritos" }, "gestures": { "tap": "Toque", @@ -7374,7 +7450,22 @@ "leftThenRight": "Izquierda y luego derecha", "rightThenLeft": "Derecha y luego izquierda", "upLeftThenDown": "Arriba a la izquierda y luego abajo", - "upRightThenDown": "Arriba a la derecha y luego abajo" - } + "upRightThenDown": "Arriba a la derecha y luego abajo", + "down": "Deslizar abajo", + "downLeft": "Abajo-izq.", + "downRight": "Abajo-der.", + "downUp": "Abajo, luego arriba", + "upThenDownRight": "Arriba, luego abajo-der.", + "downThenUpRight": "Abajo, luego arriba-der.", + "rightThenUp": "Derecha, luego arriba", + "rightThenDown": "Derecha, luego abajo", + "downThenRight": "Abajo, luego derecha", + "downThenLeft": "Abajo, luego izquierda", + "downRightThenUp": "Abajo-der., luego arriba", + "downLeftThenUp": "Abajo-izq., luego arriba", + "upLeftThenRight": "Arriba-izq., luego derecha", + "upRightThenLeft": "Arriba-der., luego izquierda" + }, + "favoriteAgentsEmpty": "No hay agentes disponibles" } } diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index cfe3b4a4ce..cfa49392eb 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -245,7 +245,73 @@ "sectionDescription": "Une boule déplaçable au-dessus de l’espace de travail qui transforme les balayages en raccourcis — pensée pour le téléphone, où il n’y a pas de clavier.", "enable": "Afficher la boule flottante", "hint": "Faites glisser pour déplacer, balayez rapidement pour un geste, appuyez longuement pour le panneau et la liste des gestes.", - "resetPosition": "Réinitialiser la position" + "resetPosition": "Réinitialiser la position", + "ballSize": "Taille de la bille", + "sizeSmall": "Petite", + "sizeMedium": "Moyenne", + "sizeLarge": "Grande", + "gestureBindings": "Affectation des gestes", + "bindingCleared": "Non attribué", + "favoriteAgents": "Agents favoris", + "favoriteAgentsHint": "Choisissez 2-3 agents à parcourir avec le geste des favoris.", + "favoriteAgentsEmpty": "Aucun agent disponible", + "panelTitle": "Actions rapides" + }, + "gestures": { + "tap": "Tap", + "doubleTap": "Double tap", + "longPress": "Long press", + "left": "Swipe left", + "right": "Swipe right", + "up": "Swipe up", + "upLeft": "Up-left", + "upRight": "Up-right", + "upThenLeft": "Up, then left", + "upThenRight": "Up, then right", + "upDown": "Up, then down", + "leftThenRight": "Left, then right", + "rightThenLeft": "Right, then left", + "upLeftThenDown": "Up-left, then down", + "upRightThenDown": "Up-right, then down", + "down": "Swipe down", + "downLeft": "Down-left", + "downRight": "Down-right", + "downUp": "Down, then up", + "upThenDownRight": "Up, then down-right", + "downThenUpRight": "Down, then up-right", + "rightThenUp": "Right, then up", + "rightThenDown": "Right, then down", + "downThenRight": "Down, then right", + "downThenLeft": "Down, then left", + "downRightThenUp": "Down-right, then up", + "downLeftThenUp": "Down-left, then up", + "upLeftThenRight": "Up-left, then right", + "upRightThenLeft": "Up-right, then left" + }, + "actions": { + "newConversation": "New conversation", + "toggleSidebar": "Toggle sidebar", + "toggleSearch": "Search", + "toggleTerminal": "Toggle terminal", + "nextTab": "Next tab", + "prevTab": "Previous tab", + "reopenClosedTab": "Reopen closed tab", + "openFolder": "Open folder", + "zoomIn": "Zoom in", + "zoomOut": "Zoom out", + "zoomReset": "Reset zoom", + "stopGenerating": "Stop generating", + "openSettings": "Settings", + "openTodos": "To-dos", + "openTodosNew": "New to-do", + "openAutomations": "Automations", + "openCanvas": "Canvas", + "openAgents": "Agents", + "openSystemAdmin": "System admin", + "renameConversation": "Rename conversation", + "openSessionFolder": "Session files", + "cycleAgent": "Next agent", + "cycleFavoriteAgent": "Favorite agents" } }, "SystemSettings": { @@ -7357,7 +7423,17 @@ "zoomOut": "Zoom arrière", "zoomReset": "Réinitialiser le zoom", "stopGenerating": "Arrêter la génération", - "openSettings": "Paramètres" + "openSettings": "Paramètres", + "openTodos": "Tâches", + "openTodosNew": "Nouvelle tâche", + "openAutomations": "Automatisations", + "openCanvas": "Canvas", + "openAgents": "Agents", + "openSystemAdmin": "Administration", + "renameConversation": "Renommer la conversation", + "openSessionFolder": "Fichiers de session", + "cycleAgent": "Agent suivant", + "cycleFavoriteAgent": "Agents favoris" }, "gestures": { "tap": "Appui", @@ -7374,7 +7450,22 @@ "leftThenRight": "Gauche, puis droite", "rightThenLeft": "Droite, puis gauche", "upLeftThenDown": "Haut-gauche, puis bas", - "upRightThenDown": "Haut-droite, puis bas" - } + "upRightThenDown": "Haut-droite, puis bas", + "down": "Balayer vers le bas", + "downLeft": "Bas-gauche", + "downRight": "Bas-droite", + "downUp": "Bas, puis haut", + "upThenDownRight": "Haut, puis bas-dr.", + "downThenUpRight": "Bas, puis haut-dr.", + "rightThenUp": "Droite, puis haut", + "rightThenDown": "Droite, puis bas", + "downThenRight": "Bas, puis droite", + "downThenLeft": "Bas, puis gauche", + "downRightThenUp": "Bas-dr., puis haut", + "downLeftThenUp": "Bas-g., puis haut", + "upLeftThenRight": "Haut-g., puis droite", + "upRightThenLeft": "Haut-dr., puis gauche" + }, + "favoriteAgentsEmpty": "Aucun agent disponible" } } diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index a915251370..25964435b8 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -245,7 +245,73 @@ "sectionDescription": "ワークスペース上に置くドラッグ可能なボール。スワイプをショートカットに変えます。キーボードのないスマートフォン向けです。", "enable": "フローティングボールを表示", "hint": "ドラッグで移動、素早いスワイプでジェスチャー、長押しでパネルとジェスチャー一覧を開きます。", - "resetPosition": "位置をリセット" + "resetPosition": "位置をリセット", + "ballSize": "ボールサイズ", + "sizeSmall": "小", + "sizeMedium": "中", + "sizeLarge": "大", + "gestureBindings": "ジェスチャー割り当て", + "bindingCleared": "未割り当て", + "favoriteAgents": "お気に入りエージェント", + "favoriteAgentsHint": "2〜3個のエージェントを選ぶと、お気に入りジェスチャーで切り替えられます。", + "favoriteAgentsEmpty": "利用可能なエージェントがありません", + "panelTitle": "クイック操作" + }, + "gestures": { + "tap": "Tap", + "doubleTap": "Double tap", + "longPress": "Long press", + "left": "Swipe left", + "right": "Swipe right", + "up": "Swipe up", + "upLeft": "Up-left", + "upRight": "Up-right", + "upThenLeft": "Up, then left", + "upThenRight": "Up, then right", + "upDown": "Up, then down", + "leftThenRight": "Left, then right", + "rightThenLeft": "Right, then left", + "upLeftThenDown": "Up-left, then down", + "upRightThenDown": "Up-right, then down", + "down": "Swipe down", + "downLeft": "Down-left", + "downRight": "Down-right", + "downUp": "Down, then up", + "upThenDownRight": "Up, then down-right", + "downThenUpRight": "Down, then up-right", + "rightThenUp": "Right, then up", + "rightThenDown": "Right, then down", + "downThenRight": "Down, then right", + "downThenLeft": "Down, then left", + "downRightThenUp": "Down-right, then up", + "downLeftThenUp": "Down-left, then up", + "upLeftThenRight": "Up-left, then right", + "upRightThenLeft": "Up-right, then left" + }, + "actions": { + "newConversation": "New conversation", + "toggleSidebar": "Toggle sidebar", + "toggleSearch": "Search", + "toggleTerminal": "Toggle terminal", + "nextTab": "Next tab", + "prevTab": "Previous tab", + "reopenClosedTab": "Reopen closed tab", + "openFolder": "Open folder", + "zoomIn": "Zoom in", + "zoomOut": "Zoom out", + "zoomReset": "Reset zoom", + "stopGenerating": "Stop generating", + "openSettings": "Settings", + "openTodos": "To-dos", + "openTodosNew": "New to-do", + "openAutomations": "Automations", + "openCanvas": "Canvas", + "openAgents": "Agents", + "openSystemAdmin": "System admin", + "renameConversation": "Rename conversation", + "openSessionFolder": "Session files", + "cycleAgent": "Next agent", + "cycleFavoriteAgent": "Favorite agents" } }, "SystemSettings": { @@ -7357,7 +7423,17 @@ "zoomOut": "縮小", "zoomReset": "ズームをリセット", "stopGenerating": "生成を停止", - "openSettings": "設定" + "openSettings": "設定", + "openTodos": "ToDo", + "openTodosNew": "ToDo作成", + "openAutomations": "オートメーション", + "openCanvas": "キャンバス", + "openAgents": "エージェント", + "openSystemAdmin": "システム管理", + "renameConversation": "会話名を変更", + "openSessionFolder": "セッションファイル", + "cycleAgent": "次のエージェント", + "cycleFavoriteAgent": "お気に入りエージェント" }, "gestures": { "tap": "タップ", @@ -7374,7 +7450,22 @@ "leftThenRight": "左→右", "rightThenLeft": "右→左", "upLeftThenDown": "左上→下", - "upRightThenDown": "右上→下" - } + "upRightThenDown": "右上→下", + "down": "下スワイプ", + "downLeft": "左下", + "downRight": "右下", + "downUp": "下から上", + "upThenDownRight": "上から右下", + "downThenUpRight": "下から右上", + "rightThenUp": "右から上", + "rightThenDown": "右から下", + "downThenRight": "下から右", + "downThenLeft": "下から左", + "downRightThenUp": "右下から上", + "downLeftThenUp": "左下から上", + "upLeftThenRight": "左上から右", + "upRightThenLeft": "右上から左" + }, + "favoriteAgentsEmpty": "利用可能なエージェントがありません" } } diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 01214e8f7c..a430f35e7a 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -245,7 +245,73 @@ "sectionDescription": "작업 공간 위의 드래그 가능한 볼로, 스와이프를 단축키로 바꿉니다. 키보드가 없는 휴대폰을 위한 기능입니다.", "enable": "플로팅 볼 표시", "hint": "드래그로 이동, 빠른 스와이프로 제스처, 길게 눌러 패널과 제스처 목록을 엽니다.", - "resetPosition": "위치 초기화" + "resetPosition": "위치 초기화", + "ballSize": "공 크기", + "sizeSmall": "작게", + "sizeMedium": "보통", + "sizeLarge": "크게", + "gestureBindings": "제스처 바인딩", + "bindingCleared": "미지정", + "favoriteAgents": "즐겨쓰는 에이전트", + "favoriteAgentsHint": "2~3개의 에이전트를 선택하면 즐겨쓰기 제스처로 순환 전환됩니다.", + "favoriteAgentsEmpty": "사용 가능한 에이전트가 없습니다", + "panelTitle": "빠른 작업" + }, + "gestures": { + "tap": "Tap", + "doubleTap": "Double tap", + "longPress": "Long press", + "left": "Swipe left", + "right": "Swipe right", + "up": "Swipe up", + "upLeft": "Up-left", + "upRight": "Up-right", + "upThenLeft": "Up, then left", + "upThenRight": "Up, then right", + "upDown": "Up, then down", + "leftThenRight": "Left, then right", + "rightThenLeft": "Right, then left", + "upLeftThenDown": "Up-left, then down", + "upRightThenDown": "Up-right, then down", + "down": "Swipe down", + "downLeft": "Down-left", + "downRight": "Down-right", + "downUp": "Down, then up", + "upThenDownRight": "Up, then down-right", + "downThenUpRight": "Down, then up-right", + "rightThenUp": "Right, then up", + "rightThenDown": "Right, then down", + "downThenRight": "Down, then right", + "downThenLeft": "Down, then left", + "downRightThenUp": "Down-right, then up", + "downLeftThenUp": "Down-left, then up", + "upLeftThenRight": "Up-left, then right", + "upRightThenLeft": "Up-right, then left" + }, + "actions": { + "newConversation": "New conversation", + "toggleSidebar": "Toggle sidebar", + "toggleSearch": "Search", + "toggleTerminal": "Toggle terminal", + "nextTab": "Next tab", + "prevTab": "Previous tab", + "reopenClosedTab": "Reopen closed tab", + "openFolder": "Open folder", + "zoomIn": "Zoom in", + "zoomOut": "Zoom out", + "zoomReset": "Reset zoom", + "stopGenerating": "Stop generating", + "openSettings": "Settings", + "openTodos": "To-dos", + "openTodosNew": "New to-do", + "openAutomations": "Automations", + "openCanvas": "Canvas", + "openAgents": "Agents", + "openSystemAdmin": "System admin", + "renameConversation": "Rename conversation", + "openSessionFolder": "Session files", + "cycleAgent": "Next agent", + "cycleFavoriteAgent": "Favorite agents" } }, "SystemSettings": { @@ -7357,7 +7423,17 @@ "zoomOut": "축소", "zoomReset": "확대/축소 초기화", "stopGenerating": "생성 중지", - "openSettings": "설정" + "openSettings": "설정", + "openTodos": "할 일", + "openTodosNew": "새 할 일", + "openAutomations": "자동화", + "openCanvas": "캔버스", + "openAgents": "에이전트", + "openSystemAdmin": "시스템 관리", + "renameConversation": "대화 이름 바꾸기", + "openSessionFolder": "세션 파일", + "cycleAgent": "다음 에이전트", + "cycleFavoriteAgent": "즐겨쓰는 에이전트" }, "gestures": { "tap": "탭", @@ -7374,7 +7450,22 @@ "leftThenRight": "왼쪽 → 오른쪽", "rightThenLeft": "오른쪽 → 왼쪽", "upLeftThenDown": "왼쪽 위 → 아래", - "upRightThenDown": "오른쪽 위 → 아래" - } + "upRightThenDown": "오른쪽 위 → 아래", + "down": "아래로", + "downLeft": "왼쪽 아래", + "downRight": "오른쪽 아래", + "downUp": "아래→위", + "upThenDownRight": "위→오른아래", + "downThenUpRight": "아래→오른위", + "rightThenUp": "오른→위", + "rightThenDown": "오른→아래", + "downThenRight": "아래→오른", + "downThenLeft": "아래→왼", + "downRightThenUp": "오른아래→위", + "downLeftThenUp": "왼아래→위", + "upLeftThenRight": "왼위→오른", + "upRightThenLeft": "오른위→왼" + }, + "favoriteAgentsEmpty": "사용 가능한 에이전트가 없습니다" } } diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 3d99db6a5a..01de443c45 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -245,7 +245,73 @@ "sectionDescription": "Uma bola arrastável sobre o espaço de trabalho que transforma deslizes em atalhos — feita para o telefone, onde não há teclado.", "enable": "Mostrar bola flutuante", "hint": "Arraste para mover, deslize rápido para um gesto, toque e segure para o painel e a lista de gestos.", - "resetPosition": "Redefinir posição" + "resetPosition": "Redefinir posição", + "ballSize": "Tamanho da bola", + "sizeSmall": "Pequeno", + "sizeMedium": "Médio", + "sizeLarge": "Grande", + "gestureBindings": "Atribuição de gestos", + "bindingCleared": "Sem atribuição", + "favoriteAgents": "Agentes favoritos", + "favoriteAgentsHint": "Escolha 2-3 agentes para alternar com o gesto de favoritos.", + "favoriteAgentsEmpty": "Nenhum agente disponível", + "panelTitle": "Ações rápidas" + }, + "gestures": { + "tap": "Tap", + "doubleTap": "Double tap", + "longPress": "Long press", + "left": "Swipe left", + "right": "Swipe right", + "up": "Swipe up", + "upLeft": "Up-left", + "upRight": "Up-right", + "upThenLeft": "Up, then left", + "upThenRight": "Up, then right", + "upDown": "Up, then down", + "leftThenRight": "Left, then right", + "rightThenLeft": "Right, then left", + "upLeftThenDown": "Up-left, then down", + "upRightThenDown": "Up-right, then down", + "down": "Swipe down", + "downLeft": "Down-left", + "downRight": "Down-right", + "downUp": "Down, then up", + "upThenDownRight": "Up, then down-right", + "downThenUpRight": "Down, then up-right", + "rightThenUp": "Right, then up", + "rightThenDown": "Right, then down", + "downThenRight": "Down, then right", + "downThenLeft": "Down, then left", + "downRightThenUp": "Down-right, then up", + "downLeftThenUp": "Down-left, then up", + "upLeftThenRight": "Up-left, then right", + "upRightThenLeft": "Up-right, then left" + }, + "actions": { + "newConversation": "New conversation", + "toggleSidebar": "Toggle sidebar", + "toggleSearch": "Search", + "toggleTerminal": "Toggle terminal", + "nextTab": "Next tab", + "prevTab": "Previous tab", + "reopenClosedTab": "Reopen closed tab", + "openFolder": "Open folder", + "zoomIn": "Zoom in", + "zoomOut": "Zoom out", + "zoomReset": "Reset zoom", + "stopGenerating": "Stop generating", + "openSettings": "Settings", + "openTodos": "To-dos", + "openTodosNew": "New to-do", + "openAutomations": "Automations", + "openCanvas": "Canvas", + "openAgents": "Agents", + "openSystemAdmin": "System admin", + "renameConversation": "Rename conversation", + "openSessionFolder": "Session files", + "cycleAgent": "Next agent", + "cycleFavoriteAgent": "Favorite agents" } }, "SystemSettings": { @@ -7357,7 +7423,17 @@ "zoomOut": "Diminuir zoom", "zoomReset": "Redefinir zoom", "stopGenerating": "Parar geração", - "openSettings": "Configurações" + "openSettings": "Configurações", + "openTodos": "Tarefas", + "openTodosNew": "Nova tarefa", + "openAutomations": "Automações", + "openCanvas": "Canvas", + "openAgents": "Agentes", + "openSystemAdmin": "Administração", + "renameConversation": "Renomear conversa", + "openSessionFolder": "Arquivos da sessão", + "cycleAgent": "Próximo agente", + "cycleFavoriteAgent": "Agentes favoritos" }, "gestures": { "tap": "Toque", @@ -7374,7 +7450,22 @@ "leftThenRight": "Esquerda, depois direita", "rightThenLeft": "Direita, depois esquerda", "upLeftThenDown": "Cima-esquerda, depois baixo", - "upRightThenDown": "Cima-direita, depois baixo" - } + "upRightThenDown": "Cima-direita, depois baixo", + "down": "Deslizar para baixo", + "downLeft": "Baixo-esq.", + "downRight": "Baixo-dir.", + "downUp": "Baixo, depois cima", + "upThenDownRight": "Cima, depois baixo-dir.", + "downThenUpRight": "Baixo, depois cima-dir.", + "rightThenUp": "Direita, depois cima", + "rightThenDown": "Direita, depois baixo", + "downThenRight": "Baixo, depois direita", + "downThenLeft": "Baixo, depois esquerda", + "downRightThenUp": "Baixo-dir., depois cima", + "downLeftThenUp": "Baixo-esq., depois cima", + "upLeftThenRight": "Cima-esq., depois direita", + "upRightThenLeft": "Cima-dir., depois esquerda" + }, + "favoriteAgentsEmpty": "Nenhum agente disponível" } } diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 6155947c3e..9e2d16d906 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -245,7 +245,73 @@ "sectionDescription": "工作区上的可拖动悬浮球,把滑动手势变成快捷键——为没有键盘的手机而设。", "enable": "显示悬浮球", "hint": "拖动可移动;快速滑动触发手势;长按打开面板与手势表。", - "resetPosition": "重置位置" + "resetPosition": "重置位置", + "ballSize": "球体大小", + "sizeSmall": "小", + "sizeMedium": "中", + "sizeLarge": "大", + "gestureBindings": "手势绑定", + "bindingCleared": "未绑定", + "favoriteAgents": "常用智能体", + "favoriteAgentsHint": "选择 2-3 个智能体,供「常用智能体」手势循环快切。", + "favoriteAgentsEmpty": "暂无可用智能体", + "panelTitle": "快捷操作" + }, + "gestures": { + "tap": "Tap", + "doubleTap": "Double tap", + "longPress": "Long press", + "left": "Swipe left", + "right": "Swipe right", + "up": "Swipe up", + "upLeft": "Up-left", + "upRight": "Up-right", + "upThenLeft": "Up, then left", + "upThenRight": "Up, then right", + "upDown": "Up, then down", + "leftThenRight": "Left, then right", + "rightThenLeft": "Right, then left", + "upLeftThenDown": "Up-left, then down", + "upRightThenDown": "Up-right, then down", + "down": "Swipe down", + "downLeft": "Down-left", + "downRight": "Down-right", + "downUp": "Down, then up", + "upThenDownRight": "Up, then down-right", + "downThenUpRight": "Down, then up-right", + "rightThenUp": "Right, then up", + "rightThenDown": "Right, then down", + "downThenRight": "Down, then right", + "downThenLeft": "Down, then left", + "downRightThenUp": "Down-right, then up", + "downLeftThenUp": "Down-left, then up", + "upLeftThenRight": "Up-left, then right", + "upRightThenLeft": "Up-right, then left" + }, + "actions": { + "newConversation": "New conversation", + "toggleSidebar": "Toggle sidebar", + "toggleSearch": "Search", + "toggleTerminal": "Toggle terminal", + "nextTab": "Next tab", + "prevTab": "Previous tab", + "reopenClosedTab": "Reopen closed tab", + "openFolder": "Open folder", + "zoomIn": "Zoom in", + "zoomOut": "Zoom out", + "zoomReset": "Reset zoom", + "stopGenerating": "Stop generating", + "openSettings": "Settings", + "openTodos": "To-dos", + "openTodosNew": "New to-do", + "openAutomations": "Automations", + "openCanvas": "Canvas", + "openAgents": "Agents", + "openSystemAdmin": "System admin", + "renameConversation": "Rename conversation", + "openSessionFolder": "Session files", + "cycleAgent": "Next agent", + "cycleFavoriteAgent": "Favorite agents" } }, "SystemSettings": { @@ -7357,7 +7423,17 @@ "zoomOut": "缩小", "zoomReset": "重置缩放", "stopGenerating": "停止生成", - "openSettings": "设置" + "openSettings": "设置", + "openTodos": "待办任务", + "openTodosNew": "新建待办", + "openAutomations": "自动化", + "openCanvas": "无限画布", + "openAgents": "智能体", + "openSystemAdmin": "系统管理", + "renameConversation": "重命名会话", + "openSessionFolder": "会话文件", + "cycleAgent": "下一个智能体", + "cycleFavoriteAgent": "常用智能体" }, "gestures": { "tap": "单击", @@ -7374,7 +7450,22 @@ "leftThenRight": "左滑后右滑", "rightThenLeft": "右滑后左滑", "upLeftThenDown": "左上滑后下滑", - "upRightThenDown": "右上滑后下滑" - } + "upRightThenDown": "右上滑后下滑", + "down": "下滑", + "downLeft": "左下", + "downRight": "右下", + "downUp": "下再上", + "upThenDownRight": "上再右下", + "downThenUpRight": "下再右上", + "rightThenUp": "右再上", + "rightThenDown": "右再下", + "downThenRight": "下再右", + "downThenLeft": "下再左", + "downRightThenUp": "右下再上", + "downLeftThenUp": "左下再上", + "upLeftThenRight": "左上再右", + "upRightThenLeft": "右上再左" + }, + "favoriteAgentsEmpty": "暂无可用智能体" } } diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index edb89163ab..bc88edf1ff 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -245,7 +245,73 @@ "sectionDescription": "工作區上的可拖動懸浮球,把滑動手勢變成快捷鍵——為沒有鍵盤的手機而設。", "enable": "顯示懸浮球", "hint": "拖動可移動;快速滑動觸發手勢;長按打開面板與手勢表。", - "resetPosition": "重設位置" + "resetPosition": "重設位置", + "ballSize": "球體大小", + "sizeSmall": "小", + "sizeMedium": "中", + "sizeLarge": "大", + "gestureBindings": "手勢綁定", + "bindingCleared": "未綁定", + "favoriteAgents": "常用代理人", + "favoriteAgentsHint": "選擇 2-3 個代理人,供「常用代理人」手勢循環快切。", + "favoriteAgentsEmpty": "暫無可用代理人", + "panelTitle": "快捷操作" + }, + "gestures": { + "tap": "Tap", + "doubleTap": "Double tap", + "longPress": "Long press", + "left": "Swipe left", + "right": "Swipe right", + "up": "Swipe up", + "upLeft": "Up-left", + "upRight": "Up-right", + "upThenLeft": "Up, then left", + "upThenRight": "Up, then right", + "upDown": "Up, then down", + "leftThenRight": "Left, then right", + "rightThenLeft": "Right, then left", + "upLeftThenDown": "Up-left, then down", + "upRightThenDown": "Up-right, then down", + "down": "Swipe down", + "downLeft": "Down-left", + "downRight": "Down-right", + "downUp": "Down, then up", + "upThenDownRight": "Up, then down-right", + "downThenUpRight": "Down, then up-right", + "rightThenUp": "Right, then up", + "rightThenDown": "Right, then down", + "downThenRight": "Down, then right", + "downThenLeft": "Down, then left", + "downRightThenUp": "Down-right, then up", + "downLeftThenUp": "Down-left, then up", + "upLeftThenRight": "Up-left, then right", + "upRightThenLeft": "Up-right, then left" + }, + "actions": { + "newConversation": "New conversation", + "toggleSidebar": "Toggle sidebar", + "toggleSearch": "Search", + "toggleTerminal": "Toggle terminal", + "nextTab": "Next tab", + "prevTab": "Previous tab", + "reopenClosedTab": "Reopen closed tab", + "openFolder": "Open folder", + "zoomIn": "Zoom in", + "zoomOut": "Zoom out", + "zoomReset": "Reset zoom", + "stopGenerating": "Stop generating", + "openSettings": "Settings", + "openTodos": "To-dos", + "openTodosNew": "New to-do", + "openAutomations": "Automations", + "openCanvas": "Canvas", + "openAgents": "Agents", + "openSystemAdmin": "System admin", + "renameConversation": "Rename conversation", + "openSessionFolder": "Session files", + "cycleAgent": "Next agent", + "cycleFavoriteAgent": "Favorite agents" } }, "SystemSettings": { @@ -7357,7 +7423,17 @@ "zoomOut": "縮小", "zoomReset": "重設縮放", "stopGenerating": "停止生成", - "openSettings": "設定" + "openSettings": "設定", + "openTodos": "待辦任務", + "openTodosNew": "新建待辦", + "openAutomations": "自動化", + "openCanvas": "無限畫布", + "openAgents": "智慧代理人", + "openSystemAdmin": "系統管理", + "renameConversation": "重新命名會話", + "openSessionFolder": "會話檔案", + "cycleAgent": "下一個代理人", + "cycleFavoriteAgent": "常用代理人" }, "gestures": { "tap": "單擊", @@ -7374,7 +7450,22 @@ "leftThenRight": "左滑後右滑", "rightThenLeft": "右滑後左滑", "upLeftThenDown": "左上滑後下滑", - "upRightThenDown": "右上滑後下滑" - } + "upRightThenDown": "右上滑後下滑", + "down": "下滑", + "downLeft": "左下", + "downRight": "右下", + "downUp": "下再上", + "upThenDownRight": "上再右下", + "downThenUpRight": "下再右上", + "rightThenUp": "右再上", + "rightThenDown": "右再下", + "downThenRight": "下再右", + "downThenLeft": "下再左", + "downRightThenUp": "右下再上", + "downLeftThenUp": "左下再上", + "upLeftThenRight": "左上再右", + "upRightThenLeft": "右上再左" + }, + "favoriteAgentsEmpty": "暫無可用代理人" } } diff --git a/src/lib/appearance-script.ts b/src/lib/appearance-script.ts index de1ce4836c..4d9f2475d8 100644 --- a/src/lib/appearance-script.ts +++ b/src/lib/appearance-script.ts @@ -34,6 +34,12 @@ export const STORAGE_KEY_CHAT_CONTENT_WIDTH = "codeg-chat-content-width" // 预水合——水合前根本不渲染球。 export const STORAGE_KEY_FLOATING_BALL_ENABLED = "codeg-floating-ball-enabled" export const STORAGE_KEY_FLOATING_BALL_POSITION = "codeg-floating-ball-position" +// 批v82:手势→动作绑定表(JSON,gesture → action | null;null 表示用户显式清除, +// 与"缺 key 回退默认"语义不同)、球体三档大小、常用 agent 快切列表。同样无需预水合。 +export const STORAGE_KEY_FLOATING_BALL_BINDINGS = "codeg-floating-ball-bindings" +export const STORAGE_KEY_FLOATING_BALL_SIZE = "codeg-floating-ball-size" +export const STORAGE_KEY_FLOATING_BALL_FAVORITE_AGENTS = + "codeg-floating-ball-favorite-agents" // 字体偏好(界面 / 编辑器 / 终端)。 // 只有界面字体需要 *_STACK(已解析的 CSS font-family 栈),供 inline 脚本零依赖地 diff --git a/src/lib/floating-ball-gestures.test.ts b/src/lib/floating-ball-gestures.test.ts index 3b0f5345bc..449326301a 100644 --- a/src/lib/floating-ball-gestures.test.ts +++ b/src/lib/floating-ball-gestures.test.ts @@ -40,8 +40,7 @@ function run(points: [number, number][]): FloatingBallGesture | null { const tracker = createFloatingBallTracker() tracker.start(points[0][0], points[0][1]) for (const point of points.slice(1)) tracker.move(point[0], point[1]) - const last = points[points.length - 1] - return tracker.end(last[0], last[1]) + return tracker.end() } const S = SWIPE_PX @@ -144,7 +143,7 @@ describe("two-stage trails", () => { expect(trail([0, -50], [60, -50])).toBe("upThenRight") expect(trail([-40, -40], [-40, 40])).toBe("upLeftThenDown") expect(trail([40, -40], [40, 40])).toBe("upRightThenDown") - expect(trail([0, -50], [(S + 10), (S + 10)])).toBe("upThenDownRight") + expect(trail([0, -50], [S + 10, S + 10])).toBe("upThenDownRight") }) it("folds the batch-v82 downward and horizontal starts", () => { @@ -155,9 +154,7 @@ describe("two-stage trails", () => { expect(trail([50, 0], [50, S + 20])).toBe("rightThenDown") expect(trail([0, 50], [S + 20, 50])).toBe("downThenRight") expect(trail([0, 50], [-(S + 20), 50])).toBe("downThenLeft") - expect(trail([S + 10, S + 10], [S + 10, -(S + 20)])).toBe( - "downRightThenUp" - ) + expect(trail([S + 10, S + 10], [S + 10, -(S + 20)])).toBe("downRightThenUp") expect(trail([-(S + 10), S + 10], [-(S + 10), -(S + 20)])).toBe( "downLeftThenUp" ) diff --git a/src/lib/floating-ball-gestures.ts b/src/lib/floating-ball-gestures.ts index 6e58f98522..30da2f4ac7 100644 --- a/src/lib/floating-ball-gestures.ts +++ b/src/lib/floating-ball-gestures.ts @@ -257,13 +257,28 @@ function singleStage(stage: Segment): FloatingBallGesture | null { } } +/** + * Name the gesture the trail would commit right now, loose-matched — the hint + * pill's source of truth. Mirrors the tracker's own folding, so the pill and + * the commit can never disagree. An unfinished two-stage trail falls back to + * its last stage so the pill shows what a stop right there would fire. + */ +export function previewGesture( + trail: readonly Segment[] +): FloatingBallGesture | null { + if (trail.length === 0) return null + const folded = foldTrail(trail) + if (folded) return folded + return singleStage(trail[trail.length - 1]) +} + export interface FloatingBallTracker { /** Touchdown. */ start(x: number, y: number): void /** Every pointer sample while the press is down. */ move(x: number, y: number): void /** Lift-off: the path gesture, or null when this press was not one. */ - end(x: number, y: number): FloatingBallGesture | null + end(): FloatingBallGesture | null /** Whether the press has travelled far enough to be a gesture at all. */ readonly moved: boolean /** Stages recognised so far, oldest first. Exposed for tests and debugging. */ @@ -329,7 +344,7 @@ export function createFloatingBallTracker(): FloatingBallTracker { anchorX = x anchorY = y }, - end(x, y) { + end(): FloatingBallGesture | null { if (!moved) return null // A completed trail is judged on its stages, not on how far the finger // ended from the origin: round trips (↓↑, ↑↓) naturally land back where From f82b86a835f15a6abd4d39dc9469f921c9b3019a Mon Sep 17 00:00:00 2001 From: fengx Date: Sun, 18 Oct 2026 00:44:36 +0800 Subject: [PATCH 7/9] =?UTF-8?q?feat(floating-ball):=20batch=20v82=20P3=20?= =?UTF-8?q?=E2=80=94=20wire=2010=20new=20actions=20with=20guards=20and=20f?= =?UTF-8?q?eedback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - openTodos/openTodosNew/openAutomations/openCanvas route the workbench; openTodosNew parks a task draft so the composer opens on an already-active tasks route too - openAgents/openSystemAdmin open the settings window at the section - renameConversation dispatches a global event carrying the DB id; the conversation header whose id matches opens its rename dialog (multi-tab safe); drafts get a toast instead - openSessionFolder opens the aux file tree, guarded on an active folder - cycleAgent cycles enabled+available agents on the active draft tab via confirmDraftAgent; on a saved conversation it opens a fresh draft pinned (forceAgent) to the next agent; cycleFavoriteAgent restricts the cycle to the user's favorite list from floating-ball settings - FloatingBall.toasts i18n keys across all 12 locale files --- .../conversation-detail-header.tsx | 19 +- .../workspace-chrome-controller.test.tsx | 166 +++++++++++++++++- .../layout/workspace-chrome-controller.tsx | 119 ++++++++++++- src/i18n/messages/ar.json | 7 +- src/i18n/messages/de.json | 7 +- src/i18n/messages/en.json | 7 +- src/i18n/messages/es.json | 7 +- src/i18n/messages/fr.json | 7 +- src/i18n/messages/ja.json | 7 +- src/i18n/messages/ko.json | 7 +- src/i18n/messages/pt.json | 7 +- src/i18n/messages/zh-CN.json | 7 +- src/i18n/messages/zh-TW.json | 7 +- src/lib/rename-conversation-events.ts | 17 ++ 14 files changed, 370 insertions(+), 21 deletions(-) create mode 100644 src/lib/rename-conversation-events.ts diff --git a/src/components/conversations/conversation-detail-header.tsx b/src/components/conversations/conversation-detail-header.tsx index a24c3bc7a4..4f1c8717d1 100644 --- a/src/components/conversations/conversation-detail-header.tsx +++ b/src/components/conversations/conversation-detail-header.tsx @@ -1,6 +1,6 @@ "use client" -import { memo, useCallback, useState } from "react" +import { memo, useCallback, useEffect, useState } from "react" import { ChevronRight, Circle, @@ -21,6 +21,7 @@ import { updateConversationTitle, } from "@/lib/api" import { formatConversationTitle } from "@/lib/conversation-title" +import { RENAME_CONVERSATION_EVENT } from "@/lib/rename-conversation-events" import { ConversationHeaderFolderPicker } from "@/components/chat/conversation-context-bar" import { useAppWorkspaceStore } from "@/stores/app-workspace-store" import { useTabActions } from "@/contexts/tab-context" @@ -211,6 +212,22 @@ export const ConversationDetailHeader = memo(function ConversationDetailHeader({ setRenameTarget(null) }, [renameTarget, renameValue, refreshConversations]) + // Floating-ball "rename session": the event carries the DB id and every + // mounted header instance hears it — only the one whose conversation matches + // opens its dialog (multi-tab safe). Drafts (id null) have nothing to rename. + useEffect(() => { + if (conversationId == null) return + const onRename = (e: Event) => { + const id = (e as CustomEvent<{ conversationId: number }>).detail + ?.conversationId + if (id !== conversationId || renameTarget != null) return + setRenameValue(title || "") + setRenameTarget({ id: conversationId, title }) + } + window.addEventListener(RENAME_CONVERSATION_EVENT, onRename) + return () => window.removeEventListener(RENAME_CONVERSATION_EVENT, onRename) + }, [conversationId, renameTarget, title]) + const handleStatusChange = useCallback( (next: ConversationStatus) => { if (conversationId == null) return diff --git a/src/components/layout/workspace-chrome-controller.test.tsx b/src/components/layout/workspace-chrome-controller.test.tsx index 5b1a377fb2..6c7f70294a 100644 --- a/src/components/layout/workspace-chrome-controller.test.tsx +++ b/src/components/layout/workspace-chrome-controller.test.tsx @@ -25,6 +25,15 @@ const state = vi.hoisted(() => ({ desktop: true, shortcuts: {} as Record, listeners: new Map void>(), + // Floating-ball action context: the active tab row and the agent universe. + activeTab: null as { + id: string + conversationId: number | null + agentType: string + } | null, + sortedAgentTypes: ["zcode", "codex"] as string[], + favoriteAgents: [] as string[], + activeFolder: { id: 1, path: "/repo" } as { id: number; path: string } | null, })) const spies = vi.hoisted(() => ({ @@ -36,9 +45,21 @@ const spies = vi.hoisted(() => ({ setActivePane: vi.fn(), closeCurrentWindow: vi.fn(() => Promise.resolve()), subscribe: vi.fn(), + setRoute: vi.fn(), + openSettingsWindow: vi.fn(() => Promise.resolve()), + auxOpenTab: vi.fn(), + confirmDraftAgent: vi.fn(), + openChatModeTab: vi.fn(), + openNewConversationTab: vi.fn(), })) -vi.mock("@/lib/api", () => ({ openSettingsWindow: vi.fn() })) +/** Captured from the (stubbed) FloatingBall: the controller's action handler. */ +let ballAction: ((action: string) => void) | null = null + +vi.mock("@/lib/api", () => ({ openSettingsWindow: spies.openSettingsWindow })) +vi.mock("next-intl", () => ({ + useTranslations: () => (key: string) => key, +})) // The floating ball reads the active ACP connection (to stop a turn) and the // zoom level. Both pull provider modules this test does not exercise, and the // ACP context imports the workspace store in a shape the stub below does not @@ -50,12 +71,25 @@ vi.mock("@/contexts/acp-connections-context", () => ({ })) vi.mock("@/hooks/use-appearance", () => ({ useZoomLevel: () => ({ zoomLevel: 100, setZoomLevel: vi.fn() }), + useFloatingBall: () => ({ + floatingBallFavoriteAgents: state.favoriteAgents, + }), +})) +vi.mock("@/hooks/use-sorted-available-agents", () => ({ + useSortedAvailableAgents: () => ({ + sortedTypes: state.sortedAgentTypes, + fresh: true, + refresh: () => Promise.resolve(), + }), })) vi.mock("@/components/layout/floating-ball", () => ({ - FloatingBall: () => null, + FloatingBall: ({ onAction }: { onAction: (action: string) => void }) => { + ballAction = onAction + return null + }, })) vi.mock("@/contexts/active-folder-context", () => ({ - useActiveFolder: () => ({ activeFolder: { id: 1, path: "/repo" } }), + useActiveFolder: () => ({ activeFolder: state.activeFolder }), })) vi.mock("@/hooks/use-is-active-chat-mode", () => ({ useIsActiveChatMode: () => false, @@ -64,23 +98,32 @@ vi.mock("@/contexts/sidebar-context", () => ({ useSidebarContext: () => ({ toggle: vi.fn() }), })) vi.mock("@/contexts/aux-panel-context", () => ({ - useAuxPanelContext: () => ({ toggle: vi.fn() }), + useAuxPanelContext: () => ({ toggle: vi.fn(), openTab: spies.auxOpenTab }), })) vi.mock("@/contexts/terminal-context", () => ({ useTerminalContext: () => ({ toggle: vi.fn() }), })) vi.mock("@/contexts/tab-context", () => ({ useTabActions: () => ({ - openNewConversationTab: vi.fn(), + openNewConversationTab: spies.openNewConversationTab, openTab: vi.fn(), switchTab: spies.switchTab, closeTab: spies.closeTab, + confirmDraftAgent: spies.confirmDraftAgent, + openChatModeTab: spies.openChatModeTab, }), useTabStore: ( - selector: (s: { tabs: { id: string }[]; activeTabId: string | null }) => T + selector: (s: { + tabs: { id: string; conversationId: number | null; agentType: string }[] + activeTabId: string | null + }) => T ) => selector({ - tabs: state.activeTabId ? [{ id: state.activeTabId }] : [], + tabs: state.activeTabId + ? state.activeTab + ? [state.activeTab] + : [{ id: state.activeTabId, conversationId: 7, agentType: "zcode" }] + : [], activeTabId: state.activeTabId, }), })) @@ -105,7 +148,10 @@ vi.mock("@/contexts/workspace-context", () => ({ }), })) vi.mock("@/contexts/workbench-route-context", () => ({ - useWorkbenchRoute: () => ({ openConversations: vi.fn() }), + useWorkbenchRoute: () => ({ + openConversations: vi.fn(), + setRoute: spies.setRoute, + }), })) vi.mock("@/contexts/search-dialog-context", () => ({ useSearchDialog: () => ({ open: false, setOpen: vi.fn() }), @@ -182,6 +228,16 @@ beforeEach(() => { return () => state.listeners.delete(event) } ) + spies.setRoute.mockClear() + spies.openSettingsWindow.mockClear() + spies.auxOpenTab.mockClear() + spies.confirmDraftAgent.mockClear() + spies.openChatModeTab.mockClear() + spies.openNewConversationTab.mockClear() + state.activeTab = { id: "conv-1", conversationId: 7, agentType: "zcode" } + state.sortedAgentTypes = ["zcode", "codex"] + state.favoriteAgents = [] + state.activeFolder = { id: 1, path: "/repo" } }) afterEach(() => { @@ -381,3 +437,97 @@ describe("WorkspaceChromeController — ⌘W the page hears", () => { expect(event.defaultPrevented).toBe(false) }) }) + +describe("WorkspaceChromeController — floating-ball actions", () => { + function fire(action: string): void { + render() + expect(typeof ballAction).toBe("function") + ballAction?.(action) + } + + it("routes the workspace actions", async () => { + fire("openTodos") + expect(spies.setRoute).toHaveBeenCalledWith("tasks") + fire("openAutomations") + expect(spies.setRoute).toHaveBeenCalledWith("automations") + fire("openCanvas") + expect(spies.setRoute).toHaveBeenCalledWith("canvas") + fire("openAgents") + await waitFor(() => + expect(spies.openSettingsWindow).toHaveBeenCalledWith("agents") + ) + fire("openSystemAdmin") + await waitFor(() => + expect(spies.openSettingsWindow).toHaveBeenCalledWith("system") + ) + }) + + it("opens the new-to-do composer even when the tasks route is already active", () => { + const received: CustomEvent[] = [] + const listener = (e: Event) => received.push(e as CustomEvent) + window.addEventListener("codeg:create-task-from-text", listener) + fire("openTodosNew") + window.removeEventListener("codeg:create-task-from-text", listener) + expect(spies.setRoute).toHaveBeenCalledWith("tasks") + expect(received).toHaveLength(1) + }) + + it("renames the active tab's conversation by id", () => { + const received: unknown[] = [] + const listener = (e: Event) => received.push((e as CustomEvent).detail) + window.addEventListener("codeg:rename-conversation", listener) + fire("renameConversation") + window.removeEventListener("codeg:rename-conversation", listener) + expect(received).toEqual([{ conversationId: 7 }]) + }) + + it("opens the file tree only with a workspace folder", () => { + fire("openSessionFolder") + expect(spies.auxOpenTab).toHaveBeenCalledWith("file_tree") + + state.activeFolder = null + fire("openSessionFolder") + expect(spies.auxOpenTab).toHaveBeenCalledTimes(1) + }) + + it("cycles the active draft tab's agent and confirms it", () => { + state.activeTab = { + id: "conv-1", + conversationId: null, + agentType: "zcode", + } + fire("cycleAgent") + expect(spies.confirmDraftAgent).toHaveBeenCalledWith("conv-1", "codex") + // The store would have applied the switch; reflect it like the real tab + // row would, so the next stroke continues from codex. + state.activeTab = { + id: "conv-1", + conversationId: null, + agentType: "codex", + } + fire("cycleAgent") + expect(spies.confirmDraftAgent).toHaveBeenLastCalledWith("conv-1", "zcode") + }) + + it("pins a new draft to the next agent when the active tab is saved", () => { + fire("cycleAgent") + expect(spies.confirmDraftAgent).not.toHaveBeenCalled() + expect(spies.openNewConversationTab).toHaveBeenCalledWith( + 1, + "/repo", + expect.objectContaining({ forceAgent: "codex" }) + ) + }) + + it("cycles only within the favorite list for cycleFavoriteAgent", () => { + state.sortedAgentTypes = ["zcode", "codex", "cline"] + state.favoriteAgents = ["codex", "cline"] + fire("cycleFavoriteAgent") + // Current tab agent zcode is not in the list → head of the list. + expect(spies.openNewConversationTab).toHaveBeenCalledWith( + 1, + "/repo", + expect.objectContaining({ forceAgent: "codex" }) + ) + }) +}) diff --git a/src/components/layout/workspace-chrome-controller.tsx b/src/components/layout/workspace-chrome-controller.tsx index 3155eea62c..54a290df78 100644 --- a/src/components/layout/workspace-chrome-controller.tsx +++ b/src/components/layout/workspace-chrome-controller.tsx @@ -6,13 +6,16 @@ import { useAcpActions, useConnectionStore, } from "@/contexts/acp-connections-context" -import { useZoomLevel } from "@/hooks/use-appearance" +import { useZoomLevel, useFloatingBall } from "@/hooks/use-appearance" import { useActiveFolder } from "@/contexts/active-folder-context" import { useIsActiveChatMode } from "@/hooks/use-is-active-chat-mode" import { useSidebarContext } from "@/contexts/sidebar-context" import { useAuxPanelContext } from "@/contexts/aux-panel-context" import { useTerminalContext } from "@/contexts/terminal-context" +import { useSortedAvailableAgents } from "@/hooks/use-sorted-available-agents" import { useTabActions, useTabStore } from "@/contexts/tab-context" +import { requestCreateTaskFromText } from "@/lib/task-compose-events" +import { requestRenameConversation } from "@/lib/rename-conversation-events" import { useWorkspaceActions, useWorkspaceFileTabs, @@ -47,8 +50,28 @@ import { SearchCommandDialog } from "@/components/conversations/search-command-d import { FloatingBall } from "@/components/layout/floating-ball" import { WorkspaceFolderDialog } from "@/components/layout/workspace-folder-dialog" import { stepZoom, DEFAULT_ZOOM_LEVEL } from "@/lib/theme-presets" +import { toast } from "sonner" +import { useTranslations } from "next-intl" +import type { AgentType } from "@/lib/types" import type { FloatingBallAction } from "@/lib/floating-ball-gestures" +/** + * Next entry in a cycling list. With a current agent it picks the following + * one (wrapping); without one — or when the current agent has since dropped + * out of the list — it starts from the head. The favorites list re-reads its + * configured order on every stroke, so cycling there follows what settings + * shows rather than a stale snapshot. + */ +function nextInCycle( + current: AgentType | undefined, + list: AgentType[] +): AgentType { + if (list.length === 0) return current ?? list[0] + if (current == null || !list.includes(current)) return list[0] + const index = list.indexOf(current) + return list[(index + 1) % list.length] +} + /** * Headless owner of the workspace's global keyboard shortcuts and the two * dialogs the shortcuts summon (search, remote directory browser). These used @@ -58,6 +81,7 @@ import type { FloatingBallAction } from "@/lib/floating-ball-gestures" * visible bar. Renders no visible chrome — only the dialogs. */ export function WorkspaceChromeController() { + const tBall = useTranslations("FloatingBall") const { activeFolder } = useActiveFolder() const isChatMode = useIsActiveChatMode() const { toggle } = useSidebarContext() @@ -81,7 +105,7 @@ export function WorkspaceChromeController() { openBrowserTab, setActivePane, } = useWorkspaceActions() - const { openConversations } = useWorkbenchRoute() + const { openConversations, setRoute } = useWorkbenchRoute() const { shortcuts } = useShortcutSettings() const { zoomLevel, setZoomLevel } = useZoomLevel() // Stopping a turn is the one floating-ball action with no shortcut: the @@ -90,6 +114,16 @@ export function WorkspaceChromeController() { // what the workspace considers "the turn in front of the user". const { cancel } = useAcpActions() const { getActiveKey } = useConnectionStore() + // Agent cycling (floating ball "next agent" / "favorite agents") needs the + // same enabled+available list the draft tabs resolve their defaults from. + const { sortedTypes: sortedAvailableAgents } = useSortedAvailableAgents() + const { confirmDraftAgent, openChatModeTab } = useTabActions() + // Session files ("open the session folder") opens the aux panel's file tree; + // no active folder means there is nothing to reveal (same guard as the + // toggle-files keyboard shortcut below). + const auxPanel = useAuxPanelContext() + const activeTab = tabs.find((tab) => tab.id === activeTabId) + const { floatingBallFavoriteAgents: favoriteAgents } = useFloatingBall() // Search open-state is shared (see search-dialog-context): the trigger lives // in the sidebar, but this always-mounted controller owns the dialog and the // ⌘K shortcut so search works even when the sidebar is collapsed. @@ -499,23 +533,104 @@ export function WorkspaceChromeController() { case "openSettings": handleOpenSettings() return + case "openTodos": + setRoute("tasks") + return + case "openTodosNew": + // Park the draft before switching — the Tasks page consumes it on + // mount, and on the live event when the route is already active. + requestCreateTaskFromText({ + text: "", + folderId: activeFolder?.id ?? null, + }) + setRoute("tasks") + return + case "openAutomations": + setRoute("automations") + return + case "openCanvas": + setRoute("canvas") + return + case "openAgents": + void openSettingsWindow("agents").catch((err) => { + console.error("[WorkspaceChromeController] agents settings:", err) + }) + return + case "openSystemAdmin": + void openSettingsWindow("system").catch((err) => { + console.error("[WorkspaceChromeController] system settings:", err) + }) + return + case "renameConversation": { + const id = activeTab?.conversationId + if (id == null) { + toast.info(tBall("toasts.renameNeedsSavedSession")) + return + } + requestRenameConversation({ conversationId: id }) + return + } + case "openSessionFolder": + if (!activeFolder) { + toast.info(tBall("toasts.sessionFolderNeedsWorkspace")) + return + } + auxPanel.openTab("file_tree") + return + case "cycleAgent": + case "cycleFavoriteAgent": { + const list = + action === "cycleAgent" + ? sortedAvailableAgents + : sortedAvailableAgents.filter((a) => favoriteAgents.includes(a)) + if (list.length <= 1) { + toast.info(tBall("toasts.cycleNeedsMoreAgents")) + return + } + const tab = activeTab + // The switch retargets the draft's ACP connection via + // useConnectionLifecycle; a persisted conversation's agent is fixed, + // so open a fresh draft pinned to the next agent instead. + if (!tab || tab.conversationId != null) { + const next = nextInCycle(tab?.agentType, list) + if (activeFolder) { + openNewConversationTab(activeFolder.id, activeFolder.path, { + forceAgent: next, + }) + } else { + openChatModeTab({ forceAgent: next }) + } + return + } + const next = nextInCycle(tab.agentType, list) + confirmDraftAgent(tab.id, next) + return + } case "panel": return } }, [ activeFolder, + activeTab, activeTabId, + auxPanel, cancel, + confirmDraftAgent, + favoriteAgents, getActiveKey, handleOpenFolder, handleOpenSettings, + openChatModeTab, openConversations, openNewConversationTab, reopenClosedTab, setSearchOpen, + setRoute, setZoomLevel, + sortedAvailableAgents, switchTab, + tBall, tabs, toggle, toggleTerminal, diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index bb16902416..bf854b2c0e 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -7466,6 +7466,11 @@ "upLeftThenRight": "أعلى-يسار ثم يمين", "upRightThenLeft": "أعلى-يمين ثم يسار" }, - "favoriteAgentsEmpty": "لا يوجد وكلاء متاحون" + "favoriteAgentsEmpty": "لا يوجد وكلاء متاحون", + "toasts": { + "renameNeedsSavedSession": "إعادة التسمية تعمل على الجلسات المحفوظة فقط — أرسل رسالة أولاً.", + "sessionFolderNeedsWorkspace": "افتح مجلد مساحة العمل أولاً.", + "cycleNeedsMoreAgents": "وكيل واحد متاح فقط — لا يوجد ما يمكن التبديل إليه." + } } } diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index f1e38eb844..f9c30e597f 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -7466,6 +7466,11 @@ "upLeftThenRight": "Oben-li., dann rechts", "upRightThenLeft": "Oben-re., dann links" }, - "favoriteAgentsEmpty": "Keine Agenten verfügbar" + "favoriteAgentsEmpty": "Keine Agenten verfügbar", + "toasts": { + "renameNeedsSavedSession": "Umbenennen gilt nur für gespeicherte Sitzungen – senden Sie zuerst eine Nachricht.", + "sessionFolderNeedsWorkspace": "Öffnen Sie zuerst einen Arbeitsbereich-Ordner.", + "cycleNeedsMoreAgents": "Nur ein Agent verfügbar – nichts zum Wechseln." + } } } diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 4d9f43bf03..631faac6cf 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -7466,6 +7466,11 @@ "upLeftThenRight": "Up-left, then right", "upRightThenLeft": "Up-right, then left" }, - "favoriteAgentsEmpty": "No available agents" + "favoriteAgentsEmpty": "No available agents", + "toasts": { + "renameNeedsSavedSession": "Rename works on a saved session — send a message first.", + "sessionFolderNeedsWorkspace": "Open a workspace folder first.", + "cycleNeedsMoreAgents": "Only one agent available — nothing to cycle." + } } } diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 1a7fdba587..0734ca1a46 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -7466,6 +7466,11 @@ "upLeftThenRight": "Arriba-izq., luego derecha", "upRightThenLeft": "Arriba-der., luego izquierda" }, - "favoriteAgentsEmpty": "No hay agentes disponibles" + "favoriteAgentsEmpty": "No hay agentes disponibles", + "toasts": { + "renameNeedsSavedSession": "Renombrar solo funciona en sesiones guardadas: envía un mensaje primero.", + "sessionFolderNeedsWorkspace": "Abre antes una carpeta de espacio de trabajo.", + "cycleNeedsMoreAgents": "Solo hay un agente disponible: nada que alternar." + } } } diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index cfa49392eb..da1c5539bc 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -7466,6 +7466,11 @@ "upLeftThenRight": "Haut-g., puis droite", "upRightThenLeft": "Haut-dr., puis gauche" }, - "favoriteAgentsEmpty": "Aucun agent disponible" + "favoriteAgentsEmpty": "Aucun agent disponible", + "toasts": { + "renameNeedsSavedSession": "Renommer ne s'applique qu'aux sessions enregistrées — envoyez d'abord un message.", + "sessionFolderNeedsWorkspace": "Ouvrez d'abord un dossier d'espace de travail.", + "cycleNeedsMoreAgents": "Un seul agent disponible — rien à parcourir." + } } } diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index 25964435b8..c00a6584c7 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -7466,6 +7466,11 @@ "upLeftThenRight": "左上から右", "upRightThenLeft": "右上から左" }, - "favoriteAgentsEmpty": "利用可能なエージェントがありません" + "favoriteAgentsEmpty": "利用可能なエージェントがありません", + "toasts": { + "renameNeedsSavedSession": "名前の変更は保存済みセッションのみ有効です。まずメッセージを送信してください。", + "sessionFolderNeedsWorkspace": "先にワークスペースフォルダを開いてください。", + "cycleNeedsMoreAgents": "利用可能なエージェントが1つだけのため、切り替えできません。" + } } } diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index a430f35e7a..8e375dbd77 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -7466,6 +7466,11 @@ "upLeftThenRight": "왼위→오른", "upRightThenLeft": "오른위→왼" }, - "favoriteAgentsEmpty": "사용 가능한 에이전트가 없습니다" + "favoriteAgentsEmpty": "사용 가능한 에이전트가 없습니다", + "toasts": { + "renameNeedsSavedSession": "이름 바꾸기는 저장된 세션에만 적용됩니다. 먼저 메시지를 보내세요.", + "sessionFolderNeedsWorkspace": "먼저 작업 공간 폴더를 여세요.", + "cycleNeedsMoreAgents": "사용 가능한 에이전트가 하나뿐이어서 전환할 수 없습니다." + } } } diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 01de443c45..acc64d9b91 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -7466,6 +7466,11 @@ "upLeftThenRight": "Cima-esq., depois direita", "upRightThenLeft": "Cima-dir., depois esquerda" }, - "favoriteAgentsEmpty": "Nenhum agente disponível" + "favoriteAgentsEmpty": "Nenhum agente disponível", + "toasts": { + "renameNeedsSavedSession": "Renomear só funciona em sessões salvas — envie uma mensagem primeiro.", + "sessionFolderNeedsWorkspace": "Abra antes uma pasta de espaço de trabalho.", + "cycleNeedsMoreAgents": "Só há um agente disponível — nada para alternar." + } } } diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 9e2d16d906..45f4b49a8a 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -7466,6 +7466,11 @@ "upLeftThenRight": "左上再右", "upRightThenLeft": "右上再左" }, - "favoriteAgentsEmpty": "暂无可用智能体" + "favoriteAgentsEmpty": "暂无可用智能体", + "toasts": { + "renameNeedsSavedSession": "重命名只对已保存的会话生效——先发一条消息。", + "sessionFolderNeedsWorkspace": "请先打开一个工作区文件夹。", + "cycleNeedsMoreAgents": "可用智能体只有一个,无可切换。" + } } } diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index bc88edf1ff..bdcb6dfb2d 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -7466,6 +7466,11 @@ "upLeftThenRight": "左上再右", "upRightThenLeft": "右上再左" }, - "favoriteAgentsEmpty": "暫無可用代理人" + "favoriteAgentsEmpty": "暫無可用代理人", + "toasts": { + "renameNeedsSavedSession": "重新命名只對已儲存的會話生效——先發一則訊息。", + "sessionFolderNeedsWorkspace": "請先開啟一個工作區資料夾。", + "cycleNeedsMoreAgents": "可用智慧型代理只有一個,無可切換。" + } } } diff --git a/src/lib/rename-conversation-events.ts b/src/lib/rename-conversation-events.ts new file mode 100644 index 0000000000..e9956e9a8a --- /dev/null +++ b/src/lib/rename-conversation-events.ts @@ -0,0 +1,17 @@ +/** + * "Rename the current session" hand-off from the floating ball. The header + * that owns the rename dialog is mounted per conversation tab, so the signal + * carries the DB id and each instance compares it against its own — a tab + * whose conversation does not match ignores the event (multi-tab safe, same + * idiom as task-compose-events but synchronous: the header stays mounted). + */ + +export const RENAME_CONVERSATION_EVENT = "codeg:rename-conversation" + +export interface RenameConversationDetail { + conversationId: number +} + +export function requestRenameConversation(detail: RenameConversationDetail) { + window.dispatchEvent(new CustomEvent(RENAME_CONVERSATION_EVENT, { detail })) +} From d5224325ac5aba12d5271a616e156535d7562e32 Mon Sep 17 00:00:00 2001 From: fengx Date: Sun, 18 Oct 2026 04:14:17 +0800 Subject: [PATCH 8/9] feat(floating-ball): settings labels i18n + 9 new bindable actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Settings screen gesture/action labels now read the translated FloatingBall namespace (arrows: ↑↓←→, compound '↑ then ←'), instead of the untranslated AppearanceSettings English mirror - New actions: openBallBindings, cycleRunningSessions (0/1/2+ running semantics via ACP connection status), cycleRecentSessions (LRU queue + depth setting 2-4), deleteCurrentConversation (reuses header confirm dialog), openImportSessions, togglePinConversation, focusModeSelector (signal prop chain to MessageInput), togglePlanFullAccess (plan substring + matchFullAccessMode), openCollaboration; new actions ship unbound by default - New modules: recent-sessions-prefs (localStorage LRU, capacity 8, corrupted-storage tolerant), floating-ball-session-events (conversationId-filtered CustomEvents, null-matches-null) - tab-store: remember recent conversation on focusTab/bindConversationTab - 10 locales updated (parity test enforced); 2 new unit test files --- src/components/chat/chat-input.tsx | 4 + src/components/chat/conversation-shell.tsx | 4 + src/components/chat/message-input.tsx | 15 + src/components/chat/mode-selector.tsx | 71 +++++ .../conversation-detail-header.tsx | 19 ++ .../conversation-detail-panel.tsx | 66 +++++ .../workspace-chrome-controller.test.tsx | 19 +- .../layout/workspace-chrome-controller.tsx | 111 +++++++- .../settings/appearance-settings.tsx | 68 ++++- src/i18n/messages/ar.json | 264 ++++++++---------- src/i18n/messages/de.json | 264 ++++++++---------- src/i18n/messages/en.json | 212 +++++++------- src/i18n/messages/es.json | 264 ++++++++---------- src/i18n/messages/fr.json | 262 ++++++++--------- src/i18n/messages/ja.json | 262 ++++++++--------- src/i18n/messages/ko.json | 264 ++++++++---------- src/i18n/messages/pt.json | 262 ++++++++--------- src/i18n/messages/zh-CN.json | 262 ++++++++--------- src/i18n/messages/zh-TW.json | 262 ++++++++--------- src/lib/floating-ball-gestures.ts | 9 + src/lib/floating-ball-session-events.test.ts | 52 ++++ src/lib/floating-ball-session-events.ts | 47 ++++ src/lib/recent-sessions-prefs.test.ts | 77 +++++ src/lib/recent-sessions-prefs.ts | 92 ++++++ src/stores/tab-store.ts | 13 + 25 files changed, 1841 insertions(+), 1404 deletions(-) create mode 100644 src/components/chat/mode-selector.tsx create mode 100644 src/lib/floating-ball-session-events.test.ts create mode 100644 src/lib/floating-ball-session-events.ts create mode 100644 src/lib/recent-sessions-prefs.test.ts create mode 100644 src/lib/recent-sessions-prefs.ts diff --git a/src/components/chat/chat-input.tsx b/src/components/chat/chat-input.tsx index 4458a603a3..760b3fbcb0 100644 --- a/src/components/chat/chat-input.tsx +++ b/src/components/chat/chat-input.tsx @@ -28,6 +28,8 @@ interface ChatInputProps { agentName?: string onFocus?: () => void onSend: (draft: PromptDraft, modeId?: string | null) => void + /** Pass-through: see `MessageInput.modeSelectorSignal`. */ + modeSelectorSignal?: number onCancel: () => void modes?: SessionModeInfo[] configOptions?: SessionConfigOptionInfo[] @@ -109,6 +111,7 @@ export const ChatInput = memo(function ChatInput({ agentName, onFocus, onSend, + modeSelectorSignal, onCancel, modes, configOptions, @@ -205,6 +208,7 @@ export const ChatInput = memo(function ChatInput({ )} void + /** Pass-through: see `MessageInput.modeSelectorSignal`. */ + modeSelectorSignal?: number onConfigOptionChange?: (configId: string, valueId: string) => void agentType?: AgentType | null availableCommands?: AvailableCommandInfo[] | null @@ -198,6 +200,7 @@ export function ConversationShell({ selectorsLoading = false, selectedModeId, onModeChange, + modeSelectorSignal, onConfigOptionChange, agentType, availableCommands, @@ -310,6 +313,7 @@ export function ConversationShell({ {!hideInput && (
void + /** Bumped by the host (floating ball "mode selector" action) to open the + * inline mode dropdown; the effect below re-fires on every increment. */ + modeSelectorSignal?: number placeholder?: string defaultPath?: string disabled?: boolean @@ -311,6 +314,7 @@ function SelectorLoadingChip({ label }: { label: string }) { export function MessageInput({ onSend, + modeSelectorSignal: modeSelectorSignalProp = 0, placeholder, defaultPath, disabled = false, @@ -907,6 +911,17 @@ export function MessageInput({ // Antigravity after its commands on `/`. const [slashMenuOpen, setSlashMenuOpen] = useState(false) const [slashSelectedIndex, setSlashSelectedIndex] = useState(0) + // One-shot signal from the floating ball's "mode selector" action: the + // imperative openModeSelector handle bumps a counter; the effect below + // re-opens the dropdown when it advances. Programmatic trigger clicks are + // idempotent while the menu is already open. + useEffect(() => { + if (modeSelectorSignalProp === 0) return + const trigger = containerRef.current?.querySelector( + "[data-mode-selector-trigger]" + ) + trigger?.click() + }, [modeSelectorSignalProp]) // The trigger char (`/` for agent commands, `$` for Codex skills) and the // typed filter token, both derived from the editor caret by // `detectSlashTrigger` rather than from a raw string offset. diff --git a/src/components/chat/mode-selector.tsx b/src/components/chat/mode-selector.tsx new file mode 100644 index 0000000000..94019be7ec --- /dev/null +++ b/src/components/chat/mode-selector.tsx @@ -0,0 +1,71 @@ +"use client" + +import { ChevronDown } from "lucide-react" +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { DropdownRadioItemContent } from "@/components/chat/dropdown-radio-item-content" +import { SelectorTooltip } from "@/components/chat/selector-tooltip" +import type { SessionModeInfo } from "@/lib/types" + +interface ModeSelectorProps { + modes: SessionModeInfo[] + selectedModeId: string | null + onSelect: (modeId: string) => void + label: string +} + +export function InlineModeSelector({ + modes, + selectedModeId, + onSelect, + label, +}: ModeSelectorProps) { + const selected = modes.find((mode) => mode.id === selectedModeId) + const currentLabel = selected?.name ?? selectedModeId ?? "" + return ( + + + + + + + + + {modes.map((mode) => ( + + + + ))} + + + + ) +} diff --git a/src/components/conversations/conversation-detail-header.tsx b/src/components/conversations/conversation-detail-header.tsx index 4f1c8717d1..49a75ed255 100644 --- a/src/components/conversations/conversation-detail-header.tsx +++ b/src/components/conversations/conversation-detail-header.tsx @@ -22,6 +22,7 @@ import { } from "@/lib/api" import { formatConversationTitle } from "@/lib/conversation-title" import { RENAME_CONVERSATION_EVENT } from "@/lib/rename-conversation-events" +import { DELETE_CONVERSATION_EVENT } from "@/lib/floating-ball-session-events" import { ConversationHeaderFolderPicker } from "@/components/chat/conversation-context-bar" import { useAppWorkspaceStore } from "@/stores/app-workspace-store" import { useTabActions } from "@/contexts/tab-context" @@ -65,6 +66,7 @@ import { import { SessionDetailsDialog } from "./session-details-dialog" import { ConversationHeaderTags } from "./conversation-tag-picker" import { useIsMobile } from "@/hooks/use-mobile" +import { forgetRecentConversation } from "@/lib/recent-sessions-prefs" interface ConversationDetailHeaderProps { tabId: string @@ -228,6 +230,20 @@ export const ConversationDetailHeader = memo(function ConversationDetailHeader({ return () => window.removeEventListener(RENAME_CONVERSATION_EVENT, onRename) }, [conversationId, renameTarget, title]) + // Floating-ball "delete session": reuse this header's existing confirm + // dialog instead of minting a second one (same multi-tab idiom as rename). + useEffect(() => { + if (conversationId == null) return + const onDelete = (e: Event) => { + const id = (e as CustomEvent<{ conversationId: number | null }>).detail + ?.conversationId + if (id !== conversationId || deleteTarget != null) return + setDeleteTarget({ id: conversationId, tabId, title: displayTitle }) + } + window.addEventListener(DELETE_CONVERSATION_EVENT, onDelete) + return () => window.removeEventListener(DELETE_CONVERSATION_EVENT, onDelete) + }, [conversationId, deleteTarget, tabId, displayTitle]) + const handleStatusChange = useCallback( (next: ConversationStatus) => { if (conversationId == null) return @@ -249,6 +265,9 @@ export const ConversationDetailHeader = memo(function ConversationDetailHeader({ if (deleteTarget == null) return try { await deleteConversation(deleteTarget.id) + // Keep the floating ball's recent-sessions LRU from cycling into a + // conversation that no longer exists. + forgetRecentConversation(deleteTarget.id) // The deleted conversation is gone — close its tab and refresh the list. // Not recorded for reopen: there is no longer a conversation to reopen. closeTab(deleteTarget.tabId, { recordForReopen: false }) diff --git a/src/components/conversations/conversation-detail-panel.tsx b/src/components/conversations/conversation-detail-panel.tsx index 3ac574c9bb..863077da99 100644 --- a/src/components/conversations/conversation-detail-panel.tsx +++ b/src/components/conversations/conversation-detail-panel.tsx @@ -145,6 +145,12 @@ import { getSavedModeId, saveModePreference, } from "@/lib/selector-prefs-storage" +import { matchFullAccessMode } from "@/lib/default-mode-prefs" +import { + eventMatchesConversation, + FOCUS_MODE_SELECTOR_EVENT, + TOGGLE_PLAN_FULL_EVENT, +} from "@/lib/floating-ball-session-events" import { adoptLegacyNewConversationDraft, buildConversationDraftStorageKey, @@ -273,6 +279,7 @@ const ConversationTabView = memo(function ConversationTabView({ // Composer-namespace copy for the queue row's click-to-insert outcomes // (same keys the composer's own mid-turn send reports). const tCmp = useTranslations("Folder.chat.messageInput") + const tBall = useTranslations("FloatingBall") const tWelcome = useTranslations("Folder.chat.welcomeInputPanel") const tDiag = useTranslations("DiagnosticsSettings") const sharedT = useTranslations("Folder.chat.shared") @@ -1624,6 +1631,63 @@ const ConversationTabView = memo(function ConversationTabView({ [effectiveModes, selectedAgent] ) + // Floating-ball "Plan ⇄ full access": classify the advertised modes into a + // plan side and a full-access side (id substring weights, same helper the + // default-mode logic uses), then hop to the other side. Falls through to + // Plan when the current mode is neither; toasts when Plan doesn't exist. + const handleTogglePlanFull = useCallback(() => { + const availableModes = effectiveModes?.available_modes ?? [] + if (availableModes.length === 0) { + toast.info(tBall("toasts.noModesAvailable")) + return + } + const current = selectedModeIdRef.current + const availableIds = availableModes.map((m) => m.id) + const planId = availableIds.find((id) => id.toLowerCase().includes("plan")) + const fullId = matchFullAccessMode(availableIds) + const target = + current != null && planId != null && current === planId + ? (fullId ?? null) + : planId + if (target == null) { + toast.info(tBall("toasts.noSwitchTarget")) + return + } + handleModeChange(target) + // Sync the agent immediately — a mode change should not wait for the next + // send's catch-up diff, especially for the full-access side. + if (target !== conn.modes?.current_mode_id) { + void acpActions.setMode(tabId, target).catch((err) => { + console.error("[ConversationDetailPanel] fb setMode:", err) + }) + } + }, [acpActions, conn.modes, effectiveModes, handleModeChange, tabId, tBall]) + + // Floating-ball hand-offs (delete → header's confirm dialog; mode selector / + // plan toggle → this panel's composer). Events carry the DB id and every + // mounted panel instance hears them — only the one whose conversation + // matches reacts (multi-tab safe, same idiom as rename). + const [modeSelectorSignal, setModeSelectorSignal] = useState(0) + useEffect(() => { + const match = (e: Event) => + eventMatchesConversation( + (e as CustomEvent<{ conversationId: number | null }>).detail, + dbConversationId + ) + const onFocusMode = (e: Event) => { + if (match(e)) setModeSelectorSignal((n) => n + 1) + } + const onTogglePlan = (e: Event) => { + if (match(e)) handleTogglePlanFull() + } + window.addEventListener(FOCUS_MODE_SELECTOR_EVENT, onFocusMode) + window.addEventListener(TOGGLE_PLAN_FULL_EVENT, onTogglePlan) + return () => { + window.removeEventListener(FOCUS_MODE_SELECTOR_EVENT, onFocusMode) + window.removeEventListener(TOGGLE_PLAN_FULL_EVENT, onTogglePlan) + } + }, [dbConversationId, handleTogglePlanFull]) + const handleAnswerQuestion = useCallback( (answer: string) => { if (connStatus !== "connected") return @@ -2299,6 +2363,7 @@ const ConversationTabView = memo(function ConversationTabView({ return ( @@ -2474,6 +2539,7 @@ const ConversationTabView = memo(function ConversationTabView({
) : null} ({ vi.mock("@/contexts/acp-connections-context", () => ({ useAcpActions: () => ({ cancel: vi.fn() }), // Not a zustand store: it returns the context value directly. - useConnectionStore: () => ({ getActiveKey: () => null }), + useConnectionStore: () => ({ + getActiveKey: () => null, + getConnection: () => null, + }), })) vi.mock("@/hooks/use-appearance", () => ({ useZoomLevel: () => ({ zoomLevel: 100, setZoomLevel: vi.fn() }), @@ -116,6 +119,11 @@ vi.mock("@/contexts/tab-context", () => ({ selector: (s: { tabs: { id: string; conversationId: number | null; agentType: string }[] activeTabId: string | null + rawTabs: { + id: string + conversationId: number | null + agentType: string + }[] }) => T ) => selector({ @@ -125,6 +133,12 @@ vi.mock("@/contexts/tab-context", () => ({ : [{ id: state.activeTabId, conversationId: 7, agentType: "zcode" }] : [], activeTabId: state.activeTabId, + rawTabs: + state.activeTabId && state.activeTab + ? [state.activeTab] + : state.activeTabId + ? [{ id: state.activeTabId, conversationId: 7, agentType: "zcode" }] + : [], }), })) vi.mock("@/contexts/workspace-context", () => ({ @@ -161,7 +175,8 @@ vi.mock("@/hooks/use-shortcut-settings", () => ({ })) vi.mock("@/stores/app-workspace-store", () => ({ isConversationDeleted: () => false, - useAppWorkspaceStore: { getState: () => ({ getFolder: () => null }) }, + useAppWorkspaceStore: (selector: (s: { conversations: [] }) => unknown) => + selector({ conversations: [] }), })) vi.mock("@/lib/closed-tab-stack", () => ({ popClosedTab: () => null })) vi.mock("@/components/conversations/search-command-dialog", () => ({ diff --git a/src/components/layout/workspace-chrome-controller.tsx b/src/components/layout/workspace-chrome-controller.tsx index 54a290df78..e6db93539a 100644 --- a/src/components/layout/workspace-chrome-controller.tsx +++ b/src/components/layout/workspace-chrome-controller.tsx @@ -1,7 +1,11 @@ "use client" import { useCallback, useEffect, useRef, useState } from "react" -import { openSettingsWindow } from "@/lib/api" +import { + openSettingsWindow, + openImportSessionsWindow, + updateConversationPinned, +} from "@/lib/api" import { useAcpActions, useConnectionStore, @@ -16,6 +20,15 @@ import { useSortedAvailableAgents } from "@/hooks/use-sorted-available-agents" import { useTabActions, useTabStore } from "@/contexts/tab-context" import { requestCreateTaskFromText } from "@/lib/task-compose-events" import { requestRenameConversation } from "@/lib/rename-conversation-events" +import { + requestDeleteConversation, + requestFocusModeSelector, + requestTogglePlanFull, +} from "@/lib/floating-ball-session-events" +import { + nextRecentConversation, + readRecentSessionDepth, +} from "@/lib/recent-sessions-prefs" import { useWorkspaceActions, useWorkspaceFileTabs, @@ -91,6 +104,11 @@ export function WorkspaceChromeController() { useTabActions() const tabs = useTabStore((s) => s.tabs) const activeTabId = useTabStore((s) => s.activeTabId) + // cycleRecentSessions picks from the localStorage LRU queue; isAvailable + // filters to conversations this window can actually open (cross-window + // pollution guard: the queue is shared, the tabs are per-window). + const conversations = useAppWorkspaceStore((s) => s.conversations) + const rawTabs = useTabStore((s) => s.rawTabs) // Tab-close/navigation shortcuts used to live in the visible tab strips. // Mobile no longer mounts those strips, so this always-mounted controller now // owns them too (see the keydown handler below). @@ -113,7 +131,7 @@ export function WorkspaceChromeController() { // whichever conversation happens to be mounted. The active connection is // what the workspace considers "the turn in front of the user". const { cancel } = useAcpActions() - const { getActiveKey } = useConnectionStore() + const { getActiveKey, getConnection } = useConnectionStore() // Agent cycling (floating ball "next agent" / "favorite agents") needs the // same enabled+available list the draft tabs resolve their defaults from. const { sortedTypes: sortedAvailableAgents } = useSortedAvailableAgents() @@ -606,6 +624,92 @@ export function WorkspaceChromeController() { confirmDraftAgent(tab.id, next) return } + case "openBallBindings": + void openSettingsWindow("appearance").catch((err) => { + console.error( + "[WorkspaceChromeController] appearance settings:", + err + ) + }) + return + case "openCollaboration": + void openSettingsWindow("collaboration").catch((err) => { + console.error( + "[WorkspaceChromeController] collaboration settings:", + err + ) + }) + return + case "openImportSessions": + void openImportSessionsWindow().catch((err) => { + console.error("[WorkspaceChromeController] import sessions:", err) + }) + return + case "deleteCurrentConversation": + requestDeleteConversation(activeTab?.conversationId ?? null) + return + case "togglePinConversation": { + const conv = conversations.find( + (c) => c.id === activeTab?.conversationId + ) + if (!conv) { + toast.info(tBall("toasts.renameNeedsSavedSession")) + return + } + void updateConversationPinned(conv.id, conv.pinned_at == null) + .then(() => toast.info(tBall("toasts.pinToggled"))) + .catch((err) => { + console.error("[WorkspaceChromeController] pin toggle:", err) + }) + return + } + case "focusModeSelector": + requestFocusModeSelector(activeTab?.conversationId ?? null) + return + case "togglePlanFullAccess": + requestTogglePlanFull(activeTab?.conversationId ?? null) + return + case "cycleRunningSessions": { + // "Running" = this window's connections currently generating. The + // connection store is per-window, so a session streaming in another + // window is invisible here — the toast phrasing accounts for that. + const runningTabs = rawTabs.filter( + (tab) => getConnection(tab.id)?.status === "prompting" + ) + if (runningTabs.length === 0) { + toast.info(tBall("toasts.noRunningSession")) + return + } + if (runningTabs.length <= 1) { + toast.info(tBall("toasts.noOtherRunning")) + return + } + const currentIndex = runningTabs.findIndex( + (tab) => tab.id === activeTabId + ) + // Active tab not running → jump to the first running tab; otherwise + // step forward through the running set in tab order. + const nextIndex = (currentIndex + 1) % runningTabs.length + switchTab(runningTabs[nextIndex].id) + return + } + case "cycleRecentSessions": { + if (!activeFolder) return + const target = nextRecentConversation( + activeTab?.conversationId ?? null, + readRecentSessionDepth(), + (id) => + conversations.some((c) => c.id === id) && + rawTabs.some((tab) => tab.conversationId === id) + ) + if (target == null) { + toast.info(tBall("toasts.noOtherRecent")) + return + } + const tab = rawTabs.find((t) => t.conversationId === target) + if (tab) switchTab(tab.id) + return + } case "panel": return } @@ -617,13 +721,16 @@ export function WorkspaceChromeController() { auxPanel, cancel, confirmDraftAgent, + conversations, favoriteAgents, getActiveKey, + getConnection, handleOpenFolder, handleOpenSettings, openChatModeTab, openConversations, openNewConversationTab, + rawTabs, reopenClosedTab, setSearchOpen, setRoute, diff --git a/src/components/settings/appearance-settings.tsx b/src/components/settings/appearance-settings.tsx index 85c3f33a32..173175e272 100644 --- a/src/components/settings/appearance-settings.tsx +++ b/src/components/settings/appearance-settings.tsx @@ -53,6 +53,11 @@ import { type ThemeColor, type ZoomLevel, } from "@/lib/theme-presets" +import { + readRecentSessionDepth, + writeRecentSessionDepth, + type RecentSessionDepth, +} from "@/lib/recent-sessions-prefs" import { PetManagerSection } from "./pet-manager-section" import { FontSettingsSection } from "./font-settings-section" import { WorkspaceBackgroundSection } from "./workspace-background-section" @@ -90,6 +95,15 @@ const BALL_BINDABLE_ACTIONS: readonly FloatingBallAction[] = [ "openSessionFolder", "cycleAgent", "cycleFavoriteAgent", + "openBallBindings", + "cycleRunningSessions", + "cycleRecentSessions", + "deleteCurrentConversation", + "openImportSessions", + "togglePinConversation", + "focusModeSelector", + "togglePlanFullAccess", + "openCollaboration", "zoomIn", "zoomOut", "zoomReset", @@ -104,9 +118,8 @@ function isFloatingBallSize(v: string): v is FloatingBallSize { /** Label of a gesture's built-in default, for the placeholder row. */ function defaultGestureLabel( action: FloatingBallAction, - // Loosely typed: the two caller namespaces (FloatingBall / AppearanceSettings) - // both mirror the actions.* keys, and next-intl's key union would force a - // per-namespace overload for no safety gain here. + // Loosely typed: callers pass the FloatingBall namespace's t; next-intl's + // key union would force a per-key overload for no safety gain here. // eslint-disable-next-line @typescript-eslint/no-explicit-any t: any ): string { @@ -187,10 +200,15 @@ export function AppearanceSettings() { // 的宽度更小。 const chatWidthMax = chatContentWidthCeiling(screenWidth, zoomLevel / 100) - // The binding dropdown renders under the AppearanceSettings namespace, whose - // mirrored actions.* keys come from FloatingBall. + // Gesture/action labels come from the FloatingBall namespace — the only one + // that actually carries translations; the AppearanceSettings.gestures/actions + // mirror is legacy and no longer read here. + const ballT = useTranslations("FloatingBall") + const [recentSessionDepth, setRecentSessionDepth] = useState( + readRecentSessionDepth + ) const actionLabel = (action: FloatingBallAction) => - action === "panel" ? t("floatingBall.panelTitle") : t(`actions.${action}`) + action === "panel" ? ballT("panelTitle") : ballT(`actions.${action}`) const resolvedThemeLabel = resolvedTheme === "dark" @@ -494,7 +512,7 @@ export function AppearanceSettings() { className="flex items-center justify-between gap-2" > - {t(`gestures.${gesture}`, { + {ballT(`gestures.${gesture}`, { fallback: gesture, })} @@ -513,9 +531,9 @@ export function AppearanceSettings() { DEFAULT_FLOATING_BALL_BINDINGS[gesture] ? defaultGestureLabel( DEFAULT_FLOATING_BALL_BINDINGS[gesture], - t + ballT ) - : t("floatingBall.bindingCleared") + : ballT("bindingCleared") } /> @@ -524,9 +542,9 @@ export function AppearanceSettings() { {DEFAULT_FLOATING_BALL_BINDINGS[gesture] ? defaultGestureLabel( DEFAULT_FLOATING_BALL_BINDINGS[gesture], - t + ballT ) - : t("floatingBall.bindingCleared")} + : ballT("bindingCleared")} {BALL_BINDABLE_ACTIONS.map((action) => ( + + {/* Depth of the recent-sessions cycle (2 = toggle between the two + most recently opened). Read/written straight to localStorage — + the writer lives in tab-store, outside React. */} +
+

{ballT("recentSessionDepth")}

+ +
{/* ===== Chat content width ===== */}
diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index bf854b2c0e..a557656ba3 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -181,6 +181,13 @@ "sectionDescription": "حركات الطي/التوسيع واستدعاءات الأدوات وحالة العمل داخل منطقة المحادثة، إضافةً إلى التمرير السلس. عند الإيقاف تتحدّث الواجهة فورًا. تستمر مؤشرات التحميل في الدوران.", "enable": "تفعيل حركات المحادثة" }, + "messageNav": { + "sectionTitle": "مستكشف الرسائل", + "sectionDescription": "موضع مستكشف رسائل المحادثة. عند اختيار أسفل اليمين يصبح بطاقة مُدمجة بسطر واحد لكل رسالة مستخدم فوق حقل الإدخال.", + "positionStart": "أعلى اليسار (افتراضي)", + "positionBottomEnd": "أسفل اليمين (مُدمج)", + "hint": "تبقى لوحتا الخطة والوكلاء الفرعيين في أعلى اليسار في كل الأحوال." + }, "chatWidth": { "sectionTitle": "عرض المحادثة", "sectionDescription": "أقصى عرض لعمود المحادثة ومربع الإدخال. يمكنك أيضًا سحب حافتي منطقة المحادثة لضبطه.", @@ -258,60 +265,69 @@ "panelTitle": "إجراءات سريعة" }, "gestures": { - "tap": "Tap", - "doubleTap": "Double tap", - "longPress": "Long press", - "left": "Swipe left", - "right": "Swipe right", - "up": "Swipe up", - "upLeft": "Up-left", - "upRight": "Up-right", - "upThenLeft": "Up, then left", - "upThenRight": "Up, then right", - "upDown": "Up, then down", - "leftThenRight": "Left, then right", - "rightThenLeft": "Right, then left", - "upLeftThenDown": "Up-left, then down", - "upRightThenDown": "Up-right, then down", - "down": "Swipe down", - "downLeft": "Down-left", - "downRight": "Down-right", - "downUp": "Down, then up", - "upThenDownRight": "Up, then down-right", - "downThenUpRight": "Down, then up-right", - "rightThenUp": "Right, then up", - "rightThenDown": "Right, then down", - "downThenRight": "Down, then right", - "downThenLeft": "Down, then left", - "downRightThenUp": "Down-right, then up", - "downLeftThenUp": "Down-left, then up", - "upLeftThenRight": "Up-left, then right", - "upRightThenLeft": "Up-right, then left" + "tap": "نقرة", + "doubleTap": "نقرة مزدوجة", + "longPress": "ضغط مطول", + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ ثم ←", + "upThenRight": "↑ ثم →", + "upDown": "↑ ثم ↓", + "leftThenRight": "← ثم →", + "rightThenLeft": "→ ثم ←", + "upLeftThenDown": "↖ ثم ↓", + "upRightThenDown": "↗ ثم ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ ثم ↑", + "upThenDownRight": "↑ ثم ↘", + "downThenUpRight": "↓ ثم ↗", + "rightThenUp": "→ ثم ↑", + "rightThenDown": "→ ثم ↓", + "downThenRight": "↓ ثم →", + "downThenLeft": "↓ ثم ←", + "downRightThenUp": "↘ ثم ↑", + "downLeftThenUp": "↙ ثم ↑", + "upLeftThenRight": "↖ ثم →", + "upRightThenLeft": "↗ ثم ←" }, "actions": { - "newConversation": "New conversation", - "toggleSidebar": "Toggle sidebar", - "toggleSearch": "Search", - "toggleTerminal": "Toggle terminal", - "nextTab": "Next tab", - "prevTab": "Previous tab", - "reopenClosedTab": "Reopen closed tab", - "openFolder": "Open folder", - "zoomIn": "Zoom in", - "zoomOut": "Zoom out", - "zoomReset": "Reset zoom", - "stopGenerating": "Stop generating", - "openSettings": "Settings", - "openTodos": "To-dos", - "openTodosNew": "New to-do", - "openAutomations": "Automations", - "openCanvas": "Canvas", - "openAgents": "Agents", - "openSystemAdmin": "System admin", - "renameConversation": "Rename conversation", - "openSessionFolder": "Session files", - "cycleAgent": "Next agent", - "cycleFavoriteAgent": "Favorite agents" + "newConversation": "محادثة جديدة", + "toggleSidebar": "تبديل الشريط الجانبي", + "toggleSearch": "بحث", + "toggleTerminal": "تبديل الطرفية", + "nextTab": "التبويب التالي", + "prevTab": "التبويب السابق", + "reopenClosedTab": "إعادة فتح التبويب المغلق", + "openFolder": "فتح مجلد", + "zoomIn": "تكبير", + "zoomOut": "تصغير", + "zoomReset": "إعادة تعيين التكبير", + "stopGenerating": "إيقاف التوليد", + "openSettings": "الإعدادات", + "openTodos": "المهام", + "openTodosNew": "مهمة جديدة", + "openAutomations": "الأتمتة", + "openCanvas": "اللوحة", + "openAgents": "الوكلاء", + "openSystemAdmin": "إدارة النظام", + "renameConversation": "إعادة تسمية المحادثة", + "openSessionFolder": "ملفات الجلسة", + "cycleAgent": "الوكيل التالي", + "cycleFavoriteAgent": "الوكلاء المفضلون", + "openBallBindings": "ربط الإيماءات", + "cycleRunningSessions": "الجلسات الجارية", + "cycleRecentSessions": "الجلسات الأخيرة", + "deleteCurrentConversation": "حذف الجلسة الحالية", + "openImportSessions": "استيراد الجلسات", + "togglePinConversation": "تثبيت / إلغاء التثبيت", + "focusModeSelector": "محدد الوضع", + "togglePlanFullAccess": "خطة / وصول كامل", + "openCollaboration": "التعاون" } }, "SystemSettings": { @@ -1185,30 +1201,6 @@ "apiKeyHint": "يُمرَّر إلى CLI باسم KIRO_API_KEY. اتركه فارغاً لاستخدام الحساب الذي سجلت الدخول به عبر kiro-cli login؛ لا يلزم إلا حيث يتعذر تسجيل الدخول بالمتصفح (الخوادم، Docker).", "saveConfig": "حفظ إعدادات Kiro" }, - "trae": { - "configManagement": "إدارة الإعدادات", - "configDescription": "يتوفر TRAE CLI 2.0 لعملاء الخطة الرئيسية من TRAE Enterprise فقط. يشغّل Codeg أداة traecli التي ثبّتّها، وهي تحفظ تسجيل الدخول والمهارات وخوادم MCP في ~/.trae. النموذج والوضع ومستوى الاستدلال ليست هنا: يُبلغ عنها TRAE CLI عبر ACP، لذا تديرها محددات المحرّر لكل جلسة.", - "signInTitle": "تسجيل الدخول", - "signInHint": "شغّل هذا الأمر مرة واحدة في الطرفية لتسجيل الدخول عبر SSO الخاص بمؤسستك. تعيد جلسات Codeg استخدام تسجيل الدخول هذا.", - "copyCommand": "نسخ الأمر", - "commandCopied": "تم نسخ الأمر", - "patLabel": "رمز الوصول الشخصي (اختياري)", - "patHint": "للأجهزة التي يتعذر فيها تسجيل الدخول بالمتصفح، مثل الخوادم أو تثبيتات Docker. يمرّره Codeg إلى TRAE CLI باسم TRAECLI_PERSONAL_ACCESS_TOKEN ويسجّل الدخول به عند بدء كل جلسة. اتركه فارغاً لاستخدام تسجيل الدخول من traecli login.", - "hostLabel": "عنوان API الخاص بالمؤسسة (اختياري)", - "hostHint": "فقط إذا كانت مؤسستك تشغّل TRAE على نطاقها الخاص. يُمرَّر باسم TRAECLI_HOST؛ اتركه فارغاً لاستخدام خدمة TRAE نفسها.", - "saveConfig": "حفظ إعدادات TRAE CLI" - }, - "commandCode": { - "configManagement": "إدارة الإعدادات", - "configDescription": "يشغّل Codeg واجهة Command Code CLI من حزمة npm المسماة command-code، وهي تحفظ تسجيل الدخول والمهارات وخوادم MCP في ~/.commandcode. النموذج والوضع ومستوى الاستدلال ليست هنا: يُبلغ عنها Command Code عبر ACP، لذا تديرها محددات المحرّر لكل جلسة.", - "signInTitle": "تسجيل الدخول", - "signInHint": "شغّل هذا الأمر مرة واحدة في الطرفية لتسجيل الدخول بحساب Command Code الخاص بك. تعيد جلسات Codeg استخدام تسجيل الدخول هذا.", - "copyCommand": "نسخ الأمر", - "commandCopied": "تم نسخ الأمر", - "apiKeyLabel": "مفتاح API (اختياري)", - "apiKeyHint": "للأجهزة التي يتعذر فيها تسجيل الدخول بالمتصفح، مثل الخوادم أو تثبيتات Docker. يُمرَّر إلى Command Code باسم COMMAND_CODE_API_KEY ويتقدّم على تسجيل الدخول من command-code login. اتركه فارغاً لاستخدام تسجيل الدخول ذاك.", - "saveConfig": "حفظ إعدادات Command Code" - }, "deepseek": { "configManagement": "إعدادات DeepSeek Harness", "configDescription": "نقطة النهاية والمفتاح متغيّرا بيئة يقرأهما deepseek-acp عند الإقلاع. أما النموذج ومستوى الاستدلال فمحدِّدان على مستوى الجلسة، ويُختاران من حقل الإدخال.", @@ -1704,10 +1696,6 @@ "saveAntigravityConfigFailed": "Failed to save Antigravity config", "kiroSaved": "تم حفظ إعدادات Kiro", "saveKiroFailed": "تعذّر حفظ إعدادات Kiro", - "traeSaved": "تم حفظ إعدادات TRAE CLI", - "saveTraeFailed": "تعذّر حفظ إعدادات TRAE CLI", - "commandCodeSaved": "تم حفظ إعدادات Command Code", - "saveCommandCodeFailed": "تعذّر حفظ إعدادات Command Code", "systemAgentAlreadyLatest": "{name} محدَّث بالفعل ({version})" }, "distribution": { @@ -2479,10 +2467,7 @@ "commits": "الالتزامات" }, "noFolderTitle": "لا يوجد مجلد مفتوح", - "noFolderHint": "افتح مجلدا لعرض محتواه هنا", - "openFullPage": "فتح في صفحة كاملة", - "back": "رجوع", - "noAutomations": "لا توجد أتمتة لهذا المشروع بعد" + "noFolderHint": "افتح مجلدا لعرض محتواه هنا" }, "windowControls": { "minimizeWindow": "تصغير النافذة", @@ -3381,7 +3366,6 @@ }, "messageInput": { "askAnything": "اسأل أي شيء...", - "promptSuggestion": "{suggestion} (اضغط Tab للاستخدام)", "removeAttachmentAria": "إزالة {name}", "attachFiles": "إرفاق ملفات", "addActions": "إضافة", @@ -3411,10 +3395,6 @@ "searchModelAria": "البحث عن النماذج", "modelListLabel": "النماذج", "recommendedBadge": "موصى به", - "recommendedValue": "موصى به: {value}", - "searchOptions": "البحث في الخيارات...", - "searchOptionsAria": "البحث في الخيارات", - "noOptions": "لم يتم العثور على خيارات", "noModels": "لم يتم العثور على نماذج", "cancel": "إلغاء", "send": "إرسال", @@ -4027,16 +4007,13 @@ }, "messageNav": { "title": "تنقل الرسائل", + "collapse": "طي تنقل الرسائل", "loadingEarlier": "جارٍ تحميل الرسائل السابقة…", "loadEarlier": "تحميل الرسائل السابقة", + "collapsedSummary": "الرسائل {count}", "fileCount": "{count, plural, one {# ملف} other {# ملفات}}", - "subAgentCount": "{count, plural, one {# وكيل فرعي} other {# وكلاء فرعيين}}", - "showDiff": "إظهار تغييرات {filePath}", - "hideDiff": "إخفاء تغييرات {filePath}", - "openFile": "فتح {filePath}", - "collapse": "طي", - "currentMessage": "الرسالة الحالية", - "scrollToCurrent": "التمرير إلى الرسالة الحالية (#{ordinal})" + "remove": "إزالة", + "noDiffDataAvailable": "لا توجد بيانات diff متاحة لـ {filePath}" }, "replyArtifacts": { "title": "الملفات المتغيرة", @@ -4094,9 +4071,6 @@ "resumeDelegation": "استئناف المهمة {task}", "resumeDelegationNoId": "استئناف مهمة مفوضة", "promptLabel": "التعليمات", - "ackRecorded": "سُجِّل في مهمة العمل", - "ackNotRecorded": "لم يُسجَّل في مهمة العمل", - "ackNotRecordedWithReason": "لم يُسجَّل في مهمة العمل: {reason}", "verdict": { "success": "ناجحة", "needs_review": "بحاجة إلى مراجعة", @@ -4214,18 +4188,6 @@ "stopDeclined": "تعذّر إيقاف هذه المهمة، ربما تكون قد انتهت بالفعل.", "stopFailed": "فشل إيقاف المهمة: {error}" }, - "remoteControl": { - "title": "التحكم عن بُعد مُفعّل", - "open": "فتح في المتصفح", - "copy": "نسخ الرابط", - "copied": "تم النسخ", - "qr": "رمز QR", - "qrLabel": "رمز QR لرابط التحكم عن بُعد لهذه الجلسة", - "qrHint": "امسحه بهاتفك لمتابعة هذه الجلسة في تطبيق Claude أو على claude.ai/code.", - "noUrl": "لم يُرجع Claude Code رابطًا لهذه الجلسة", - "turnOff": "إيقاف", - "turnOffBusy": "متاح بعد أن ينتهي Claude" - }, "proposedPlan": { "title": "الخطة المقترحة", "planning": "جارٍ التخطيط…" @@ -7433,44 +7395,62 @@ "renameConversation": "إعادة تسمية المحادثة", "openSessionFolder": "ملفات الجلسة", "cycleAgent": "الوكيل التالي", - "cycleFavoriteAgent": "الوكلاء المفضلون" + "cycleFavoriteAgent": "الوكلاء المفضلون", + "openBallBindings": "ربط الإيماءات", + "cycleRunningSessions": "الجلسات الجارية", + "cycleRecentSessions": "الجلسات الأخيرة", + "deleteCurrentConversation": "حذف الجلسة الحالية", + "openImportSessions": "استيراد الجلسات", + "togglePinConversation": "تثبيت / إلغاء التثبيت", + "focusModeSelector": "محدد الوضع", + "togglePlanFullAccess": "خطة / وصول كامل", + "openCollaboration": "التعاون" }, "gestures": { "tap": "نقرة", "doubleTap": "نقرة مزدوجة", - "longPress": "ضغط مطوّل", - "left": "سحب لليسار", - "right": "سحب لليمين", - "up": "سحب للأعلى", - "upLeft": "أعلى اليسار", - "upRight": "أعلى اليمين", - "upThenLeft": "أعلى ثم يسار", - "upThenRight": "أعلى ثم يمين", - "upDown": "أعلى ثم أسفل", - "leftThenRight": "يسار ثم يمين", - "rightThenLeft": "يمين ثم يسار", - "upLeftThenDown": "أعلى اليسار ثم أسفل", - "upRightThenDown": "أعلى اليمين ثم أسفل", - "down": "اسحب لأسفل", - "downLeft": "أسفل-يسار", - "downRight": "أسفل-يمين", - "downUp": "أسفل ثم أعلى", - "upThenDownRight": "أعلى ثم أسفل-يمين", - "downThenUpRight": "أسفل ثم أعلى-يمين", - "rightThenUp": "يمين ثم أعلى", - "rightThenDown": "يمين ثم أسفل", - "downThenRight": "أسفل ثم يمين", - "downThenLeft": "أسفل ثم يسار", - "downRightThenUp": "أسفل-يمين ثم أعلى", - "downLeftThenUp": "أسفل-يسار ثم أعلى", - "upLeftThenRight": "أعلى-يسار ثم يمين", - "upRightThenLeft": "أعلى-يمين ثم يسار" + "longPress": "ضغط مطول", + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ ثم ←", + "upThenRight": "↑ ثم →", + "upDown": "↑ ثم ↓", + "leftThenRight": "← ثم →", + "rightThenLeft": "→ ثم ←", + "upLeftThenDown": "↖ ثم ↓", + "upRightThenDown": "↗ ثم ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ ثم ↑", + "upThenDownRight": "↑ ثم ↘", + "downThenUpRight": "↓ ثم ↗", + "rightThenUp": "→ ثم ↑", + "rightThenDown": "→ ثم ↓", + "downThenRight": "↓ ثم →", + "downThenLeft": "↓ ثم ←", + "downRightThenUp": "↘ ثم ↑", + "downLeftThenUp": "↙ ثم ↑", + "upLeftThenRight": "↖ ثم →", + "upRightThenLeft": "↗ ثم ←" }, "favoriteAgentsEmpty": "لا يوجد وكلاء متاحون", "toasts": { "renameNeedsSavedSession": "إعادة التسمية تعمل على الجلسات المحفوظة فقط — أرسل رسالة أولاً.", "sessionFolderNeedsWorkspace": "افتح مجلد مساحة العمل أولاً.", - "cycleNeedsMoreAgents": "وكيل واحد متاح فقط — لا يوجد ما يمكن التبديل إليه." - } + "cycleNeedsMoreAgents": "وكيل واحد متاح فقط — لا يوجد ما يمكن التبديل إليه.", + "noOtherRunning": "لا توجد جلسة جارية أخرى.", + "noOtherRecent": "لا توجد جلسة حديثة أخرى.", + "noPlanMode": "لا يحتوي هذا الوكيل على وضع الخطة.", + "pinToggled": "تم تحديث حالة التثبيت.", + "noRunningSession": "لا توجد جلسة جارية بعد.", + "noModesAvailable": "لا يوفر هذا الوكيل أي أوضاع.", + "noSwitchTarget": "لا يوجد وضع للتبديل إليه (لم يتم العثور على وضع Plan أو الوصول الكامل)." + }, + "recentSessionDepth": "عمق تبديل الجلسات الأخيرة", + "bindingCleared": "غير مربوط" } -} +} \ No newline at end of file diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index f9c30e597f..cf581c7fb1 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -181,6 +181,13 @@ "sectionDescription": "Animationen für Ein-/Ausklappen, Tool-Aufrufe und Arbeitsstatus im Unterhaltungsbereich sowie sanftes Scrollen. Wenn deaktiviert, werden Änderungen sofort angezeigt. Ladeanzeigen drehen sich weiterhin.", "enable": "Chat-Animationen aktivieren" }, + "messageNav": { + "sectionTitle": "Nachrichten-Navigator", + "sectionDescription": "Position des Nachrichten-Navigators. Unten rechts wird er zu einer kompakten Karte mit einer Zeile pro Nutzernachricht über dem Eingabefeld.", + "positionStart": "Oben links (Standard)", + "positionBottomEnd": "Unten rechts (kompakt)", + "hint": "Plan- und Subagenten-Panels bleiben in beiden Fällen oben links." + }, "chatWidth": { "sectionTitle": "Chat-Breite", "sectionDescription": "Maximale Breite der Unterhaltungsspalte und des Eingabefelds. Sie lässt sich auch durch Ziehen der Chat-Ränder anpassen.", @@ -258,60 +265,69 @@ "panelTitle": "Schnellaktionen" }, "gestures": { - "tap": "Tap", - "doubleTap": "Double tap", - "longPress": "Long press", - "left": "Swipe left", - "right": "Swipe right", - "up": "Swipe up", - "upLeft": "Up-left", - "upRight": "Up-right", - "upThenLeft": "Up, then left", - "upThenRight": "Up, then right", - "upDown": "Up, then down", - "leftThenRight": "Left, then right", - "rightThenLeft": "Right, then left", - "upLeftThenDown": "Up-left, then down", - "upRightThenDown": "Up-right, then down", - "down": "Swipe down", - "downLeft": "Down-left", - "downRight": "Down-right", - "downUp": "Down, then up", - "upThenDownRight": "Up, then down-right", - "downThenUpRight": "Down, then up-right", - "rightThenUp": "Right, then up", - "rightThenDown": "Right, then down", - "downThenRight": "Down, then right", - "downThenLeft": "Down, then left", - "downRightThenUp": "Down-right, then up", - "downLeftThenUp": "Down-left, then up", - "upLeftThenRight": "Up-left, then right", - "upRightThenLeft": "Up-right, then left" + "tap": "Tippen", + "doubleTap": "Doppelt tippen", + "longPress": "Lange drücken", + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ dann ←", + "upThenRight": "↑ dann →", + "upDown": "↑ dann ↓", + "leftThenRight": "← dann →", + "rightThenLeft": "→ dann ←", + "upLeftThenDown": "↖ dann ↓", + "upRightThenDown": "↗ dann ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ dann ↑", + "upThenDownRight": "↑ dann ↘", + "downThenUpRight": "↓ dann ↗", + "rightThenUp": "→ dann ↑", + "rightThenDown": "→ dann ↓", + "downThenRight": "↓ dann →", + "downThenLeft": "↓ dann ←", + "downRightThenUp": "↘ dann ↑", + "downLeftThenUp": "↙ dann ↑", + "upLeftThenRight": "↖ dann →", + "upRightThenLeft": "↗ dann ←" }, "actions": { - "newConversation": "New conversation", - "toggleSidebar": "Toggle sidebar", - "toggleSearch": "Search", - "toggleTerminal": "Toggle terminal", - "nextTab": "Next tab", - "prevTab": "Previous tab", - "reopenClosedTab": "Reopen closed tab", - "openFolder": "Open folder", - "zoomIn": "Zoom in", - "zoomOut": "Zoom out", - "zoomReset": "Reset zoom", - "stopGenerating": "Stop generating", - "openSettings": "Settings", - "openTodos": "To-dos", - "openTodosNew": "New to-do", - "openAutomations": "Automations", + "newConversation": "Neue Unterhaltung", + "toggleSidebar": "Seitenleiste umschalten", + "toggleSearch": "Suchen", + "toggleTerminal": "Terminal umschalten", + "nextTab": "Nächster Tab", + "prevTab": "Vorheriger Tab", + "reopenClosedTab": "Geschlossenen Tab wieder öffnen", + "openFolder": "Ordner öffnen", + "zoomIn": "Vergrößern", + "zoomOut": "Verkleinern", + "zoomReset": "Zoom zurücksetzen", + "stopGenerating": "Generierung stoppen", + "openSettings": "Einstellungen", + "openTodos": "Aufgaben", + "openTodosNew": "Neue Aufgabe", + "openAutomations": "Automatisierungen", "openCanvas": "Canvas", - "openAgents": "Agents", - "openSystemAdmin": "System admin", - "renameConversation": "Rename conversation", - "openSessionFolder": "Session files", - "cycleAgent": "Next agent", - "cycleFavoriteAgent": "Favorite agents" + "openAgents": "Agenten", + "openSystemAdmin": "Systemverwaltung", + "renameConversation": "Unterhaltung umbenennen", + "openSessionFolder": "Sitzungsdateien", + "cycleAgent": "Nächster Agent", + "cycleFavoriteAgent": "Favoriten-Agenten", + "openBallBindings": "Gesten-Zuordnung", + "cycleRunningSessions": "Laufende Sitzungen", + "cycleRecentSessions": "Letzte Sitzungen", + "deleteCurrentConversation": "Aktuelle Sitzung löschen", + "openImportSessions": "Sitzungen importieren", + "togglePinConversation": "Anpinnen / lösen", + "focusModeSelector": "Modus-Auswahl", + "togglePlanFullAccess": "Plan / Vollzugriff", + "openCollaboration": "Kollaboration" } }, "SystemSettings": { @@ -1185,30 +1201,6 @@ "apiKeyHint": "Wird der CLI als KIRO_API_KEY übergeben. Leer lassen, um das per kiro-cli login angemeldete Konto zu nutzen; nur nötig, wo keine Browser-Anmeldung möglich ist (Server, Docker).", "saveConfig": "Kiro-Konfiguration speichern" }, - "trae": { - "configManagement": "Konfiguration", - "configDescription": "TRAE CLI 2.0 steht nur Kunden des Flaggschiff-Tarifs von TRAE Enterprise zur Verfügung. Codeg startet das von dir installierte traecli, das Anmeldung, Skills und MCP-Server in ~/.trae speichert. Modell, Modus und Denkaufwand stehen nicht hier: TRAE CLI meldet sie über ACP, daher verwalten die Auswahlfelder im Eingabebereich sie pro Sitzung.", - "signInTitle": "Anmelden", - "signInHint": "Führe dies einmal in einem Terminal aus, um dich mit dem SSO deines Unternehmens anzumelden. Sitzungen in Codeg verwenden diese Anmeldung wieder.", - "copyCommand": "Befehl kopieren", - "commandCopied": "Befehl kopiert", - "patLabel": "Persönliches Zugriffstoken (optional)", - "patHint": "Für Rechner, auf denen keine Browser-Anmeldung möglich ist, etwa Server oder Docker-Installationen. Codeg übergibt es TRAE CLI als TRAECLI_PERSONAL_ACCESS_TOKEN und meldet sich bei jedem Sitzungsstart damit an. Leer lassen, um die Anmeldung aus traecli login zu nutzen.", - "hostLabel": "Enterprise-API-URL (optional)", - "hostHint": "Nur wenn deine Organisation TRAE unter einer eigenen Domain betreibt. Wird als TRAECLI_HOST übergeben; leer lassen für den Dienst von TRAE.", - "saveConfig": "TRAE CLI-Konfiguration speichern" - }, - "commandCode": { - "configManagement": "Konfiguration", - "configDescription": "Codeg startet die Command Code CLI aus dem npm-Paket command-code, die Anmeldung, Skills und MCP-Server in ~/.commandcode speichert. Modell, Modus und Denkaufwand stehen nicht hier: Command Code meldet sie über ACP, daher verwalten die Auswahlfelder im Eingabebereich sie pro Sitzung.", - "signInTitle": "Anmelden", - "signInHint": "Führe dies einmal in einem Terminal aus, um dich mit deinem Command Code-Konto anzumelden. Sitzungen in Codeg verwenden diese Anmeldung wieder.", - "copyCommand": "Befehl kopieren", - "commandCopied": "Befehl kopiert", - "apiKeyLabel": "API-Schlüssel (optional)", - "apiKeyHint": "Für Rechner, auf denen keine Browser-Anmeldung möglich ist, etwa Server oder Docker-Installationen. Wird Command Code als COMMAND_CODE_API_KEY übergeben und hat Vorrang vor der Anmeldung aus command-code login. Leer lassen, um diese Anmeldung zu nutzen.", - "saveConfig": "Command Code-Konfiguration speichern" - }, "deepseek": { "configManagement": "DeepSeek-Harness-Konfiguration", "configDescription": "Endpunkt und Schlüssel sind Umgebungsvariablen, die deepseek-acp beim Start liest. Modell und Denkstufe sind Auswahlfelder pro Sitzung — sie werden im Eingabefeld gewählt.", @@ -1704,10 +1696,6 @@ "saveAntigravityConfigFailed": "Failed to save Antigravity config", "kiroSaved": "Kiro-Konfiguration gespeichert", "saveKiroFailed": "Kiro-Konfiguration konnte nicht gespeichert werden", - "traeSaved": "TRAE CLI-Konfiguration gespeichert", - "saveTraeFailed": "TRAE CLI-Konfiguration konnte nicht gespeichert werden", - "commandCodeSaved": "Command Code-Konfiguration gespeichert", - "saveCommandCodeFailed": "Command Code-Konfiguration konnte nicht gespeichert werden", "systemAgentAlreadyLatest": "{name} ist bereits aktuell ({version})" }, "distribution": { @@ -2479,10 +2467,7 @@ "commits": "Einträge" }, "noFolderTitle": "Kein Ordner geöffnet", - "noFolderHint": "Öffnen Sie einen Ordner, um seinen Inhalt hier zu sehen", - "openFullPage": "Als ganze Seite öffnen", - "back": "Zurück", - "noAutomations": "Für dieses Projekt gibt es noch keine Automatisierungen" + "noFolderHint": "Öffnen Sie einen Ordner, um seinen Inhalt hier zu sehen" }, "windowControls": { "minimizeWindow": "Fenster minimieren", @@ -3381,7 +3366,6 @@ }, "messageInput": { "askAnything": "Fragen Sie alles...", - "promptSuggestion": "{suggestion} (Tab zum Übernehmen)", "removeAttachmentAria": "{name} entfernen", "attachFiles": "Dateien anhängen", "addActions": "Hinzufügen", @@ -3411,10 +3395,6 @@ "searchModelAria": "Modelle suchen", "modelListLabel": "Modelle", "recommendedBadge": "Empfohlen", - "recommendedValue": "Empfohlen: {value}", - "searchOptions": "Optionen suchen...", - "searchOptionsAria": "Optionen suchen", - "noOptions": "Keine Optionen gefunden", "noModels": "Keine Modelle gefunden", "cancel": "Abbrechen", "send": "Senden", @@ -4027,16 +4007,13 @@ }, "messageNav": { "title": "Nachrichtennavigation", + "collapse": "Nachrichtennavigation ausblenden", "loadingEarlier": "Frühere Nachrichten werden geladen…", "loadEarlier": "Frühere Nachrichten laden", + "collapsedSummary": "Nachrichten {count}", "fileCount": "{count, plural, one {# Datei} other {# Dateien}}", - "subAgentCount": "{count, plural, one {# Subagent} other {# Subagenten}}", - "showDiff": "Änderungen an {filePath} anzeigen", - "hideDiff": "Änderungen an {filePath} ausblenden", - "openFile": "{filePath} öffnen", - "collapse": "Einklappen", - "currentMessage": "Aktuelle Nachricht", - "scrollToCurrent": "Zur aktuellen Nachricht (#{ordinal}) scrollen" + "remove": "Entfernen", + "noDiffDataAvailable": "Keine Diff-Daten verfügbar für {filePath}" }, "replyArtifacts": { "title": "Geänderte Dateien", @@ -4094,9 +4071,6 @@ "resumeDelegation": "Aufgabe {task} wird fortgesetzt", "resumeDelegationNoId": "Delegierte Aufgabe wird fortgesetzt", "promptLabel": "Anweisung", - "ackRecorded": "In der Arbeitsaufgabe erfasst", - "ackNotRecorded": "Nicht in der Arbeitsaufgabe erfasst", - "ackNotRecordedWithReason": "Nicht in der Arbeitsaufgabe erfasst: {reason}", "verdict": { "success": "erfolgreich", "needs_review": "Prüfung nötig", @@ -4214,18 +4188,6 @@ "stopDeclined": "Diese Aufgabe konnte nicht gestoppt werden – sie ist möglicherweise bereits beendet.", "stopFailed": "Aufgabe konnte nicht gestoppt werden: {error}" }, - "remoteControl": { - "title": "Fernsteuerung aktiv", - "open": "Im Browser öffnen", - "copy": "Link kopieren", - "copied": "Kopiert", - "qr": "QR-Code", - "qrLabel": "QR-Code des Fernsteuerungs-Links dieser Sitzung", - "qrHint": "Mit dem Handy scannen, um diese Sitzung in der Claude-App oder auf claude.ai/code fortzusetzen.", - "noUrl": "Claude Code hat keinen Link für diese Sitzung zurückgegeben", - "turnOff": "Ausschalten", - "turnOffBusy": "Verfügbar, sobald Claude fertig ist" - }, "proposedPlan": { "title": "Vorgeschlagener Plan", "planning": "Wird geplant…" @@ -7433,44 +7395,62 @@ "renameConversation": "Unterhaltung umbenennen", "openSessionFolder": "Sitzungsdateien", "cycleAgent": "Nächster Agent", - "cycleFavoriteAgent": "Favoriten-Agenten" + "cycleFavoriteAgent": "Favoriten-Agenten", + "openBallBindings": "Gesten-Zuordnung", + "cycleRunningSessions": "Laufende Sitzungen", + "cycleRecentSessions": "Letzte Sitzungen", + "deleteCurrentConversation": "Aktuelle Sitzung löschen", + "openImportSessions": "Sitzungen importieren", + "togglePinConversation": "Anpinnen / lösen", + "focusModeSelector": "Modus-Auswahl", + "togglePlanFullAccess": "Plan / Vollzugriff", + "openCollaboration": "Kollaboration" }, "gestures": { "tap": "Tippen", - "doubleTap": "Doppeltippen", - "longPress": "Langes Drücken", - "left": "Nach links wischen", - "right": "Nach rechts wischen", - "up": "Nach oben wischen", - "upLeft": "Oben links", - "upRight": "Oben rechts", - "upThenLeft": "Oben, dann links", - "upThenRight": "Oben, dann rechts", - "upDown": "Oben, dann unten", - "leftThenRight": "Links, dann rechts", - "rightThenLeft": "Rechts, dann links", - "upLeftThenDown": "Oben links, dann unten", - "upRightThenDown": "Oben rechts, dann unten", - "down": "Nach unten", - "downLeft": "Unten-links", - "downRight": "Unten-rechts", - "downUp": "Unten, dann oben", - "upThenDownRight": "Oben, dann unten-re.", - "downThenUpRight": "Unten, dann oben-re.", - "rightThenUp": "Rechts, dann oben", - "rightThenDown": "Rechts, dann unten", - "downThenRight": "Unten, dann rechts", - "downThenLeft": "Unten, dann links", - "downRightThenUp": "Unten-re., dann oben", - "downLeftThenUp": "Unten-li., dann oben", - "upLeftThenRight": "Oben-li., dann rechts", - "upRightThenLeft": "Oben-re., dann links" + "doubleTap": "Doppelt tippen", + "longPress": "Lange drücken", + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ dann ←", + "upThenRight": "↑ dann →", + "upDown": "↑ dann ↓", + "leftThenRight": "← dann →", + "rightThenLeft": "→ dann ←", + "upLeftThenDown": "↖ dann ↓", + "upRightThenDown": "↗ dann ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ dann ↑", + "upThenDownRight": "↑ dann ↘", + "downThenUpRight": "↓ dann ↗", + "rightThenUp": "→ dann ↑", + "rightThenDown": "→ dann ↓", + "downThenRight": "↓ dann →", + "downThenLeft": "↓ dann ←", + "downRightThenUp": "↘ dann ↑", + "downLeftThenUp": "↙ dann ↑", + "upLeftThenRight": "↖ dann →", + "upRightThenLeft": "↗ dann ←" }, "favoriteAgentsEmpty": "Keine Agenten verfügbar", "toasts": { "renameNeedsSavedSession": "Umbenennen gilt nur für gespeicherte Sitzungen – senden Sie zuerst eine Nachricht.", "sessionFolderNeedsWorkspace": "Öffnen Sie zuerst einen Arbeitsbereich-Ordner.", - "cycleNeedsMoreAgents": "Nur ein Agent verfügbar – nichts zum Wechseln." - } + "cycleNeedsMoreAgents": "Nur ein Agent verfügbar – nichts zum Wechseln.", + "noOtherRunning": "Keine weitere laufende Sitzung.", + "noOtherRecent": "Keine weitere letzte Sitzung.", + "noPlanMode": "Dieser Agent hat keinen Plan-Modus.", + "pinToggled": "Pin-Status aktualisiert.", + "noRunningSession": "Noch keine laufende Sitzung.", + "noModesAvailable": "Dieser Agent stellt keine Modi bereit.", + "noSwitchTarget": "Kein Modus zum Wechseln gefunden (Plan- oder Vollzugriffsmodus fehlt)." + }, + "recentSessionDepth": "Umschalttiefe letzte Sitzungen", + "bindingCleared": "Nicht zugewiesen" } -} +} \ No newline at end of file diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 631faac6cf..5a5cf2ac7b 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -181,6 +181,13 @@ "sectionDescription": "Entrance, expand/collapse, tool-call and working-status animations in the conversation area, plus smooth scrolling. Turn off for instant, motion-free updates. Spinners keep rotating.", "enable": "Enable chat animations" }, + "messageNav": { + "sectionTitle": "Message navigator", + "sectionDescription": "Where the per-conversation message navigator sits. Bottom-right keeps it as a compact one-row-per-turn card above the composer, out of the reading column.", + "positionStart": "Top-left (default)", + "positionBottomEnd": "Bottom-right (compact)", + "hint": "Plan and sub-agent panels stay top-left either way." + }, "chatWidth": { "sectionTitle": "Chat width", "sectionDescription": "Maximum width of the conversation column and the composer. You can also drag the edges of the chat column to adjust it.", @@ -261,32 +268,32 @@ "tap": "Tap", "doubleTap": "Double tap", "longPress": "Long press", - "left": "Swipe left", - "right": "Swipe right", - "up": "Swipe up", - "upLeft": "Up-left", - "upRight": "Up-right", - "upThenLeft": "Up, then left", - "upThenRight": "Up, then right", - "upDown": "Up, then down", - "leftThenRight": "Left, then right", - "rightThenLeft": "Right, then left", - "upLeftThenDown": "Up-left, then down", - "upRightThenDown": "Up-right, then down", - "down": "Swipe down", - "downLeft": "Down-left", - "downRight": "Down-right", - "downUp": "Down, then up", - "upThenDownRight": "Up, then down-right", - "downThenUpRight": "Down, then up-right", - "rightThenUp": "Right, then up", - "rightThenDown": "Right, then down", - "downThenRight": "Down, then right", - "downThenLeft": "Down, then left", - "downRightThenUp": "Down-right, then up", - "downLeftThenUp": "Down-left, then up", - "upLeftThenRight": "Up-left, then right", - "upRightThenLeft": "Up-right, then left" + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ then ←", + "upThenRight": "↑ then →", + "upDown": "↑ then ↓", + "leftThenRight": "← then →", + "rightThenLeft": "→ then ←", + "upLeftThenDown": "↖ then ↓", + "upRightThenDown": "↗ then ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ then ↑", + "upThenDownRight": "↑ then ↘", + "downThenUpRight": "↓ then ↗", + "rightThenUp": "→ then ↑", + "rightThenDown": "→ then ↓", + "downThenRight": "↓ then →", + "downThenLeft": "↓ then ←", + "downRightThenUp": "↘ then ↑", + "downLeftThenUp": "↙ then ↑", + "upLeftThenRight": "↖ then →", + "upRightThenLeft": "↗ then ←" }, "actions": { "newConversation": "New conversation", @@ -311,7 +318,16 @@ "renameConversation": "Rename conversation", "openSessionFolder": "Session files", "cycleAgent": "Next agent", - "cycleFavoriteAgent": "Favorite agents" + "cycleFavoriteAgent": "Favorite agents", + "openBallBindings": "Gesture bindings", + "cycleRunningSessions": "Running sessions", + "cycleRecentSessions": "Recent sessions", + "deleteCurrentConversation": "Delete current session", + "openImportSessions": "Import sessions", + "togglePinConversation": "Pin / unpin session", + "focusModeSelector": "Mode selector", + "togglePlanFullAccess": "Plan / full access", + "openCollaboration": "Collaboration" } }, "SystemSettings": { @@ -1185,30 +1201,6 @@ "apiKeyHint": "Passed to the CLI as KIRO_API_KEY. Leave empty to use the account you signed in with via kiro-cli login; an API key is only needed where no browser sign-in is possible (servers, Docker).", "saveConfig": "Save Kiro Config" }, - "trae": { - "configManagement": "Configuration", - "configDescription": "TRAE CLI 2.0 is available to customers on the flagship TRAE Enterprise plan. Codeg runs the traecli you installed, which keeps its sign-in, skills and MCP servers in ~/.trae. Model, mode and reasoning effort are not here: TRAE CLI reports them over ACP, so the composer's own selectors own them per session.", - "signInTitle": "Sign in", - "signInHint": "Run this once in a terminal to sign in with your enterprise SSO. Sessions in Codeg reuse that login.", - "copyCommand": "Copy command", - "commandCopied": "Command copied", - "patLabel": "Personal access token (optional)", - "patHint": "For a machine where browser sign-in is not possible, such as a server or Docker install. Codeg passes it to TRAE CLI as TRAECLI_PERSONAL_ACCESS_TOKEN and signs in with it whenever a session starts. Leave empty to use the login from traecli login.", - "hostLabel": "Enterprise API URL (optional)", - "hostHint": "Only if your organization runs TRAE on its own domain. Passed as TRAECLI_HOST; leave empty for TRAE's own service.", - "saveConfig": "Save TRAE CLI Config" - }, - "commandCode": { - "configManagement": "Configuration", - "configDescription": "Codeg runs the Command Code CLI from the command-code npm package, which keeps its sign-in, skills and MCP servers in ~/.commandcode. Model, mode and reasoning effort are not here: Command Code reports them over ACP, so the composer's own selectors own them per session.", - "signInTitle": "Sign in", - "signInHint": "Run this once in a terminal to sign in with your Command Code account. Sessions in Codeg reuse that login.", - "copyCommand": "Copy command", - "commandCopied": "Command copied", - "apiKeyLabel": "API key (optional)", - "apiKeyHint": "For a machine where browser sign-in is not possible, such as a server or Docker install. Passed to Command Code as COMMAND_CODE_API_KEY, which takes precedence over the login from command-code login. Leave empty to use that login.", - "saveConfig": "Save Command Code Config" - }, "deepseek": { "configManagement": "DeepSeek Harness Configuration", "configDescription": "The endpoint and key are environment variables deepseek-acp reads at launch. Model and reasoning effort are per-session selectors — pick them in the composer.", @@ -1704,10 +1696,6 @@ "saveAntigravityConfigFailed": "Failed to save Antigravity config", "kiroSaved": "Kiro config saved", "saveKiroFailed": "Failed to save Kiro config", - "traeSaved": "TRAE CLI config saved", - "saveTraeFailed": "Failed to save TRAE CLI config", - "commandCodeSaved": "Command Code config saved", - "saveCommandCodeFailed": "Failed to save Command Code config", "systemAgentAlreadyLatest": "{name} is already up to date ({version})" }, "distribution": { @@ -2479,10 +2467,7 @@ "commits": "Commits" }, "noFolderTitle": "No folder open", - "noFolderHint": "Open a folder to see its contents here", - "openFullPage": "Open full page", - "back": "Back", - "noAutomations": "No automations for this project yet" + "noFolderHint": "Open a folder to see its contents here" }, "windowControls": { "minimizeWindow": "Minimize window", @@ -3381,7 +3366,6 @@ }, "messageInput": { "askAnything": "Ask anything...", - "promptSuggestion": "{suggestion} (Tab to use)", "removeAttachmentAria": "Remove {name}", "attachFiles": "Attach files", "attachLocalUpload": "Upload local file", @@ -3413,10 +3397,6 @@ "searchModelAria": "Search models", "modelListLabel": "Models", "recommendedBadge": "Recommended", - "recommendedValue": "Recommended: {value}", - "searchOptions": "Search options...", - "searchOptionsAria": "Search options", - "noOptions": "No options found", "noModels": "No models found", "cancel": "Cancel", "send": "Send", @@ -4027,16 +4007,13 @@ }, "messageNav": { "title": "Message navigation", + "collapse": "Collapse message navigation", "loadingEarlier": "Loading earlier messages…", "loadEarlier": "Load earlier messages", + "collapsedSummary": "Messages {count}", "fileCount": "{count, plural, one {# file} other {# files}}", - "subAgentCount": "{count, plural, one {# sub-agent} other {# sub-agents}}", - "showDiff": "Show the changes to {filePath}", - "hideDiff": "Hide the changes to {filePath}", - "openFile": "Open {filePath}", - "collapse": "Collapse", - "currentMessage": "Current message", - "scrollToCurrent": "Scroll to the current message (#{ordinal})" + "remove": "Remove", + "noDiffDataAvailable": "No diff data available for {filePath}" }, "replyArtifacts": { "title": "Files changed", @@ -4094,9 +4071,6 @@ "resumeDelegation": "Resuming task {task}", "resumeDelegationNoId": "Resuming a delegated task", "promptLabel": "Prompt", - "ackRecorded": "Recorded on the work task", - "ackNotRecorded": "Not recorded on the work task", - "ackNotRecordedWithReason": "Not recorded on the work task: {reason}", "verdict": { "success": "success", "needs_review": "needs review", @@ -4214,18 +4188,6 @@ "stopDeclined": "That task could not be stopped — it may have already finished.", "stopFailed": "Failed to stop the task: {error}" }, - "remoteControl": { - "title": "Remote Control is on", - "open": "Open in browser", - "copy": "Copy link", - "copied": "Copied", - "qr": "QR code", - "qrLabel": "QR code for this session's Remote Control link", - "qrHint": "Scan with your phone to continue this session in the Claude app or on claude.ai/code.", - "noUrl": "Claude Code returned no link for this session", - "turnOff": "Turn off", - "turnOffBusy": "Available once Claude has finished" - }, "proposedPlan": { "title": "Proposed plan", "planning": "Planning…" @@ -7433,44 +7395,62 @@ "renameConversation": "Rename conversation", "openSessionFolder": "Session files", "cycleAgent": "Next agent", - "cycleFavoriteAgent": "Favorite agents" + "cycleFavoriteAgent": "Favorite agents", + "openBallBindings": "Gesture bindings", + "cycleRunningSessions": "Running sessions", + "cycleRecentSessions": "Recent sessions", + "deleteCurrentConversation": "Delete current session", + "openImportSessions": "Import sessions", + "togglePinConversation": "Pin / unpin session", + "focusModeSelector": "Mode selector", + "togglePlanFullAccess": "Plan / full access", + "openCollaboration": "Collaboration" }, "gestures": { "tap": "Tap", "doubleTap": "Double tap", "longPress": "Long press", - "left": "Swipe left", - "right": "Swipe right", - "up": "Swipe up", - "upLeft": "Up-left", - "upRight": "Up-right", - "upThenLeft": "Up, then left", - "upThenRight": "Up, then right", - "upDown": "Up, then down", - "leftThenRight": "Left, then right", - "rightThenLeft": "Right, then left", - "upLeftThenDown": "Up-left, then down", - "upRightThenDown": "Up-right, then down", - "down": "Swipe down", - "downLeft": "Down-left", - "downRight": "Down-right", - "downUp": "Down, then up", - "upThenDownRight": "Up, then down-right", - "downThenUpRight": "Down, then up-right", - "rightThenUp": "Right, then up", - "rightThenDown": "Right, then down", - "downThenRight": "Down, then right", - "downThenLeft": "Down, then left", - "downRightThenUp": "Down-right, then up", - "downLeftThenUp": "Down-left, then up", - "upLeftThenRight": "Up-left, then right", - "upRightThenLeft": "Up-right, then left" + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ then ←", + "upThenRight": "↑ then →", + "upDown": "↑ then ↓", + "leftThenRight": "← then →", + "rightThenLeft": "→ then ←", + "upLeftThenDown": "↖ then ↓", + "upRightThenDown": "↗ then ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ then ↑", + "upThenDownRight": "↑ then ↘", + "downThenUpRight": "↓ then ↗", + "rightThenUp": "→ then ↑", + "rightThenDown": "→ then ↓", + "downThenRight": "↓ then →", + "downThenLeft": "↓ then ←", + "downRightThenUp": "↘ then ↑", + "downLeftThenUp": "↙ then ↑", + "upLeftThenRight": "↖ then →", + "upRightThenLeft": "↗ then ←" }, "favoriteAgentsEmpty": "No available agents", "toasts": { "renameNeedsSavedSession": "Rename works on a saved session — send a message first.", "sessionFolderNeedsWorkspace": "Open a workspace folder first.", - "cycleNeedsMoreAgents": "Only one agent available — nothing to cycle." - } + "cycleNeedsMoreAgents": "Only one agent available — nothing to cycle.", + "noOtherRunning": "No other running session to switch to.", + "noOtherRecent": "No other recent session to switch to.", + "noPlanMode": "This agent has no Plan mode.", + "pinToggled": "Pin state updated.", + "noRunningSession": "No running session yet.", + "noModesAvailable": "This agent does not expose any modes.", + "noSwitchTarget": "No mode to switch to (Plan or full-access mode not found)." + }, + "recentSessionDepth": "Recent-session switch depth", + "bindingCleared": "Unbound" } -} +} \ No newline at end of file diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 0734ca1a46..fa70d2dc1b 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -181,6 +181,13 @@ "sectionDescription": "Animaciones de plegado/desplegado, llamadas a herramientas y estado de trabajo en el área de conversación, además del desplazamiento suave. Desactívalas para actualizaciones inmediatas. Los indicadores de carga siguen girando.", "enable": "Activar las animaciones del chat" }, + "messageNav": { + "sectionTitle": "Navegador de mensajes", + "sectionDescription": "Posición del navegador de mensajes. Abajo a la derecha se convierte en una tarjeta compacta con una fila por mensaje del usuario, sobre el cuadro de entrada.", + "positionStart": "Arriba a la izquierda (predeterminado)", + "positionBottomEnd": "Abajo a la derecha (compacto)", + "hint": "Los paneles de plan y de subagentes siguen arriba a la izquierda." + }, "chatWidth": { "sectionTitle": "Ancho del chat", "sectionDescription": "Ancho máximo de la columna de conversación y del cuadro de entrada. También puedes arrastrar los bordes del chat para ajustarlo.", @@ -258,60 +265,69 @@ "panelTitle": "Acciones rápidas" }, "gestures": { - "tap": "Tap", - "doubleTap": "Double tap", - "longPress": "Long press", - "left": "Swipe left", - "right": "Swipe right", - "up": "Swipe up", - "upLeft": "Up-left", - "upRight": "Up-right", - "upThenLeft": "Up, then left", - "upThenRight": "Up, then right", - "upDown": "Up, then down", - "leftThenRight": "Left, then right", - "rightThenLeft": "Right, then left", - "upLeftThenDown": "Up-left, then down", - "upRightThenDown": "Up-right, then down", - "down": "Swipe down", - "downLeft": "Down-left", - "downRight": "Down-right", - "downUp": "Down, then up", - "upThenDownRight": "Up, then down-right", - "downThenUpRight": "Down, then up-right", - "rightThenUp": "Right, then up", - "rightThenDown": "Right, then down", - "downThenRight": "Down, then right", - "downThenLeft": "Down, then left", - "downRightThenUp": "Down-right, then up", - "downLeftThenUp": "Down-left, then up", - "upLeftThenRight": "Up-left, then right", - "upRightThenLeft": "Up-right, then left" + "tap": "Toque", + "doubleTap": "Doble toque", + "longPress": "Mantener pulsado", + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ luego ←", + "upThenRight": "↑ luego →", + "upDown": "↑ luego ↓", + "leftThenRight": "← luego →", + "rightThenLeft": "→ luego ←", + "upLeftThenDown": "↖ luego ↓", + "upRightThenDown": "↗ luego ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ luego ↑", + "upThenDownRight": "↑ luego ↘", + "downThenUpRight": "↓ luego ↗", + "rightThenUp": "→ luego ↑", + "rightThenDown": "→ luego ↓", + "downThenRight": "↓ luego →", + "downThenLeft": "↓ luego ←", + "downRightThenUp": "↘ luego ↑", + "downLeftThenUp": "↙ luego ↑", + "upLeftThenRight": "↖ luego →", + "upRightThenLeft": "↗ luego ←" }, "actions": { - "newConversation": "New conversation", - "toggleSidebar": "Toggle sidebar", - "toggleSearch": "Search", - "toggleTerminal": "Toggle terminal", - "nextTab": "Next tab", - "prevTab": "Previous tab", - "reopenClosedTab": "Reopen closed tab", - "openFolder": "Open folder", - "zoomIn": "Zoom in", - "zoomOut": "Zoom out", - "zoomReset": "Reset zoom", - "stopGenerating": "Stop generating", - "openSettings": "Settings", - "openTodos": "To-dos", - "openTodosNew": "New to-do", - "openAutomations": "Automations", - "openCanvas": "Canvas", - "openAgents": "Agents", - "openSystemAdmin": "System admin", - "renameConversation": "Rename conversation", - "openSessionFolder": "Session files", - "cycleAgent": "Next agent", - "cycleFavoriteAgent": "Favorite agents" + "newConversation": "Nueva conversación", + "toggleSidebar": "Mostrar u ocultar la barra lateral", + "toggleSearch": "Buscar", + "toggleTerminal": "Mostrar u ocultar la terminal", + "nextTab": "Pestaña siguiente", + "prevTab": "Pestaña anterior", + "reopenClosedTab": "Reabrir pestaña cerrada", + "openFolder": "Abrir carpeta", + "zoomIn": "Acercar", + "zoomOut": "Alejar", + "zoomReset": "Restablecer zoom", + "stopGenerating": "Detener la generación", + "openSettings": "Ajustes", + "openTodos": "Tareas", + "openTodosNew": "Nueva tarea", + "openAutomations": "Automatizaciones", + "openCanvas": "Lienzo", + "openAgents": "Agentes", + "openSystemAdmin": "Administración", + "renameConversation": "Renombrar conversación", + "openSessionFolder": "Archivos de sesión", + "cycleAgent": "Siguiente agente", + "cycleFavoriteAgent": "Agentes favoritos", + "openBallBindings": "Atajos de gestos", + "cycleRunningSessions": "Sesiones en curso", + "cycleRecentSessions": "Sesiones recientes", + "deleteCurrentConversation": "Eliminar sesión actual", + "openImportSessions": "Importar sesiones", + "togglePinConversation": "Fijar / desfijar", + "focusModeSelector": "Selector de modo", + "togglePlanFullAccess": "Plan / acceso total", + "openCollaboration": "Colaboración" } }, "SystemSettings": { @@ -1185,30 +1201,6 @@ "apiKeyHint": "Se pasa a la CLI como KIRO_API_KEY. Déjala vacía para usar la cuenta con la que iniciaste sesión mediante kiro-cli login; solo hace falta donde no se puede iniciar sesión con el navegador (servidores, Docker).", "saveConfig": "Guardar configuración de Kiro" }, - "trae": { - "configManagement": "Configuración", - "configDescription": "TRAE CLI 2.0 solo está disponible para clientes del plan insignia de TRAE Enterprise. Codeg ejecuta el traecli que instalaste, que guarda su inicio de sesión, sus habilidades y sus servidores MCP en ~/.trae. El modelo, el modo y el esfuerzo de razonamiento no están aquí: TRAE CLI los informa por ACP, así que los selectores del compositor los gestionan en cada sesión.", - "signInTitle": "Iniciar sesión", - "signInHint": "Ejecuta esto una vez en una terminal para iniciar sesión con el SSO de tu empresa. Las sesiones de Codeg reutilizan ese inicio de sesión.", - "copyCommand": "Copiar comando", - "commandCopied": "Comando copiado", - "patLabel": "Token de acceso personal (opcional)", - "patHint": "Para una máquina donde no se puede iniciar sesión con el navegador, como un servidor o una instalación de Docker. Codeg lo pasa a TRAE CLI como TRAECLI_PERSONAL_ACCESS_TOKEN e inicia sesión con él cada vez que empieza una sesión. Déjalo vacío para usar el inicio de sesión de traecli login.", - "hostLabel": "URL de la API empresarial (opcional)", - "hostHint": "Solo si tu organización ejecuta TRAE en su propio dominio. Se pasa como TRAECLI_HOST; déjala vacía para usar el servicio de TRAE.", - "saveConfig": "Guardar configuración de TRAE CLI" - }, - "commandCode": { - "configManagement": "Configuración", - "configDescription": "Codeg ejecuta la CLI de Command Code del paquete npm command-code, que guarda su inicio de sesión, sus habilidades y sus servidores MCP en ~/.commandcode. El modelo, el modo y el esfuerzo de razonamiento no están aquí: Command Code los informa por ACP, así que los selectores del compositor los gestionan en cada sesión.", - "signInTitle": "Iniciar sesión", - "signInHint": "Ejecuta esto una vez en una terminal para iniciar sesión con tu cuenta de Command Code. Las sesiones de Codeg reutilizan ese inicio de sesión.", - "copyCommand": "Copiar comando", - "commandCopied": "Comando copiado", - "apiKeyLabel": "Clave de API (opcional)", - "apiKeyHint": "Para una máquina donde no se puede iniciar sesión con el navegador, como un servidor o una instalación de Docker. Se pasa a Command Code como COMMAND_CODE_API_KEY, que tiene prioridad sobre el inicio de sesión de command-code login. Déjala vacía para usar ese inicio de sesión.", - "saveConfig": "Guardar configuración de Command Code" - }, "deepseek": { "configManagement": "Configuración de DeepSeek Harness", "configDescription": "El endpoint y la clave son variables de entorno que deepseek-acp lee al arrancar. El modelo y el esfuerzo de razonamiento son selectores por sesión: se eligen en el campo de mensaje.", @@ -1704,10 +1696,6 @@ "saveAntigravityConfigFailed": "Failed to save Antigravity config", "kiroSaved": "Configuración de Kiro guardada", "saveKiroFailed": "No se pudo guardar la configuración de Kiro", - "traeSaved": "Configuración de TRAE CLI guardada", - "saveTraeFailed": "No se pudo guardar la configuración de TRAE CLI", - "commandCodeSaved": "Configuración de Command Code guardada", - "saveCommandCodeFailed": "No se pudo guardar la configuración de Command Code", "systemAgentAlreadyLatest": "{name} ya está al día ({version})" }, "distribution": { @@ -2479,10 +2467,7 @@ "commits": "Confirmaciones" }, "noFolderTitle": "No hay carpeta abierta", - "noFolderHint": "Abre una carpeta para ver su contenido aquí", - "openFullPage": "Abrir en página completa", - "back": "Atrás", - "noAutomations": "Este proyecto aún no tiene automatizaciones" + "noFolderHint": "Abre una carpeta para ver su contenido aquí" }, "windowControls": { "minimizeWindow": "Minimizar ventana", @@ -3381,7 +3366,6 @@ }, "messageInput": { "askAnything": "Pregunta lo que sea...", - "promptSuggestion": "{suggestion} (Tab para usarla)", "removeAttachmentAria": "Quitar {name}", "attachFiles": "Adjuntar archivos", "addActions": "Añadir", @@ -3411,10 +3395,6 @@ "searchModelAria": "Buscar modelos", "modelListLabel": "Modelos", "recommendedBadge": "Recomendado", - "recommendedValue": "Recomendado: {value}", - "searchOptions": "Buscar opciones...", - "searchOptionsAria": "Buscar opciones", - "noOptions": "No se encontraron opciones", "noModels": "No se encontraron modelos", "cancel": "Cancelar", "send": "Enviar", @@ -4027,16 +4007,13 @@ }, "messageNav": { "title": "Navegación de mensajes", + "collapse": "Contraer navegación de mensajes", "loadingEarlier": "Cargando mensajes anteriores…", "loadEarlier": "Cargar mensajes anteriores", + "collapsedSummary": "Mensajes {count}", "fileCount": "{count, plural, one {# archivo} other {# archivos}}", - "subAgentCount": "{count, plural, one {# subagente} other {# subagentes}}", - "showDiff": "Mostrar los cambios de {filePath}", - "hideDiff": "Ocultar los cambios de {filePath}", - "openFile": "Abrir {filePath}", - "collapse": "Contraer", - "currentMessage": "Mensaje actual", - "scrollToCurrent": "Desplazarse al mensaje actual (#{ordinal})" + "remove": "Quitar", + "noDiffDataAvailable": "No hay datos de diff disponibles para {filePath}" }, "replyArtifacts": { "title": "Archivos modificados", @@ -4094,9 +4071,6 @@ "resumeDelegation": "Reanudando la tarea {task}", "resumeDelegationNoId": "Reanudando una tarea delegada", "promptLabel": "Instrucción", - "ackRecorded": "Registrado en la tarea de trabajo", - "ackNotRecorded": "No registrado en la tarea de trabajo", - "ackNotRecordedWithReason": "No registrado en la tarea de trabajo: {reason}", "verdict": { "success": "correcta", "needs_review": "requiere revisión", @@ -4214,18 +4188,6 @@ "stopDeclined": "No se pudo detener esa tarea; puede que ya haya terminado.", "stopFailed": "No se pudo detener la tarea: {error}" }, - "remoteControl": { - "title": "Control remoto activado", - "open": "Abrir en el navegador", - "copy": "Copiar enlace", - "copied": "Copiado", - "qr": "Código QR", - "qrLabel": "Código QR del enlace de control remoto de esta sesión", - "qrHint": "Escanéalo con el teléfono para continuar esta sesión en la app de Claude o en claude.ai/code.", - "noUrl": "Claude Code no devolvió ningún enlace para esta sesión", - "turnOff": "Desactivar", - "turnOffBusy": "Disponible cuando Claude termine" - }, "proposedPlan": { "title": "Plan propuesto", "planning": "Planificando…" @@ -7433,44 +7395,62 @@ "renameConversation": "Renombrar conversación", "openSessionFolder": "Archivos de sesión", "cycleAgent": "Siguiente agente", - "cycleFavoriteAgent": "Agentes favoritos" + "cycleFavoriteAgent": "Agentes favoritos", + "openBallBindings": "Atajos de gestos", + "cycleRunningSessions": "Sesiones en curso", + "cycleRecentSessions": "Sesiones recientes", + "deleteCurrentConversation": "Eliminar sesión actual", + "openImportSessions": "Importar sesiones", + "togglePinConversation": "Fijar / desfijar", + "focusModeSelector": "Selector de modo", + "togglePlanFullAccess": "Plan / acceso total", + "openCollaboration": "Colaboración" }, "gestures": { "tap": "Toque", "doubleTap": "Doble toque", - "longPress": "Pulsación larga", - "left": "Deslizar a la izquierda", - "right": "Deslizar a la derecha", - "up": "Deslizar hacia arriba", - "upLeft": "Arriba a la izquierda", - "upRight": "Arriba a la derecha", - "upThenLeft": "Arriba y luego a la izquierda", - "upThenRight": "Arriba y luego a la derecha", - "upDown": "Arriba y luego abajo", - "leftThenRight": "Izquierda y luego derecha", - "rightThenLeft": "Derecha y luego izquierda", - "upLeftThenDown": "Arriba a la izquierda y luego abajo", - "upRightThenDown": "Arriba a la derecha y luego abajo", - "down": "Deslizar abajo", - "downLeft": "Abajo-izq.", - "downRight": "Abajo-der.", - "downUp": "Abajo, luego arriba", - "upThenDownRight": "Arriba, luego abajo-der.", - "downThenUpRight": "Abajo, luego arriba-der.", - "rightThenUp": "Derecha, luego arriba", - "rightThenDown": "Derecha, luego abajo", - "downThenRight": "Abajo, luego derecha", - "downThenLeft": "Abajo, luego izquierda", - "downRightThenUp": "Abajo-der., luego arriba", - "downLeftThenUp": "Abajo-izq., luego arriba", - "upLeftThenRight": "Arriba-izq., luego derecha", - "upRightThenLeft": "Arriba-der., luego izquierda" + "longPress": "Mantener pulsado", + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ luego ←", + "upThenRight": "↑ luego →", + "upDown": "↑ luego ↓", + "leftThenRight": "← luego →", + "rightThenLeft": "→ luego ←", + "upLeftThenDown": "↖ luego ↓", + "upRightThenDown": "↗ luego ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ luego ↑", + "upThenDownRight": "↑ luego ↘", + "downThenUpRight": "↓ luego ↗", + "rightThenUp": "→ luego ↑", + "rightThenDown": "→ luego ↓", + "downThenRight": "↓ luego →", + "downThenLeft": "↓ luego ←", + "downRightThenUp": "↘ luego ↑", + "downLeftThenUp": "↙ luego ↑", + "upLeftThenRight": "↖ luego →", + "upRightThenLeft": "↗ luego ←" }, "favoriteAgentsEmpty": "No hay agentes disponibles", "toasts": { "renameNeedsSavedSession": "Renombrar solo funciona en sesiones guardadas: envía un mensaje primero.", "sessionFolderNeedsWorkspace": "Abre antes una carpeta de espacio de trabajo.", - "cycleNeedsMoreAgents": "Solo hay un agente disponible: nada que alternar." - } + "cycleNeedsMoreAgents": "Solo hay un agente disponible: nada que alternar.", + "noOtherRunning": "No hay otra sesión en curso.", + "noOtherRecent": "No hay otra sesión reciente.", + "noPlanMode": "Este agente no tiene modo Plan.", + "pinToggled": "Estado de fijado actualizado.", + "noRunningSession": "Aún no hay sesión en curso.", + "noModesAvailable": "Este agente no expone ningún modo.", + "noSwitchTarget": "No hay modo al que cambiar (no se encontró modo Plan ni de acceso completo)." + }, + "recentSessionDepth": "Profundidad de sesiones recientes", + "bindingCleared": "Sin asignar" } -} +} \ No newline at end of file diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index da1c5539bc..7a4b3a2930 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -181,6 +181,13 @@ "sectionDescription": "Animations de pliage/dépliage, d'appels d'outils et d'état de travail dans la zone de conversation, ainsi que le défilement fluide. Désactivez-les pour des mises à jour instantanées. Les indicateurs de chargement continuent de tourner.", "enable": "Activer les animations du chat" }, + "messageNav": { + "sectionTitle": "Navigateur de messages", + "sectionDescription": "Position du navigateur de messages. En bas à droite, il devient une carte compacte avec une ligne par message utilisateur, au-dessus de la zone de saisie.", + "positionStart": "En haut à gauche (par défaut)", + "positionBottomEnd": "En bas à droite (compact)", + "hint": "Les panneaux de plan et de sous-agents restent en haut à gauche." + }, "chatWidth": { "sectionTitle": "Largeur du chat", "sectionDescription": "Largeur maximale de la colonne de conversation et de la zone de saisie. Vous pouvez aussi faire glisser les bords du chat pour l'ajuster.", @@ -258,60 +265,69 @@ "panelTitle": "Actions rapides" }, "gestures": { - "tap": "Tap", - "doubleTap": "Double tap", - "longPress": "Long press", - "left": "Swipe left", - "right": "Swipe right", - "up": "Swipe up", - "upLeft": "Up-left", - "upRight": "Up-right", - "upThenLeft": "Up, then left", - "upThenRight": "Up, then right", - "upDown": "Up, then down", - "leftThenRight": "Left, then right", - "rightThenLeft": "Right, then left", - "upLeftThenDown": "Up-left, then down", - "upRightThenDown": "Up-right, then down", - "down": "Swipe down", - "downLeft": "Down-left", - "downRight": "Down-right", - "downUp": "Down, then up", - "upThenDownRight": "Up, then down-right", - "downThenUpRight": "Down, then up-right", - "rightThenUp": "Right, then up", - "rightThenDown": "Right, then down", - "downThenRight": "Down, then right", - "downThenLeft": "Down, then left", - "downRightThenUp": "Down-right, then up", - "downLeftThenUp": "Down-left, then up", - "upLeftThenRight": "Up-left, then right", - "upRightThenLeft": "Up-right, then left" + "tap": "Touche", + "doubleTap": "Double touche", + "longPress": "Appui long", + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ puis ←", + "upThenRight": "↑ puis →", + "upDown": "↑ puis ↓", + "leftThenRight": "← puis →", + "rightThenLeft": "→ puis ←", + "upLeftThenDown": "↖ puis ↓", + "upRightThenDown": "↗ puis ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ puis ↑", + "upThenDownRight": "↑ puis ↘", + "downThenUpRight": "↓ puis ↗", + "rightThenUp": "→ puis ↑", + "rightThenDown": "→ puis ↓", + "downThenRight": "↓ puis →", + "downThenLeft": "↓ puis ←", + "downRightThenUp": "↘ puis ↑", + "downLeftThenUp": "↙ puis ↑", + "upLeftThenRight": "↖ puis →", + "upRightThenLeft": "↗ puis ←" }, "actions": { - "newConversation": "New conversation", - "toggleSidebar": "Toggle sidebar", - "toggleSearch": "Search", - "toggleTerminal": "Toggle terminal", - "nextTab": "Next tab", - "prevTab": "Previous tab", - "reopenClosedTab": "Reopen closed tab", - "openFolder": "Open folder", - "zoomIn": "Zoom in", - "zoomOut": "Zoom out", - "zoomReset": "Reset zoom", - "stopGenerating": "Stop generating", - "openSettings": "Settings", - "openTodos": "To-dos", - "openTodosNew": "New to-do", - "openAutomations": "Automations", + "newConversation": "Nouvelle conversation", + "toggleSidebar": "Afficher ou masquer la barre latérale", + "toggleSearch": "Rechercher", + "toggleTerminal": "Afficher ou masquer le terminal", + "nextTab": "Onglet suivant", + "prevTab": "Onglet précédent", + "reopenClosedTab": "Rouvrir l’onglet fermé", + "openFolder": "Ouvrir un dossier", + "zoomIn": "Zoom avant", + "zoomOut": "Zoom arrière", + "zoomReset": "Réinitialiser le zoom", + "stopGenerating": "Arrêter la génération", + "openSettings": "Paramètres", + "openTodos": "Tâches", + "openTodosNew": "Nouvelle tâche", + "openAutomations": "Automatisations", "openCanvas": "Canvas", "openAgents": "Agents", - "openSystemAdmin": "System admin", - "renameConversation": "Rename conversation", - "openSessionFolder": "Session files", - "cycleAgent": "Next agent", - "cycleFavoriteAgent": "Favorite agents" + "openSystemAdmin": "Administration", + "renameConversation": "Renommer la conversation", + "openSessionFolder": "Fichiers de session", + "cycleAgent": "Agent suivant", + "cycleFavoriteAgent": "Agents favoris", + "openBallBindings": "Assignation des gestes", + "cycleRunningSessions": "Sessions en cours", + "cycleRecentSessions": "Sessions récentes", + "deleteCurrentConversation": "Supprimer la session", + "openImportSessions": "Importer des sessions", + "togglePinConversation": "Épingler / désépingler", + "focusModeSelector": "Sélecteur de mode", + "togglePlanFullAccess": "Plan / accès complet", + "openCollaboration": "Collaboration" } }, "SystemSettings": { @@ -1185,30 +1201,6 @@ "apiKeyHint": "Transmise à la CLI en tant que KIRO_API_KEY. Laissez vide pour utiliser le compte connecté via kiro-cli login ; nécessaire seulement là où la connexion par navigateur est impossible (serveurs, Docker).", "saveConfig": "Enregistrer la configuration Kiro" }, - "trae": { - "configManagement": "Configuration", - "configDescription": "TRAE CLI 2.0 est réservé aux clients de l'offre phare de TRAE Enterprise. Codeg lance le traecli que vous avez installé, qui conserve sa connexion, ses compétences et ses serveurs MCP dans ~/.trae. Le modèle, le mode et l'effort de raisonnement ne sont pas ici : TRAE CLI les communique via ACP, donc les sélecteurs du compositeur les gèrent pour chaque session.", - "signInTitle": "Connexion", - "signInHint": "Exécutez cette commande une fois dans un terminal pour vous connecter avec le SSO de votre entreprise. Les sessions de Codeg réutilisent cette connexion.", - "copyCommand": "Copier la commande", - "commandCopied": "Commande copiée", - "patLabel": "Jeton d'accès personnel (facultatif)", - "patHint": "Pour une machine où la connexion par navigateur est impossible, comme un serveur ou une installation Docker. Codeg le transmet à TRAE CLI en tant que TRAECLI_PERSONAL_ACCESS_TOKEN et se connecte avec lui au démarrage de chaque session. Laissez vide pour utiliser la connexion de traecli login.", - "hostLabel": "URL de l'API d'entreprise (facultative)", - "hostHint": "Uniquement si votre organisation exploite TRAE sur son propre domaine. Transmise en tant que TRAECLI_HOST ; laissez vide pour le service de TRAE.", - "saveConfig": "Enregistrer la configuration TRAE CLI" - }, - "commandCode": { - "configManagement": "Configuration", - "configDescription": "Codeg lance la CLI Command Code du paquet npm command-code, qui conserve sa connexion, ses compétences et ses serveurs MCP dans ~/.commandcode. Le modèle, le mode et l'effort de raisonnement ne sont pas ici : Command Code les communique via ACP, donc les sélecteurs du compositeur les gèrent pour chaque session.", - "signInTitle": "Connexion", - "signInHint": "Exécutez cette commande une fois dans un terminal pour vous connecter avec votre compte Command Code. Les sessions de Codeg réutilisent cette connexion.", - "copyCommand": "Copier la commande", - "commandCopied": "Commande copiée", - "apiKeyLabel": "Clé d'API (facultative)", - "apiKeyHint": "Pour une machine où la connexion par navigateur est impossible, comme un serveur ou une installation Docker. Transmise à Command Code en tant que COMMAND_CODE_API_KEY, prioritaire sur la connexion de command-code login. Laissez vide pour utiliser cette connexion.", - "saveConfig": "Enregistrer la configuration Command Code" - }, "deepseek": { "configManagement": "Configuration de DeepSeek Harness", "configDescription": "Le point de terminaison et la clé sont des variables d'environnement que deepseek-acp lit au démarrage. Le modèle et le niveau de raisonnement sont des sélecteurs par session : ils se choisissent dans le champ de saisie.", @@ -1704,10 +1696,6 @@ "saveAntigravityConfigFailed": "Failed to save Antigravity config", "kiroSaved": "Configuration Kiro enregistrée", "saveKiroFailed": "Impossible d'enregistrer la configuration Kiro", - "traeSaved": "Configuration TRAE CLI enregistrée", - "saveTraeFailed": "Impossible d'enregistrer la configuration TRAE CLI", - "commandCodeSaved": "Configuration Command Code enregistrée", - "saveCommandCodeFailed": "Impossible d'enregistrer la configuration Command Code", "systemAgentAlreadyLatest": "{name} est déjà à jour ({version})" }, "distribution": { @@ -2479,10 +2467,7 @@ "commits": "Validations" }, "noFolderTitle": "Aucun dossier ouvert", - "noFolderHint": "Ouvrez un dossier pour voir son contenu ici", - "openFullPage": "Ouvrir en pleine page", - "back": "Retour", - "noAutomations": "Aucune automatisation pour ce projet pour l’instant" + "noFolderHint": "Ouvrez un dossier pour voir son contenu ici" }, "windowControls": { "minimizeWindow": "Minimiser la fenêtre", @@ -3381,7 +3366,6 @@ }, "messageInput": { "askAnything": "Posez n'importe quelle question...", - "promptSuggestion": "{suggestion} (Tab pour l’utiliser)", "removeAttachmentAria": "Retirer {name}", "attachFiles": "Joindre des fichiers", "addActions": "Ajouter", @@ -3411,10 +3395,6 @@ "searchModelAria": "Rechercher des modèles", "modelListLabel": "Modèles", "recommendedBadge": "Recommandé", - "recommendedValue": "Recommandé : {value}", - "searchOptions": "Rechercher des options...", - "searchOptionsAria": "Rechercher des options", - "noOptions": "Aucune option trouvée", "noModels": "Aucun modèle trouvé", "cancel": "Annuler", "send": "Envoyer", @@ -4027,16 +4007,13 @@ }, "messageNav": { "title": "Navigation des messages", + "collapse": "Réduire la navigation des messages", "loadingEarlier": "Chargement des messages précédents…", "loadEarlier": "Charger les messages précédents", + "collapsedSummary": "Messages {count}", "fileCount": "{count, plural, one {# fichier} other {# fichiers}}", - "subAgentCount": "{count, plural, one {# sous-agent} other {# sous-agents}}", - "showDiff": "Afficher les modifications de {filePath}", - "hideDiff": "Masquer les modifications de {filePath}", - "openFile": "Ouvrir {filePath}", - "collapse": "Réduire", - "currentMessage": "Message actuel", - "scrollToCurrent": "Faire défiler jusqu’au message actuel (#{ordinal})" + "remove": "Retirer", + "noDiffDataAvailable": "Aucune donnée de diff disponible pour {filePath}" }, "replyArtifacts": { "title": "Fichiers modifiés", @@ -4094,9 +4071,6 @@ "resumeDelegation": "Reprise de la tâche {task}", "resumeDelegationNoId": "Reprise d'une tâche déléguée", "promptLabel": "Instruction", - "ackRecorded": "Enregistré dans la tâche", - "ackNotRecorded": "Non enregistré dans la tâche", - "ackNotRecordedWithReason": "Non enregistré dans la tâche : {reason}", "verdict": { "success": "réussie", "needs_review": "à relire", @@ -4214,18 +4188,6 @@ "stopDeclined": "Impossible d'arrêter cette tâche : elle est peut-être déjà terminée.", "stopFailed": "Échec de l'arrêt de la tâche : {error}" }, - "remoteControl": { - "title": "Contrôle à distance activé", - "open": "Ouvrir dans le navigateur", - "copy": "Copier le lien", - "copied": "Copié", - "qr": "Code QR", - "qrLabel": "Code QR du lien de contrôle à distance de cette session", - "qrHint": "Scannez-le avec votre téléphone pour poursuivre cette session dans l'app Claude ou sur claude.ai/code.", - "noUrl": "Claude Code n'a renvoyé aucun lien pour cette session", - "turnOff": "Désactiver", - "turnOffBusy": "Disponible une fois que Claude a terminé" - }, "proposedPlan": { "title": "Plan proposé", "planning": "Planification…" @@ -7433,44 +7395,62 @@ "renameConversation": "Renommer la conversation", "openSessionFolder": "Fichiers de session", "cycleAgent": "Agent suivant", - "cycleFavoriteAgent": "Agents favoris" + "cycleFavoriteAgent": "Agents favoris", + "openBallBindings": "Assignation des gestes", + "cycleRunningSessions": "Sessions en cours", + "cycleRecentSessions": "Sessions récentes", + "deleteCurrentConversation": "Supprimer la session", + "openImportSessions": "Importer des sessions", + "togglePinConversation": "Épingler / désépingler", + "focusModeSelector": "Sélecteur de mode", + "togglePlanFullAccess": "Plan / accès complet", + "openCollaboration": "Collaboration" }, "gestures": { - "tap": "Appui", - "doubleTap": "Double appui", + "tap": "Touche", + "doubleTap": "Double touche", "longPress": "Appui long", - "left": "Balayage à gauche", - "right": "Balayage à droite", - "up": "Balayage vers le haut", - "upLeft": "Haut-gauche", - "upRight": "Haut-droite", - "upThenLeft": "Haut, puis gauche", - "upThenRight": "Haut, puis droite", - "upDown": "Haut, puis bas", - "leftThenRight": "Gauche, puis droite", - "rightThenLeft": "Droite, puis gauche", - "upLeftThenDown": "Haut-gauche, puis bas", - "upRightThenDown": "Haut-droite, puis bas", - "down": "Balayer vers le bas", - "downLeft": "Bas-gauche", - "downRight": "Bas-droite", - "downUp": "Bas, puis haut", - "upThenDownRight": "Haut, puis bas-dr.", - "downThenUpRight": "Bas, puis haut-dr.", - "rightThenUp": "Droite, puis haut", - "rightThenDown": "Droite, puis bas", - "downThenRight": "Bas, puis droite", - "downThenLeft": "Bas, puis gauche", - "downRightThenUp": "Bas-dr., puis haut", - "downLeftThenUp": "Bas-g., puis haut", - "upLeftThenRight": "Haut-g., puis droite", - "upRightThenLeft": "Haut-dr., puis gauche" + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ puis ←", + "upThenRight": "↑ puis →", + "upDown": "↑ puis ↓", + "leftThenRight": "← puis →", + "rightThenLeft": "→ puis ←", + "upLeftThenDown": "↖ puis ↓", + "upRightThenDown": "↗ puis ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ puis ↑", + "upThenDownRight": "↑ puis ↘", + "downThenUpRight": "↓ puis ↗", + "rightThenUp": "→ puis ↑", + "rightThenDown": "→ puis ↓", + "downThenRight": "↓ puis →", + "downThenLeft": "↓ puis ←", + "downRightThenUp": "↘ puis ↑", + "downLeftThenUp": "↙ puis ↑", + "upLeftThenRight": "↖ puis →", + "upRightThenLeft": "↗ puis ←" }, "favoriteAgentsEmpty": "Aucun agent disponible", "toasts": { "renameNeedsSavedSession": "Renommer ne s'applique qu'aux sessions enregistrées — envoyez d'abord un message.", "sessionFolderNeedsWorkspace": "Ouvrez d'abord un dossier d'espace de travail.", - "cycleNeedsMoreAgents": "Un seul agent disponible — rien à parcourir." - } + "cycleNeedsMoreAgents": "Un seul agent disponible — rien à parcourir.", + "noOtherRunning": "Aucune autre session en cours.", + "noOtherRecent": "Aucune autre session récente.", + "noPlanMode": "Cet agent n'a pas de mode Plan.", + "pinToggled": "État d'épinglage mis à jour.", + "noRunningSession": "Aucune session en cours.", + "noModesAvailable": "Cet agent n'expose aucun mode.", + "noSwitchTarget": "Aucun mode vers lequel basculer (mode Plan ou accès complet introuvable)." + }, + "recentSessionDepth": "Profondeur des sessions récentes", + "bindingCleared": "Non attribué" } -} +} \ No newline at end of file diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index c00a6584c7..d7e7556308 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -181,6 +181,13 @@ "sectionDescription": "会話エリア内の折りたたみ/展開、ツール呼び出し、作業状態などのアニメーションとスムーズスクロールを制御します。オフにすると即時に更新されます。スピナーは回転し続けます。", "enable": "チャットのアニメーションを有効にする" }, + "messageNav": { + "sectionTitle": "メッセージナビ", + "sectionDescription": "会話のメッセージナビの位置。右下を選ぶと、ユーザーの発話ごとに 1 行のコンパクトなカードになり、入力欄の上に表示されます。", + "positionStart": "左上(既定)", + "positionBottomEnd": "右下(コンパクト)", + "hint": "プランパネルとサブエージェントパネルは常に左上に残ります。" + }, "chatWidth": { "sectionTitle": "チャット幅", "sectionDescription": "会話列と入力欄の最大幅です。チャット領域の左右の端をドラッグして調整することもできます。", @@ -258,60 +265,69 @@ "panelTitle": "クイック操作" }, "gestures": { - "tap": "Tap", - "doubleTap": "Double tap", - "longPress": "Long press", - "left": "Swipe left", - "right": "Swipe right", - "up": "Swipe up", - "upLeft": "Up-left", - "upRight": "Up-right", - "upThenLeft": "Up, then left", - "upThenRight": "Up, then right", - "upDown": "Up, then down", - "leftThenRight": "Left, then right", - "rightThenLeft": "Right, then left", - "upLeftThenDown": "Up-left, then down", - "upRightThenDown": "Up-right, then down", - "down": "Swipe down", - "downLeft": "Down-left", - "downRight": "Down-right", - "downUp": "Down, then up", - "upThenDownRight": "Up, then down-right", - "downThenUpRight": "Down, then up-right", - "rightThenUp": "Right, then up", - "rightThenDown": "Right, then down", - "downThenRight": "Down, then right", - "downThenLeft": "Down, then left", - "downRightThenUp": "Down-right, then up", - "downLeftThenUp": "Down-left, then up", - "upLeftThenRight": "Up-left, then right", - "upRightThenLeft": "Up-right, then left" + "tap": "タップ", + "doubleTap": "ダブルタップ", + "longPress": "長押し", + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ その後 ←", + "upThenRight": "↑ その後 →", + "upDown": "↑ その後 ↓", + "leftThenRight": "← その後 →", + "rightThenLeft": "→ その後 ←", + "upLeftThenDown": "↖ その後 ↓", + "upRightThenDown": "↗ その後 ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ その後 ↑", + "upThenDownRight": "↑ その後 ↘", + "downThenUpRight": "↓ その後 ↗", + "rightThenUp": "→ その後 ↑", + "rightThenDown": "→ その後 ↓", + "downThenRight": "↓ その後 →", + "downThenLeft": "↓ その後 ←", + "downRightThenUp": "↘ その後 ↑", + "downLeftThenUp": "↙ その後 ↑", + "upLeftThenRight": "↖ その後 →", + "upRightThenLeft": "↗ その後 ←" }, "actions": { - "newConversation": "New conversation", - "toggleSidebar": "Toggle sidebar", - "toggleSearch": "Search", - "toggleTerminal": "Toggle terminal", - "nextTab": "Next tab", - "prevTab": "Previous tab", - "reopenClosedTab": "Reopen closed tab", - "openFolder": "Open folder", - "zoomIn": "Zoom in", - "zoomOut": "Zoom out", - "zoomReset": "Reset zoom", - "stopGenerating": "Stop generating", - "openSettings": "Settings", - "openTodos": "To-dos", - "openTodosNew": "New to-do", - "openAutomations": "Automations", - "openCanvas": "Canvas", - "openAgents": "Agents", - "openSystemAdmin": "System admin", - "renameConversation": "Rename conversation", - "openSessionFolder": "Session files", - "cycleAgent": "Next agent", - "cycleFavoriteAgent": "Favorite agents" + "newConversation": "新しい会話", + "toggleSidebar": "サイドバーの切り替え", + "toggleSearch": "検索", + "toggleTerminal": "ターミナルの切り替え", + "nextTab": "次のタブ", + "prevTab": "前のタブ", + "reopenClosedTab": "閉じたタブを開き直す", + "openFolder": "フォルダーを開く", + "zoomIn": "拡大", + "zoomOut": "縮小", + "zoomReset": "ズームをリセット", + "stopGenerating": "生成を停止", + "openSettings": "設定", + "openTodos": "ToDo", + "openTodosNew": "ToDo作成", + "openAutomations": "オートメーション", + "openCanvas": "キャンバス", + "openAgents": "エージェント", + "openSystemAdmin": "システム管理", + "renameConversation": "会話名を変更", + "openSessionFolder": "セッションファイル", + "cycleAgent": "次のエージェント", + "cycleFavoriteAgent": "お気に入りエージェント", + "openBallBindings": "ジェスチャー設定", + "cycleRunningSessions": "実行中のセッション", + "cycleRecentSessions": "最近のセッション", + "deleteCurrentConversation": "現在のセッションを削除", + "openImportSessions": "セッションをインポート", + "togglePinConversation": "ピン留め切り替え", + "focusModeSelector": "モード選択", + "togglePlanFullAccess": "Plan/フルアクセス切替", + "openCollaboration": "コラボレーション" } }, "SystemSettings": { @@ -1185,30 +1201,6 @@ "apiKeyHint": "KIRO_API_KEY として CLI に渡されます。空欄なら kiro-cli login でサインインしたアカウントを使います。ブラウザでサインインできない環境(サーバー、Docker)でのみ必要です。", "saveConfig": "Kiro 設定を保存" }, - "trae": { - "configManagement": "設定管理", - "configDescription": "TRAE CLI 2.0 は TRAE エンタープライズ版のフラッグシッププランのお客様のみ利用できます。Codeg はインストール済みの traecli を実行し、サインイン情報・スキル・MCP サーバーは ~/.trae に保存されます。モデル、モード、推論レベルはここにはありません。TRAE CLI が ACP で報告するため、入力欄のセレクターがセッションごとに管理します。", - "signInTitle": "サインイン", - "signInHint": "ターミナルでこのコマンドを一度実行し、エンタープライズ SSO でサインインしてください。Codeg のセッションはそのサインインを再利用します。", - "copyCommand": "コマンドをコピー", - "commandCopied": "コマンドをコピーしました", - "patLabel": "個人アクセストークン(任意)", - "patHint": "サーバーや Docker など、ブラウザでサインインできないマシン向けです。Codeg は TRAECLI_PERSONAL_ACCESS_TOKEN として TRAE CLI に渡し、セッション開始時にこのトークンでサインインします。空欄なら traecli login のサインインを使います。", - "hostLabel": "エンタープライズ API URL(任意)", - "hostHint": "組織が独自ドメインで TRAE を運用している場合のみ入力します。TRAECLI_HOST として渡されます。空欄なら TRAE の公式サービスを使います。", - "saveConfig": "TRAE CLI 設定を保存" - }, - "commandCode": { - "configManagement": "設定管理", - "configDescription": "Codeg は command-code npm パッケージの Command Code CLI を実行し、サインイン情報・スキル・MCP サーバーは ~/.commandcode に保存されます。モデル、モード、推論レベルはここにはありません。Command Code が ACP で報告するため、入力欄のセレクターがセッションごとに管理します。", - "signInTitle": "サインイン", - "signInHint": "ターミナルでこのコマンドを一度実行し、Command Code アカウントでサインインしてください。Codeg のセッションはそのサインインを再利用します。", - "copyCommand": "コマンドをコピー", - "commandCopied": "コマンドをコピーしました", - "apiKeyLabel": "API キー(任意)", - "apiKeyHint": "サーバーや Docker など、ブラウザでサインインできないマシン向けです。COMMAND_CODE_API_KEY として Command Code に渡され、command-code login のサインインより優先されます。空欄ならそのサインインを使います。", - "saveConfig": "Command Code 設定を保存" - }, "deepseek": { "configManagement": "DeepSeek Harness の設定", "configDescription": "エンドポイントと API キーは deepseek-acp が起動時に読み込む環境変数です。モデルと推論レベルはセッションごとのセレクターなので、入力欄で切り替えます。", @@ -1704,10 +1696,6 @@ "saveAntigravityConfigFailed": "Failed to save Antigravity config", "kiroSaved": "Kiro 設定を保存しました", "saveKiroFailed": "Kiro 設定の保存に失敗しました", - "traeSaved": "TRAE CLI 設定を保存しました", - "saveTraeFailed": "TRAE CLI 設定の保存に失敗しました", - "commandCodeSaved": "Command Code 設定を保存しました", - "saveCommandCodeFailed": "Command Code 設定の保存に失敗しました", "systemAgentAlreadyLatest": "{name} は最新です({version})" }, "distribution": { @@ -2479,10 +2467,7 @@ "commits": "コミット" }, "noFolderTitle": "開いているフォルダがありません", - "noFolderHint": "フォルダを開くとここに表示されます", - "openFullPage": "フルページで開く", - "back": "戻る", - "noAutomations": "このプロジェクトにはまだオートメーションがありません" + "noFolderHint": "フォルダを開くとここに表示されます" }, "windowControls": { "minimizeWindow": "ウィンドウを最小化", @@ -3381,7 +3366,6 @@ }, "messageInput": { "askAnything": "何でも質問してください...", - "promptSuggestion": "{suggestion}(Tab で入力)", "removeAttachmentAria": "{name} を削除", "attachFiles": "ファイルを添付", "addActions": "追加", @@ -3411,10 +3395,6 @@ "searchModelAria": "モデルを検索", "modelListLabel": "モデル", "recommendedBadge": "推奨", - "recommendedValue": "推奨: {value}", - "searchOptions": "オプションを検索...", - "searchOptionsAria": "オプションを検索", - "noOptions": "オプションが見つかりません", "noModels": "モデルが見つかりません", "cancel": "キャンセル", "send": "送信", @@ -4027,16 +4007,13 @@ }, "messageNav": { "title": "メッセージナビ", + "collapse": "メッセージナビを閉じる", "loadingEarlier": "以前のメッセージを読み込み中…", "loadEarlier": "以前のメッセージを読み込む", + "collapsedSummary": "メッセージ {count}", "fileCount": "{count, plural, one {# 個のファイル} other {# 個のファイル}}", - "subAgentCount": "{count, plural, one {# 個のサブエージェント} other {# 個のサブエージェント}}", - "showDiff": "{filePath} の変更を表示", - "hideDiff": "{filePath} の変更を隠す", - "openFile": "{filePath} を開く", - "collapse": "折りたたむ", - "currentMessage": "現在のメッセージ", - "scrollToCurrent": "現在のメッセージ(#{ordinal})へスクロール" + "remove": "削除", + "noDiffDataAvailable": "{filePath} の差分データがありません" }, "replyArtifacts": { "title": "変更されたファイル", @@ -4094,9 +4071,6 @@ "resumeDelegation": "タスク {task} を再開中", "resumeDelegationNoId": "委任タスクを再開中", "promptLabel": "プロンプト", - "ackRecorded": "作業タスクに記録しました", - "ackNotRecorded": "作業タスクに記録されていません", - "ackNotRecordedWithReason": "作業タスクに記録されていません: {reason}", "verdict": { "success": "成功", "needs_review": "要確認", @@ -4214,18 +4188,6 @@ "stopDeclined": "このタスクは停止できませんでした。すでに終了している可能性があります。", "stopFailed": "タスクの停止に失敗しました: {error}" }, - "remoteControl": { - "title": "リモートコントロール有効", - "open": "ブラウザで開く", - "copy": "リンクをコピー", - "copied": "コピーしました", - "qr": "QR コード", - "qrLabel": "このセッションのリモートコントロール用リンクの QR コード", - "qrHint": "スマートフォンでスキャンすると、Claude アプリまたは claude.ai/code でこのセッションを続けられます。", - "noUrl": "Claude Code はこのセッションのリンクを返しませんでした", - "turnOff": "オフにする", - "turnOffBusy": "Claude の作業が終わると使えます" - }, "proposedPlan": { "title": "提案された計画", "planning": "計画中…" @@ -7433,44 +7395,62 @@ "renameConversation": "会話名を変更", "openSessionFolder": "セッションファイル", "cycleAgent": "次のエージェント", - "cycleFavoriteAgent": "お気に入りエージェント" + "cycleFavoriteAgent": "お気に入りエージェント", + "openBallBindings": "ジェスチャー設定", + "cycleRunningSessions": "実行中のセッション", + "cycleRecentSessions": "最近のセッション", + "deleteCurrentConversation": "現在のセッションを削除", + "openImportSessions": "セッションをインポート", + "togglePinConversation": "ピン留め切り替え", + "focusModeSelector": "モード選択", + "togglePlanFullAccess": "Plan/フルアクセス切替", + "openCollaboration": "コラボレーション" }, "gestures": { "tap": "タップ", "doubleTap": "ダブルタップ", "longPress": "長押し", - "left": "左スワイプ", - "right": "右スワイプ", - "up": "上スワイプ", - "upLeft": "左上スワイプ", - "upRight": "右上スワイプ", - "upThenLeft": "上→左", - "upThenRight": "上→右", - "upDown": "上→下", - "leftThenRight": "左→右", - "rightThenLeft": "右→左", - "upLeftThenDown": "左上→下", - "upRightThenDown": "右上→下", - "down": "下スワイプ", - "downLeft": "左下", - "downRight": "右下", - "downUp": "下から上", - "upThenDownRight": "上から右下", - "downThenUpRight": "下から右上", - "rightThenUp": "右から上", - "rightThenDown": "右から下", - "downThenRight": "下から右", - "downThenLeft": "下から左", - "downRightThenUp": "右下から上", - "downLeftThenUp": "左下から上", - "upLeftThenRight": "左上から右", - "upRightThenLeft": "右上から左" + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ その後 ←", + "upThenRight": "↑ その後 →", + "upDown": "↑ その後 ↓", + "leftThenRight": "← その後 →", + "rightThenLeft": "→ その後 ←", + "upLeftThenDown": "↖ その後 ↓", + "upRightThenDown": "↗ その後 ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ その後 ↑", + "upThenDownRight": "↑ その後 ↘", + "downThenUpRight": "↓ その後 ↗", + "rightThenUp": "→ その後 ↑", + "rightThenDown": "→ その後 ↓", + "downThenRight": "↓ その後 →", + "downThenLeft": "↓ その後 ←", + "downRightThenUp": "↘ その後 ↑", + "downLeftThenUp": "↙ その後 ↑", + "upLeftThenRight": "↖ その後 →", + "upRightThenLeft": "↗ その後 ←" }, "favoriteAgentsEmpty": "利用可能なエージェントがありません", "toasts": { "renameNeedsSavedSession": "名前の変更は保存済みセッションのみ有効です。まずメッセージを送信してください。", "sessionFolderNeedsWorkspace": "先にワークスペースフォルダを開いてください。", - "cycleNeedsMoreAgents": "利用可能なエージェントが1つだけのため、切り替えできません。" - } + "cycleNeedsMoreAgents": "利用可能なエージェントが1つだけのため、切り替えできません。", + "noOtherRunning": "切り替え先の実行中セッションがありません", + "noOtherRecent": "切り替え先の最近のセッションがありません", + "noPlanMode": "このエージェントにPlanモードはありません", + "pinToggled": "ピン留め状態を更新しました", + "noRunningSession": "実行中のセッションはまだありません", + "noModesAvailable": "このエージェントには利用可能なモードがありません。", + "noSwitchTarget": "切り替え先のモードがありません(Plan または完全アクセスモードが見つかりません)。" + }, + "recentSessionDepth": "最近セッション切替数", + "bindingCleared": "未割り当て" } -} +} \ No newline at end of file diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 8e375dbd77..6819fb8e8d 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -181,6 +181,13 @@ "sectionDescription": "대화 영역의 접기/펼치기, 도구 호출, 작업 상태 등의 애니메이션과 부드러운 스크롤을 제어합니다. 끄면 즉시 갱신됩니다. 스피너는 계속 회전합니다.", "enable": "채팅 애니메이션 사용" }, + "messageNav": { + "sectionTitle": "메시지 탐색기", + "sectionDescription": "대화 메시지 탐색기의 위치입니다. 오른쪽 아래를 선택하면 사용자 발화마다 한 줄인 컴팩트 카드로 입력창 위에 표시됩니다.", + "positionStart": "왼쪽 위(기본)", + "positionBottomEnd": "오른쪽 아래(컴팩트)", + "hint": "계획 및 하위 에이전트 패널은 항상 왼쪽 위에 남습니다." + }, "chatWidth": { "sectionTitle": "채팅 너비", "sectionDescription": "대화 영역과 입력창의 최대 너비입니다. 채팅 영역의 좌우 가장자리를 드래그해서 조정할 수도 있습니다.", @@ -258,60 +265,69 @@ "panelTitle": "빠른 작업" }, "gestures": { - "tap": "Tap", - "doubleTap": "Double tap", - "longPress": "Long press", - "left": "Swipe left", - "right": "Swipe right", - "up": "Swipe up", - "upLeft": "Up-left", - "upRight": "Up-right", - "upThenLeft": "Up, then left", - "upThenRight": "Up, then right", - "upDown": "Up, then down", - "leftThenRight": "Left, then right", - "rightThenLeft": "Right, then left", - "upLeftThenDown": "Up-left, then down", - "upRightThenDown": "Up-right, then down", - "down": "Swipe down", - "downLeft": "Down-left", - "downRight": "Down-right", - "downUp": "Down, then up", - "upThenDownRight": "Up, then down-right", - "downThenUpRight": "Down, then up-right", - "rightThenUp": "Right, then up", - "rightThenDown": "Right, then down", - "downThenRight": "Down, then right", - "downThenLeft": "Down, then left", - "downRightThenUp": "Down-right, then up", - "downLeftThenUp": "Down-left, then up", - "upLeftThenRight": "Up-left, then right", - "upRightThenLeft": "Up-right, then left" + "tap": "탭", + "doubleTap": "더블 탭", + "longPress": "길게 누름", + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ 후 ←", + "upThenRight": "↑ 후 →", + "upDown": "↑ 후 ↓", + "leftThenRight": "← 후 →", + "rightThenLeft": "→ 후 ←", + "upLeftThenDown": "↖ 후 ↓", + "upRightThenDown": "↗ 후 ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ 후 ↑", + "upThenDownRight": "↑ 후 ↘", + "downThenUpRight": "↓ 후 ↗", + "rightThenUp": "→ 후 ↑", + "rightThenDown": "→ 후 ↓", + "downThenRight": "↓ 후 →", + "downThenLeft": "↓ 후 ←", + "downRightThenUp": "↘ 후 ↑", + "downLeftThenUp": "↙ 후 ↑", + "upLeftThenRight": "↖ 후 →", + "upRightThenLeft": "↗ 후 ←" }, "actions": { - "newConversation": "New conversation", - "toggleSidebar": "Toggle sidebar", - "toggleSearch": "Search", - "toggleTerminal": "Toggle terminal", - "nextTab": "Next tab", - "prevTab": "Previous tab", - "reopenClosedTab": "Reopen closed tab", - "openFolder": "Open folder", - "zoomIn": "Zoom in", - "zoomOut": "Zoom out", - "zoomReset": "Reset zoom", - "stopGenerating": "Stop generating", - "openSettings": "Settings", - "openTodos": "To-dos", - "openTodosNew": "New to-do", - "openAutomations": "Automations", - "openCanvas": "Canvas", - "openAgents": "Agents", - "openSystemAdmin": "System admin", - "renameConversation": "Rename conversation", - "openSessionFolder": "Session files", - "cycleAgent": "Next agent", - "cycleFavoriteAgent": "Favorite agents" + "newConversation": "새 대화", + "toggleSidebar": "사이드바 전환", + "toggleSearch": "검색", + "toggleTerminal": "터미널 전환", + "nextTab": "다음 탭", + "prevTab": "이전 탭", + "reopenClosedTab": "닫은 탭 다시 열기", + "openFolder": "폴더 열기", + "zoomIn": "확대", + "zoomOut": "축소", + "zoomReset": "확대/축소 초기화", + "stopGenerating": "생성 중지", + "openSettings": "설정", + "openTodos": "할 일", + "openTodosNew": "새 할 일", + "openAutomations": "자동화", + "openCanvas": "캔버스", + "openAgents": "에이전트", + "openSystemAdmin": "시스템 관리", + "renameConversation": "대화 이름 바꾸기", + "openSessionFolder": "세션 파일", + "cycleAgent": "다음 에이전트", + "cycleFavoriteAgent": "즐겨쓰는 에이전트", + "openBallBindings": "제스처 바인딩", + "cycleRunningSessions": "실행 중인 세션", + "cycleRecentSessions": "최근 세션", + "deleteCurrentConversation": "현재 세션 삭제", + "openImportSessions": "セッション 가져오기", + "togglePinConversation": "고정/고정 해제", + "focusModeSelector": "모드 선택", + "togglePlanFullAccess": "Plan/전체 권한 전환", + "openCollaboration": "협업" } }, "SystemSettings": { @@ -1185,30 +1201,6 @@ "apiKeyHint": "KIRO_API_KEY로 CLI에 전달됩니다. 비워 두면 kiro-cli login으로 로그인한 계정을 사용합니다. 브라우저 로그인이 불가능한 환경(서버, Docker)에서만 필요합니다.", "saveConfig": "Kiro 구성 저장" }, - "trae": { - "configManagement": "구성 관리", - "configDescription": "TRAE CLI 2.0은 TRAE 엔터프라이즈 플래그십 요금제 고객만 사용할 수 있습니다. Codeg는 설치된 traecli를 실행하며, 로그인 정보·스킬·MCP 서버는 ~/.trae에 저장됩니다. 모델, 모드, 추론 강도는 여기에 없습니다. TRAE CLI가 ACP로 보고하므로 입력창의 선택기가 세션별로 관리합니다.", - "signInTitle": "로그인", - "signInHint": "터미널에서 이 명령을 한 번 실행해 엔터프라이즈 SSO로 로그인하세요. Codeg의 세션은 그 로그인을 재사용합니다.", - "copyCommand": "명령 복사", - "commandCopied": "명령을 복사했습니다", - "patLabel": "개인 액세스 토큰(선택)", - "patHint": "서버나 Docker처럼 브라우저 로그인이 불가능한 머신용입니다. Codeg는 TRAECLI_PERSONAL_ACCESS_TOKEN으로 TRAE CLI에 전달하고 세션을 시작할 때마다 이 토큰으로 로그인합니다. 비워 두면 traecli login의 로그인을 사용합니다.", - "hostLabel": "엔터프라이즈 API URL(선택)", - "hostHint": "조직이 자체 도메인에서 TRAE를 운영하는 경우에만 입력하세요. TRAECLI_HOST로 전달되며, 비워 두면 TRAE 공식 서비스를 사용합니다.", - "saveConfig": "TRAE CLI 구성 저장" - }, - "commandCode": { - "configManagement": "구성 관리", - "configDescription": "Codeg는 command-code npm 패키지의 Command Code CLI를 실행하며, 로그인 정보·스킬·MCP 서버는 ~/.commandcode에 저장됩니다. 모델, 모드, 추론 강도는 여기에 없습니다. Command Code가 ACP로 보고하므로 입력창의 선택기가 세션별로 관리합니다.", - "signInTitle": "로그인", - "signInHint": "터미널에서 이 명령을 한 번 실행해 Command Code 계정으로 로그인하세요. Codeg의 세션은 그 로그인을 재사용합니다.", - "copyCommand": "명령 복사", - "commandCopied": "명령을 복사했습니다", - "apiKeyLabel": "API 키(선택)", - "apiKeyHint": "서버나 Docker처럼 브라우저 로그인이 불가능한 머신용입니다. COMMAND_CODE_API_KEY로 Command Code에 전달되며 command-code login의 로그인보다 우선합니다. 비워 두면 그 로그인을 사용합니다.", - "saveConfig": "Command Code 구성 저장" - }, "deepseek": { "configManagement": "DeepSeek Harness 구성", "configDescription": "엔드포인트와 API 키는 deepseek-acp가 시작할 때 읽는 환경 변수입니다. 모델과 추론 강도는 세션별 선택기이므로 입력창에서 바꿉니다.", @@ -1704,10 +1696,6 @@ "saveAntigravityConfigFailed": "Failed to save Antigravity config", "kiroSaved": "Kiro 구성을 저장했습니다", "saveKiroFailed": "Kiro 구성을 저장하지 못했습니다", - "traeSaved": "TRAE CLI 구성을 저장했습니다", - "saveTraeFailed": "TRAE CLI 구성을 저장하지 못했습니다", - "commandCodeSaved": "Command Code 구성을 저장했습니다", - "saveCommandCodeFailed": "Command Code 구성을 저장하지 못했습니다", "systemAgentAlreadyLatest": "{name}은(는) 이미 최신 버전입니다({version})" }, "distribution": { @@ -2479,10 +2467,7 @@ "commits": "커밋" }, "noFolderTitle": "열린 폴더 없음", - "noFolderHint": "폴더를 열면 여기에 표시됩니다", - "openFullPage": "전체 페이지에서 열기", - "back": "뒤로", - "noAutomations": "이 프로젝트에는 아직 자동화가 없습니다" + "noFolderHint": "폴더를 열면 여기에 표시됩니다" }, "windowControls": { "minimizeWindow": "창 최소화", @@ -3381,7 +3366,6 @@ }, "messageInput": { "askAnything": "무엇이든 물어보세요...", - "promptSuggestion": "{suggestion} (Tab 키로 입력)", "removeAttachmentAria": "{name} 제거", "attachFiles": "파일 첨부", "addActions": "추가", @@ -3411,10 +3395,6 @@ "searchModelAria": "모델 검색", "modelListLabel": "모델", "recommendedBadge": "권장", - "recommendedValue": "권장: {value}", - "searchOptions": "옵션 검색...", - "searchOptionsAria": "옵션 검색", - "noOptions": "옵션을 찾을 수 없습니다", "noModels": "모델을 찾을 수 없습니다", "cancel": "취소", "send": "보내기", @@ -4027,16 +4007,13 @@ }, "messageNav": { "title": "메시지 탐색", + "collapse": "메시지 탐색 접기", "loadingEarlier": "이전 메시지를 불러오는 중…", "loadEarlier": "이전 메시지 불러오기", + "collapsedSummary": "메시지 {count}", "fileCount": "{count, plural, one {#개 파일} other {#개 파일}}", - "subAgentCount": "{count, plural, one {서브 에이전트 #개} other {서브 에이전트 #개}}", - "showDiff": "{filePath}의 변경 사항 보기", - "hideDiff": "{filePath}의 변경 사항 숨기기", - "openFile": "{filePath} 열기", - "collapse": "접기", - "currentMessage": "현재 메시지", - "scrollToCurrent": "현재 메시지(#{ordinal})로 스크롤" + "remove": "제거", + "noDiffDataAvailable": "{filePath}에 대한 diff 데이터가 없습니다" }, "replyArtifacts": { "title": "변경된 파일", @@ -4094,9 +4071,6 @@ "resumeDelegation": "작업 {task} 재개 중", "resumeDelegationNoId": "위임 작업 재개 중", "promptLabel": "프롬프트", - "ackRecorded": "작업 항목에 기록됨", - "ackNotRecorded": "작업 항목에 기록되지 않음", - "ackNotRecordedWithReason": "작업 항목에 기록되지 않음: {reason}", "verdict": { "success": "성공", "needs_review": "검토 필요", @@ -4214,18 +4188,6 @@ "stopDeclined": "해당 작업을 중지할 수 없습니다. 이미 완료되었을 수 있습니다.", "stopFailed": "작업 중지에 실패했습니다: {error}" }, - "remoteControl": { - "title": "원격 제어 켜짐", - "open": "브라우저에서 열기", - "copy": "링크 복사", - "copied": "복사됨", - "qr": "QR 코드", - "qrLabel": "이 세션의 원격 제어 링크 QR 코드", - "qrHint": "휴대폰으로 스캔하면 Claude 앱이나 claude.ai/code에서 이 세션을 이어갈 수 있습니다.", - "noUrl": "Claude Code가 이 세션의 링크를 반환하지 않았습니다", - "turnOff": "끄기", - "turnOffBusy": "Claude가 작업을 마치면 사용할 수 있습니다" - }, "proposedPlan": { "title": "제안된 계획", "planning": "계획 중…" @@ -7433,44 +7395,62 @@ "renameConversation": "대화 이름 바꾸기", "openSessionFolder": "세션 파일", "cycleAgent": "다음 에이전트", - "cycleFavoriteAgent": "즐겨쓰는 에이전트" + "cycleFavoriteAgent": "즐겨쓰는 에이전트", + "openBallBindings": "제스처 바인딩", + "cycleRunningSessions": "실행 중인 세션", + "cycleRecentSessions": "최근 세션", + "deleteCurrentConversation": "현재 세션 삭제", + "openImportSessions": "セッション 가져오기", + "togglePinConversation": "고정/고정 해제", + "focusModeSelector": "모드 선택", + "togglePlanFullAccess": "Plan/전체 권한 전환", + "openCollaboration": "협업" }, "gestures": { "tap": "탭", "doubleTap": "더블 탭", - "longPress": "길게 누르기", - "left": "왼쪽 스와이프", - "right": "오른쪽 스와이프", - "up": "위로 스와이프", - "upLeft": "왼쪽 위", - "upRight": "오른쪽 위", - "upThenLeft": "위 → 왼쪽", - "upThenRight": "위 → 오른쪽", - "upDown": "위 → 아래", - "leftThenRight": "왼쪽 → 오른쪽", - "rightThenLeft": "오른쪽 → 왼쪽", - "upLeftThenDown": "왼쪽 위 → 아래", - "upRightThenDown": "오른쪽 위 → 아래", - "down": "아래로", - "downLeft": "왼쪽 아래", - "downRight": "오른쪽 아래", - "downUp": "아래→위", - "upThenDownRight": "위→오른아래", - "downThenUpRight": "아래→오른위", - "rightThenUp": "오른→위", - "rightThenDown": "오른→아래", - "downThenRight": "아래→오른", - "downThenLeft": "아래→왼", - "downRightThenUp": "오른아래→위", - "downLeftThenUp": "왼아래→위", - "upLeftThenRight": "왼위→오른", - "upRightThenLeft": "오른위→왼" + "longPress": "길게 누름", + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ 후 ←", + "upThenRight": "↑ 후 →", + "upDown": "↑ 후 ↓", + "leftThenRight": "← 후 →", + "rightThenLeft": "→ 후 ←", + "upLeftThenDown": "↖ 후 ↓", + "upRightThenDown": "↗ 후 ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ 후 ↑", + "upThenDownRight": "↑ 후 ↘", + "downThenUpRight": "↓ 후 ↗", + "rightThenUp": "→ 후 ↑", + "rightThenDown": "→ 후 ↓", + "downThenRight": "↓ 후 →", + "downThenLeft": "↓ 후 ←", + "downRightThenUp": "↘ 후 ↑", + "downLeftThenUp": "↙ 후 ↑", + "upLeftThenRight": "↖ 후 →", + "upRightThenLeft": "↗ 후 ←" }, "favoriteAgentsEmpty": "사용 가능한 에이전트가 없습니다", "toasts": { "renameNeedsSavedSession": "이름 바꾸기는 저장된 세션에만 적용됩니다. 먼저 메시지를 보내세요.", "sessionFolderNeedsWorkspace": "먼저 작업 공간 폴더를 여세요.", - "cycleNeedsMoreAgents": "사용 가능한 에이전트가 하나뿐이어서 전환할 수 없습니다." - } + "cycleNeedsMoreAgents": "사용 가능한 에이전트가 하나뿐이어서 전환할 수 없습니다.", + "noOtherRunning": "전환할 다른 실행 세션이 없습니다", + "noOtherRecent": "전환할 다른 최근 세션이 없습니다", + "noPlanMode": "이 에이전트에 Plan 모드가 없습니다", + "pinToggled": "고정 상태가 변경되었습니다", + "noRunningSession": "아직 실행 중인 세션이 없습니다", + "noModesAvailable": "이 에이전트에는 사용 가능한 모드가 없습니다.", + "noSwitchTarget": "전환할 모드가 없습니다(Plan 또는 전체 접근 모드를 찾을 수 없음)." + }, + "recentSessionDepth": "최근 세션 전환 깊이", + "bindingCleared": "미지정" } -} +} \ No newline at end of file diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index acc64d9b91..9e1bc0c918 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -181,6 +181,13 @@ "sectionDescription": "Animações de recolher/expandir, chamadas de ferramentas e estado de trabalho na área de conversa, além da rolagem suave. Desative para atualizações instantâneas. Os indicadores de carregamento continuam girando.", "enable": "Ativar as animações do chat" }, + "messageNav": { + "sectionTitle": "Navegador de mensagens", + "sectionDescription": "Posição do navegador de mensagens. No canto inferior direito vira um cartão compacto com uma linha por mensagem do usuário, acima do campo de entrada.", + "positionStart": "Canto superior esquerdo (padrão)", + "positionBottomEnd": "Canto inferior direito (compacto)", + "hint": "Os painéis de plano e de subagentes permanecem no canto superior esquerdo." + }, "chatWidth": { "sectionTitle": "Largura do chat", "sectionDescription": "Largura máxima da coluna de conversa e da caixa de entrada. Você também pode arrastar as bordas do chat para ajustá-la.", @@ -258,60 +265,69 @@ "panelTitle": "Ações rápidas" }, "gestures": { - "tap": "Tap", - "doubleTap": "Double tap", - "longPress": "Long press", - "left": "Swipe left", - "right": "Swipe right", - "up": "Swipe up", - "upLeft": "Up-left", - "upRight": "Up-right", - "upThenLeft": "Up, then left", - "upThenRight": "Up, then right", - "upDown": "Up, then down", - "leftThenRight": "Left, then right", - "rightThenLeft": "Right, then left", - "upLeftThenDown": "Up-left, then down", - "upRightThenDown": "Up-right, then down", - "down": "Swipe down", - "downLeft": "Down-left", - "downRight": "Down-right", - "downUp": "Down, then up", - "upThenDownRight": "Up, then down-right", - "downThenUpRight": "Down, then up-right", - "rightThenUp": "Right, then up", - "rightThenDown": "Right, then down", - "downThenRight": "Down, then right", - "downThenLeft": "Down, then left", - "downRightThenUp": "Down-right, then up", - "downLeftThenUp": "Down-left, then up", - "upLeftThenRight": "Up-left, then right", - "upRightThenLeft": "Up-right, then left" + "tap": "Toque", + "doubleTap": "Toque duplo", + "longPress": "Pressão longa", + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ depois ←", + "upThenRight": "↑ depois →", + "upDown": "↑ depois ↓", + "leftThenRight": "← depois →", + "rightThenLeft": "→ depois ←", + "upLeftThenDown": "↖ depois ↓", + "upRightThenDown": "↗ depois ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ depois ↑", + "upThenDownRight": "↑ depois ↘", + "downThenUpRight": "↓ depois ↗", + "rightThenUp": "→ depois ↑", + "rightThenDown": "→ depois ↓", + "downThenRight": "↓ depois →", + "downThenLeft": "↓ depois ←", + "downRightThenUp": "↘ depois ↑", + "downLeftThenUp": "↙ depois ↑", + "upLeftThenRight": "↖ depois →", + "upRightThenLeft": "↗ depois ←" }, "actions": { - "newConversation": "New conversation", - "toggleSidebar": "Toggle sidebar", - "toggleSearch": "Search", - "toggleTerminal": "Toggle terminal", - "nextTab": "Next tab", - "prevTab": "Previous tab", - "reopenClosedTab": "Reopen closed tab", - "openFolder": "Open folder", - "zoomIn": "Zoom in", - "zoomOut": "Zoom out", - "zoomReset": "Reset zoom", - "stopGenerating": "Stop generating", - "openSettings": "Settings", - "openTodos": "To-dos", - "openTodosNew": "New to-do", - "openAutomations": "Automations", + "newConversation": "Nova conversa", + "toggleSidebar": "Alternar barra lateral", + "toggleSearch": "Pesquisar", + "toggleTerminal": "Alternar terminal", + "nextTab": "Próxima aba", + "prevTab": "Aba anterior", + "reopenClosedTab": "Reabrir aba fechada", + "openFolder": "Abrir pasta", + "zoomIn": "Aumentar zoom", + "zoomOut": "Diminuir zoom", + "zoomReset": "Redefinir zoom", + "stopGenerating": "Parar geração", + "openSettings": "Configurações", + "openTodos": "Tarefas", + "openTodosNew": "Nova tarefa", + "openAutomations": "Automações", "openCanvas": "Canvas", - "openAgents": "Agents", - "openSystemAdmin": "System admin", - "renameConversation": "Rename conversation", - "openSessionFolder": "Session files", - "cycleAgent": "Next agent", - "cycleFavoriteAgent": "Favorite agents" + "openAgents": "Agentes", + "openSystemAdmin": "Administração", + "renameConversation": "Renomear conversa", + "openSessionFolder": "Arquivos da sessão", + "cycleAgent": "Próximo agente", + "cycleFavoriteAgent": "Agentes favoritos", + "openBallBindings": "Atalhos de gestos", + "cycleRunningSessions": "Sessões em execução", + "cycleRecentSessions": "Sessões recentes", + "deleteCurrentConversation": "Excluir sessão atual", + "openImportSessions": "Importar sessões", + "togglePinConversation": "Fixar / desafixar", + "focusModeSelector": "Seletor de modo", + "togglePlanFullAccess": "Plano / acesso total", + "openCollaboration": "Colaboração" } }, "SystemSettings": { @@ -1185,30 +1201,6 @@ "apiKeyHint": "Passada à CLI como KIRO_API_KEY. Deixe vazia para usar a conta conectada via kiro-cli login; só é necessária onde não há login pelo navegador (servidores, Docker).", "saveConfig": "Salvar configuração do Kiro" }, - "trae": { - "configManagement": "Configuração", - "configDescription": "O TRAE CLI 2.0 está disponível apenas para clientes do plano principal do TRAE Enterprise. O Codeg executa o traecli que você instalou, que guarda o login, as skills e os servidores MCP em ~/.trae. Modelo, modo e esforço de raciocínio não ficam aqui: o TRAE CLI os informa via ACP, então os seletores do compositor os gerenciam em cada sessão.", - "signInTitle": "Entrar", - "signInHint": "Execute isto uma vez em um terminal para entrar com o SSO da sua empresa. As sessões do Codeg reutilizam esse login.", - "copyCommand": "Copiar comando", - "commandCopied": "Comando copiado", - "patLabel": "Token de acesso pessoal (opcional)", - "patHint": "Para uma máquina em que não é possível entrar pelo navegador, como um servidor ou uma instalação Docker. O Codeg o passa ao TRAE CLI como TRAECLI_PERSONAL_ACCESS_TOKEN e entra com ele sempre que uma sessão começa. Deixe vazio para usar o login do traecli login.", - "hostLabel": "URL da API corporativa (opcional)", - "hostHint": "Somente se a sua organização executa o TRAE em um domínio próprio. É passada como TRAECLI_HOST; deixe vazia para usar o serviço do TRAE.", - "saveConfig": "Salvar configuração do TRAE CLI" - }, - "commandCode": { - "configManagement": "Configuração", - "configDescription": "O Codeg executa a CLI do Command Code do pacote npm command-code, que guarda o login, as skills e os servidores MCP em ~/.commandcode. Modelo, modo e esforço de raciocínio não ficam aqui: o Command Code os informa via ACP, então os seletores do compositor os gerenciam em cada sessão.", - "signInTitle": "Entrar", - "signInHint": "Execute isto uma vez em um terminal para entrar com sua conta do Command Code. As sessões do Codeg reutilizam esse login.", - "copyCommand": "Copiar comando", - "commandCopied": "Comando copiado", - "apiKeyLabel": "Chave de API (opcional)", - "apiKeyHint": "Para uma máquina em que não é possível entrar pelo navegador, como um servidor ou uma instalação Docker. Passada ao Command Code como COMMAND_CODE_API_KEY, que tem prioridade sobre o login do command-code login. Deixe vazia para usar esse login.", - "saveConfig": "Salvar configuração do Command Code" - }, "deepseek": { "configManagement": "Configuração do DeepSeek Harness", "configDescription": "O endpoint e a chave são variáveis de ambiente que o deepseek-acp lê ao iniciar. O modelo e o nível de raciocínio são seletores por sessão — escolha-os no campo de mensagem.", @@ -1704,10 +1696,6 @@ "saveAntigravityConfigFailed": "Failed to save Antigravity config", "kiroSaved": "Configuração do Kiro salva", "saveKiroFailed": "Falha ao salvar a configuração do Kiro", - "traeSaved": "Configuração do TRAE CLI salva", - "saveTraeFailed": "Falha ao salvar a configuração do TRAE CLI", - "commandCodeSaved": "Configuração do Command Code salva", - "saveCommandCodeFailed": "Falha ao salvar a configuração do Command Code", "systemAgentAlreadyLatest": "{name} já está atualizado ({version})" }, "distribution": { @@ -2479,10 +2467,7 @@ "commits": "Confirmações" }, "noFolderTitle": "Nenhuma pasta aberta", - "noFolderHint": "Abra uma pasta para ver seu conteúdo aqui", - "openFullPage": "Abrir em página inteira", - "back": "Voltar", - "noAutomations": "Este projeto ainda não tem automações" + "noFolderHint": "Abra uma pasta para ver seu conteúdo aqui" }, "windowControls": { "minimizeWindow": "Minimizar janela", @@ -3381,7 +3366,6 @@ }, "messageInput": { "askAnything": "Pergunte qualquer coisa...", - "promptSuggestion": "{suggestion} (Tab para usar)", "removeAttachmentAria": "Remover {name}", "attachFiles": "Anexar arquivos", "addActions": "Adicionar", @@ -3411,10 +3395,6 @@ "searchModelAria": "Buscar modelos", "modelListLabel": "Modelos", "recommendedBadge": "Recomendado", - "recommendedValue": "Recomendado: {value}", - "searchOptions": "Buscar opções...", - "searchOptionsAria": "Buscar opções", - "noOptions": "Nenhuma opção encontrada", "noModels": "Nenhum modelo encontrado", "cancel": "Cancelar", "send": "Enviar", @@ -4027,16 +4007,13 @@ }, "messageNav": { "title": "Navegação de mensagens", + "collapse": "Recolher navegação de mensagens", "loadingEarlier": "Carregando mensagens anteriores…", "loadEarlier": "Carregar mensagens anteriores", + "collapsedSummary": "Mensagens {count}", "fileCount": "{count, plural, one {# arquivo} other {# arquivos}}", - "subAgentCount": "{count, plural, one {# subagente} other {# subagentes}}", - "showDiff": "Mostrar as alterações de {filePath}", - "hideDiff": "Ocultar as alterações de {filePath}", - "openFile": "Abrir {filePath}", - "collapse": "Recolher", - "currentMessage": "Mensagem atual", - "scrollToCurrent": "Rolar até a mensagem atual (#{ordinal})" + "remove": "Remover", + "noDiffDataAvailable": "Nenhum dado de diff disponível para {filePath}" }, "replyArtifacts": { "title": "Arquivos alterados", @@ -4094,9 +4071,6 @@ "resumeDelegation": "Retomando a tarefa {task}", "resumeDelegationNoId": "Retomando uma tarefa delegada", "promptLabel": "Instrução", - "ackRecorded": "Registrado na tarefa de trabalho", - "ackNotRecorded": "Não registrado na tarefa de trabalho", - "ackNotRecordedWithReason": "Não registrado na tarefa de trabalho: {reason}", "verdict": { "success": "sucesso", "needs_review": "requer revisão", @@ -4214,18 +4188,6 @@ "stopDeclined": "Não foi possível parar essa tarefa — ela pode já ter terminado.", "stopFailed": "Falha ao parar a tarefa: {error}" }, - "remoteControl": { - "title": "Controle remoto ativado", - "open": "Abrir no navegador", - "copy": "Copiar link", - "copied": "Copiado", - "qr": "Código QR", - "qrLabel": "Código QR do link de controle remoto desta sessão", - "qrHint": "Escaneie com o celular para continuar esta sessão no app do Claude ou em claude.ai/code.", - "noUrl": "O Claude Code não retornou nenhum link para esta sessão", - "turnOff": "Desativar", - "turnOffBusy": "Disponível quando o Claude terminar" - }, "proposedPlan": { "title": "Plano proposto", "planning": "A planear…" @@ -7433,44 +7395,62 @@ "renameConversation": "Renomear conversa", "openSessionFolder": "Arquivos da sessão", "cycleAgent": "Próximo agente", - "cycleFavoriteAgent": "Agentes favoritos" + "cycleFavoriteAgent": "Agentes favoritos", + "openBallBindings": "Atalhos de gestos", + "cycleRunningSessions": "Sessões em execução", + "cycleRecentSessions": "Sessões recentes", + "deleteCurrentConversation": "Excluir sessão atual", + "openImportSessions": "Importar sessões", + "togglePinConversation": "Fixar / desafixar", + "focusModeSelector": "Seletor de modo", + "togglePlanFullAccess": "Plano / acesso total", + "openCollaboration": "Colaboração" }, "gestures": { "tap": "Toque", "doubleTap": "Toque duplo", - "longPress": "Toque longo", - "left": "Deslizar para a esquerda", - "right": "Deslizar para a direita", - "up": "Deslizar para cima", - "upLeft": "Cima-esquerda", - "upRight": "Cima-direita", - "upThenLeft": "Cima, depois esquerda", - "upThenRight": "Cima, depois direita", - "upDown": "Cima, depois baixo", - "leftThenRight": "Esquerda, depois direita", - "rightThenLeft": "Direita, depois esquerda", - "upLeftThenDown": "Cima-esquerda, depois baixo", - "upRightThenDown": "Cima-direita, depois baixo", - "down": "Deslizar para baixo", - "downLeft": "Baixo-esq.", - "downRight": "Baixo-dir.", - "downUp": "Baixo, depois cima", - "upThenDownRight": "Cima, depois baixo-dir.", - "downThenUpRight": "Baixo, depois cima-dir.", - "rightThenUp": "Direita, depois cima", - "rightThenDown": "Direita, depois baixo", - "downThenRight": "Baixo, depois direita", - "downThenLeft": "Baixo, depois esquerda", - "downRightThenUp": "Baixo-dir., depois cima", - "downLeftThenUp": "Baixo-esq., depois cima", - "upLeftThenRight": "Cima-esq., depois direita", - "upRightThenLeft": "Cima-dir., depois esquerda" + "longPress": "Pressão longa", + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ depois ←", + "upThenRight": "↑ depois →", + "upDown": "↑ depois ↓", + "leftThenRight": "← depois →", + "rightThenLeft": "→ depois ←", + "upLeftThenDown": "↖ depois ↓", + "upRightThenDown": "↗ depois ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ depois ↑", + "upThenDownRight": "↑ depois ↘", + "downThenUpRight": "↓ depois ↗", + "rightThenUp": "→ depois ↑", + "rightThenDown": "→ depois ↓", + "downThenRight": "↓ depois →", + "downThenLeft": "↓ depois ←", + "downRightThenUp": "↘ depois ↑", + "downLeftThenUp": "↙ depois ↑", + "upLeftThenRight": "↖ depois →", + "upRightThenLeft": "↗ depois ←" }, "favoriteAgentsEmpty": "Nenhum agente disponível", "toasts": { "renameNeedsSavedSession": "Renomear só funciona em sessões salvas — envie uma mensagem primeiro.", "sessionFolderNeedsWorkspace": "Abra antes uma pasta de espaço de trabalho.", - "cycleNeedsMoreAgents": "Só há um agente disponível — nada para alternar." - } + "cycleNeedsMoreAgents": "Só há um agente disponível — nada para alternar.", + "noOtherRunning": "Nenhuma outra sessão em execução.", + "noOtherRecent": "Nenhuma outra sessão recente.", + "noPlanMode": "Este agente não tem modo Plano.", + "pinToggled": "Estado de fixação atualizado.", + "noRunningSession": "Ainda não há sessão em execução.", + "noModesAvailable": "Este agente não expõe nenhum modo.", + "noSwitchTarget": "Nenhum modo para alternar (modo Plan ou de acesso completo não encontrado)." + }, + "recentSessionDepth": "Profundidade de sessões recentes", + "bindingCleared": "Sem atribuição" } -} +} \ No newline at end of file diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 45f4b49a8a..db78c4acfc 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -181,6 +181,13 @@ "sectionDescription": "会话区域内的折叠/展开、工具调用、工作状态等动画,以及平滑滚动。关闭后界面立即更新、不再有过渡动画;加载转圈图标仍会旋转。", "enable": "启用聊天区域动画" }, + "messageNav": { + "sectionTitle": "消息导航", + "sectionDescription": "会话消息导航卡的位置。右下角会变成紧凑卡片:每次用户回话一行,贴在输入框上方,不挡阅读区。", + "positionStart": "左上(默认)", + "positionBottomEnd": "右下(紧凑)", + "hint": "计划面板与子智能体面板始终留在左上。" + }, "chatWidth": { "sectionTitle": "聊天区域宽度", "sectionDescription": "会话列与输入框的最大宽度。也可以直接拖动聊天区域左右两侧的边缘来调整。", @@ -258,60 +265,69 @@ "panelTitle": "快捷操作" }, "gestures": { - "tap": "Tap", - "doubleTap": "Double tap", - "longPress": "Long press", - "left": "Swipe left", - "right": "Swipe right", - "up": "Swipe up", - "upLeft": "Up-left", - "upRight": "Up-right", - "upThenLeft": "Up, then left", - "upThenRight": "Up, then right", - "upDown": "Up, then down", - "leftThenRight": "Left, then right", - "rightThenLeft": "Right, then left", - "upLeftThenDown": "Up-left, then down", - "upRightThenDown": "Up-right, then down", - "down": "Swipe down", - "downLeft": "Down-left", - "downRight": "Down-right", - "downUp": "Down, then up", - "upThenDownRight": "Up, then down-right", - "downThenUpRight": "Down, then up-right", - "rightThenUp": "Right, then up", - "rightThenDown": "Right, then down", - "downThenRight": "Down, then right", - "downThenLeft": "Down, then left", - "downRightThenUp": "Down-right, then up", - "downLeftThenUp": "Down-left, then up", - "upLeftThenRight": "Up-left, then right", - "upRightThenLeft": "Up-right, then left" + "tap": "单击", + "doubleTap": "双击", + "longPress": "长按", + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ 然后 ←", + "upThenRight": "↑ 然后 →", + "upDown": "↑ 然后 ↓", + "leftThenRight": "← 然后 →", + "rightThenLeft": "→ 然后 ←", + "upLeftThenDown": "↖ 然后 ↓", + "upRightThenDown": "↗ 然后 ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ 然后 ↑", + "upThenDownRight": "↑ 然后 ↘", + "downThenUpRight": "↓ 然后 ↗", + "rightThenUp": "→ 然后 ↑", + "rightThenDown": "→ 然后 ↓", + "downThenRight": "↓ 然后 →", + "downThenLeft": "↓ 然后 ←", + "downRightThenUp": "↘ 然后 ↑", + "downLeftThenUp": "↙ 然后 ↑", + "upLeftThenRight": "↖ 然后 →", + "upRightThenLeft": "↗ 然后 ←" }, "actions": { - "newConversation": "New conversation", - "toggleSidebar": "Toggle sidebar", - "toggleSearch": "Search", - "toggleTerminal": "Toggle terminal", - "nextTab": "Next tab", - "prevTab": "Previous tab", - "reopenClosedTab": "Reopen closed tab", - "openFolder": "Open folder", - "zoomIn": "Zoom in", - "zoomOut": "Zoom out", - "zoomReset": "Reset zoom", - "stopGenerating": "Stop generating", - "openSettings": "Settings", - "openTodos": "To-dos", - "openTodosNew": "New to-do", - "openAutomations": "Automations", - "openCanvas": "Canvas", - "openAgents": "Agents", - "openSystemAdmin": "System admin", - "renameConversation": "Rename conversation", - "openSessionFolder": "Session files", - "cycleAgent": "Next agent", - "cycleFavoriteAgent": "Favorite agents" + "newConversation": "新建会话", + "toggleSidebar": "切换侧栏", + "toggleSearch": "搜索", + "toggleTerminal": "切换终端", + "nextTab": "下一个标签", + "prevTab": "上一个标签", + "reopenClosedTab": "恢复关闭的标签", + "openFolder": "打开文件夹", + "zoomIn": "放大", + "zoomOut": "缩小", + "zoomReset": "重置缩放", + "stopGenerating": "停止生成", + "openSettings": "设置", + "openTodos": "待办任务", + "openTodosNew": "新建待办", + "openAutomations": "自动化", + "openCanvas": "无限画布", + "openAgents": "智能体", + "openSystemAdmin": "系统管理", + "renameConversation": "重命名会话", + "openSessionFolder": "会话文件", + "cycleAgent": "下一个智能体", + "cycleFavoriteAgent": "常用智能体", + "openBallBindings": "手势绑定", + "cycleRunningSessions": "运行中的会话", + "cycleRecentSessions": "最近的会话", + "deleteCurrentConversation": "删除当前会话", + "openImportSessions": "导入会话", + "togglePinConversation": "置顶/取消置顶", + "focusModeSelector": "模式选择", + "togglePlanFullAccess": "Plan/全权限切换", + "openCollaboration": "协作" } }, "SystemSettings": { @@ -1185,30 +1201,6 @@ "apiKeyHint": "以 KIRO_API_KEY 传给 CLI。留空则使用 kiro-cli login 登录的账号;只有无法用浏览器登录的环境(服务器、Docker)才需要 API Key。", "saveConfig": "保存 Kiro 配置" }, - "trae": { - "configManagement": "配置管理", - "configDescription": "TRAE CLI 2.0 仅向 TRAE 企业版旗舰版套餐客户开放。Codeg 运行你安装的 traecli,它把登录状态、技能和 MCP 服务器保存在 ~/.trae 中。模型、模式和推理强度不在这里设置:TRAE CLI 会通过 ACP 上报它们,由输入框自带的选择器按会话管理。", - "signInTitle": "登录", - "signInHint": "在终端中运行一次下面的命令,用企业 SSO 登录。Codeg 里的会话会复用这次登录。", - "copyCommand": "复制命令", - "commandCopied": "命令已复制", - "patLabel": "个人访问令牌(可选)", - "patHint": "适用于无法用浏览器登录的机器,例如服务器或 Docker 部署。Codeg 会以 TRAECLI_PERSONAL_ACCESS_TOKEN 传给 TRAE CLI,并在每次启动会话时用它登录。留空则使用 traecli login 的登录。", - "hostLabel": "企业 API 地址(可选)", - "hostHint": "仅当你的组织在自有域名上部署 TRAE 时填写。以 TRAECLI_HOST 传入;留空则使用 TRAE 官方服务。", - "saveConfig": "保存 TRAE CLI 配置" - }, - "commandCode": { - "configManagement": "配置管理", - "configDescription": "Codeg 运行 command-code npm 包提供的 Command Code CLI,它把登录状态、技能和 MCP 服务器保存在 ~/.commandcode 中。模型、模式和推理强度不在这里设置:Command Code 会通过 ACP 上报它们,由输入框自带的选择器按会话管理。", - "signInTitle": "登录", - "signInHint": "在终端中运行一次下面的命令,登录你的 Command Code 账号。Codeg 里的会话会复用这次登录。", - "copyCommand": "复制命令", - "commandCopied": "命令已复制", - "apiKeyLabel": "API 密钥(可选)", - "apiKeyHint": "适用于无法用浏览器登录的机器,例如服务器或 Docker 部署。以 COMMAND_CODE_API_KEY 传给 Command Code,优先于 command-code login 的登录。留空则使用那次登录。", - "saveConfig": "保存 Command Code 配置" - }, "deepseek": { "configManagement": "DeepSeek Harness 配置", "configDescription": "端点与密钥是 deepseek-acp 启动时读取的环境变量。模型和推理档位是会话级选择器,在输入框里切换。", @@ -1704,10 +1696,6 @@ "saveAntigravityConfigFailed": "保存 Antigravity 配置失败", "kiroSaved": "Kiro 配置已保存", "saveKiroFailed": "保存 Kiro 配置失败", - "traeSaved": "TRAE CLI 配置已保存", - "saveTraeFailed": "保存 TRAE CLI 配置失败", - "commandCodeSaved": "Command Code 配置已保存", - "saveCommandCodeFailed": "保存 Command Code 配置失败", "systemAgentAlreadyLatest": "{name} 已是最新版本({version})" }, "distribution": { @@ -2479,10 +2467,7 @@ "commits": "提交" }, "noFolderTitle": "暂无打开的文件夹", - "noFolderHint": "打开一个文件夹后在这里查看", - "openFullPage": "在完整页面中打开", - "back": "返回", - "noAutomations": "此项目还没有自动化" + "noFolderHint": "打开一个文件夹后在这里查看" }, "windowControls": { "minimizeWindow": "最小化窗口", @@ -3381,7 +3366,6 @@ }, "messageInput": { "askAnything": "请开始输入...", - "promptSuggestion": "{suggestion}(按 Tab 填入)", "removeAttachmentAria": "移除 {name}", "attachFiles": "附加文件", "attachLocalUpload": "附加本地文件", @@ -3413,10 +3397,6 @@ "searchModelAria": "搜索模型", "modelListLabel": "模型", "recommendedBadge": "推荐", - "recommendedValue": "推荐:{value}", - "searchOptions": "搜索选项...", - "searchOptionsAria": "搜索选项", - "noOptions": "未找到选项", "noModels": "未找到模型", "cancel": "取消", "send": "发送", @@ -4027,16 +4007,13 @@ }, "messageNav": { "title": "消息导航", + "collapse": "收起消息导航", "loadingEarlier": "正在加载更早的消息…", "loadEarlier": "加载更早的消息", + "collapsedSummary": "消息 {count}", "fileCount": "{count} 个文件", - "subAgentCount": "{count} 个子智能体", - "showDiff": "展开 {filePath} 的改动", - "hideDiff": "收起 {filePath} 的改动", - "openFile": "打开 {filePath}", - "collapse": "收起", - "currentMessage": "当前消息", - "scrollToCurrent": "滚动到当前消息(#{ordinal})" + "remove": "移除", + "noDiffDataAvailable": "未找到 {filePath} 的差异数据" }, "replyArtifacts": { "title": "改动文件", @@ -4094,9 +4071,6 @@ "resumeDelegation": "恢复任务 {task}", "resumeDelegationNoId": "恢复委派任务", "promptLabel": "指令", - "ackRecorded": "已记录到待办任务", - "ackNotRecorded": "未记录到待办任务", - "ackNotRecordedWithReason": "未记录到待办任务:{reason}", "verdict": { "success": "成功", "needs_review": "待复核", @@ -4214,18 +4188,6 @@ "stopDeclined": "无法停止该任务,它可能已经结束。", "stopFailed": "停止任务失败:{error}" }, - "remoteControl": { - "title": "远程控制已开启", - "open": "在浏览器中打开", - "copy": "复制链接", - "copied": "已复制", - "qr": "二维码", - "qrLabel": "本会话远程控制链接的二维码", - "qrHint": "用手机扫码,在 Claude App 或 claude.ai/code 上继续本会话。", - "noUrl": "Claude Code 没有返回本会话的链接", - "turnOff": "关闭", - "turnOffBusy": "Claude 完成当前工作后可用" - }, "proposedPlan": { "title": "计划提案", "planning": "规划中…" @@ -7433,44 +7395,62 @@ "renameConversation": "重命名会话", "openSessionFolder": "会话文件", "cycleAgent": "下一个智能体", - "cycleFavoriteAgent": "常用智能体" + "cycleFavoriteAgent": "常用智能体", + "openBallBindings": "手势绑定", + "cycleRunningSessions": "运行中的会话", + "cycleRecentSessions": "最近的会话", + "deleteCurrentConversation": "删除当前会话", + "openImportSessions": "导入会话", + "togglePinConversation": "置顶/取消置顶", + "focusModeSelector": "模式选择", + "togglePlanFullAccess": "Plan/全权限切换", + "openCollaboration": "协作" }, "gestures": { "tap": "单击", "doubleTap": "双击", "longPress": "长按", - "left": "左滑", - "right": "右滑", - "up": "上滑", - "upLeft": "左上滑", - "upRight": "右上滑", - "upThenLeft": "上滑后左滑", - "upThenRight": "上滑后右滑", - "upDown": "上滑后下滑", - "leftThenRight": "左滑后右滑", - "rightThenLeft": "右滑后左滑", - "upLeftThenDown": "左上滑后下滑", - "upRightThenDown": "右上滑后下滑", - "down": "下滑", - "downLeft": "左下", - "downRight": "右下", - "downUp": "下再上", - "upThenDownRight": "上再右下", - "downThenUpRight": "下再右上", - "rightThenUp": "右再上", - "rightThenDown": "右再下", - "downThenRight": "下再右", - "downThenLeft": "下再左", - "downRightThenUp": "右下再上", - "downLeftThenUp": "左下再上", - "upLeftThenRight": "左上再右", - "upRightThenLeft": "右上再左" + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ 然后 ←", + "upThenRight": "↑ 然后 →", + "upDown": "↑ 然后 ↓", + "leftThenRight": "← 然后 →", + "rightThenLeft": "→ 然后 ←", + "upLeftThenDown": "↖ 然后 ↓", + "upRightThenDown": "↗ 然后 ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ 然后 ↑", + "upThenDownRight": "↑ 然后 ↘", + "downThenUpRight": "↓ 然后 ↗", + "rightThenUp": "→ 然后 ↑", + "rightThenDown": "→ 然后 ↓", + "downThenRight": "↓ 然后 →", + "downThenLeft": "↓ 然后 ←", + "downRightThenUp": "↘ 然后 ↑", + "downLeftThenUp": "↙ 然后 ↑", + "upLeftThenRight": "↖ 然后 →", + "upRightThenLeft": "↗ 然后 ←" }, "favoriteAgentsEmpty": "暂无可用智能体", "toasts": { "renameNeedsSavedSession": "重命名只对已保存的会话生效——先发一条消息。", "sessionFolderNeedsWorkspace": "请先打开一个工作区文件夹。", - "cycleNeedsMoreAgents": "可用智能体只有一个,无可切换。" - } + "cycleNeedsMoreAgents": "可用智能体只有一个,无可切换。", + "noOtherRunning": "没有其他运行中的会话", + "noOtherRecent": "没有其他最近的会话", + "noPlanMode": "当前智能体没有 Plan 模式", + "pinToggled": "置顶状态已更新", + "noRunningSession": "还没有运行中的会话", + "noModesAvailable": "该智能体没有可用模式。", + "noSwitchTarget": "没有可切换的模式(未找到 Plan 或完整访问模式)。" + }, + "recentSessionDepth": "最近会话切换深度", + "bindingCleared": "未绑定" } -} +} \ No newline at end of file diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index bdcb6dfb2d..5abd210c9e 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -181,6 +181,13 @@ "sectionDescription": "對話區域內的摺疊/展開、工具呼叫、工作狀態等動畫,以及平滑捲動。關閉後介面會立即更新、不再有過渡動畫;載入轉圈圖示仍會旋轉。", "enable": "啟用聊天區域動畫" }, + "messageNav": { + "sectionTitle": "訊息導覽", + "sectionDescription": "對話訊息導覽卡的位置。右下角會變成緊湊卡片:每次使用者回話一行,貼在輸入框上方,不擋閱讀區。", + "positionStart": "左上(預設)", + "positionBottomEnd": "右下(緊湊)", + "hint": "計畫面板與子代理面板始終留在左上。" + }, "chatWidth": { "sectionTitle": "聊天區域寬度", "sectionDescription": "對話列與輸入框的最大寬度。也可以直接拖曳聊天區域左右兩側的邊緣來調整。", @@ -258,60 +265,69 @@ "panelTitle": "快捷操作" }, "gestures": { - "tap": "Tap", - "doubleTap": "Double tap", - "longPress": "Long press", - "left": "Swipe left", - "right": "Swipe right", - "up": "Swipe up", - "upLeft": "Up-left", - "upRight": "Up-right", - "upThenLeft": "Up, then left", - "upThenRight": "Up, then right", - "upDown": "Up, then down", - "leftThenRight": "Left, then right", - "rightThenLeft": "Right, then left", - "upLeftThenDown": "Up-left, then down", - "upRightThenDown": "Up-right, then down", - "down": "Swipe down", - "downLeft": "Down-left", - "downRight": "Down-right", - "downUp": "Down, then up", - "upThenDownRight": "Up, then down-right", - "downThenUpRight": "Down, then up-right", - "rightThenUp": "Right, then up", - "rightThenDown": "Right, then down", - "downThenRight": "Down, then right", - "downThenLeft": "Down, then left", - "downRightThenUp": "Down-right, then up", - "downLeftThenUp": "Down-left, then up", - "upLeftThenRight": "Up-left, then right", - "upRightThenLeft": "Up-right, then left" + "tap": "單擊", + "doubleTap": "雙擊", + "longPress": "長按", + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ 然後 ←", + "upThenRight": "↑ 然後 →", + "upDown": "↑ 然後 ↓", + "leftThenRight": "← 然後 →", + "rightThenLeft": "→ 然後 ←", + "upLeftThenDown": "↖ 然後 ↓", + "upRightThenDown": "↗ 然後 ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ 然後 ↑", + "upThenDownRight": "↑ 然後 ↘", + "downThenUpRight": "↓ 然後 ↗", + "rightThenUp": "→ 然後 ↑", + "rightThenDown": "→ 然後 ↓", + "downThenRight": "↓ 然後 →", + "downThenLeft": "↓ 然後 ←", + "downRightThenUp": "↘ 然後 ↑", + "downLeftThenUp": "↙ 然後 ↑", + "upLeftThenRight": "↖ 然後 →", + "upRightThenLeft": "↗ 然後 ←" }, "actions": { - "newConversation": "New conversation", - "toggleSidebar": "Toggle sidebar", - "toggleSearch": "Search", - "toggleTerminal": "Toggle terminal", - "nextTab": "Next tab", - "prevTab": "Previous tab", - "reopenClosedTab": "Reopen closed tab", - "openFolder": "Open folder", - "zoomIn": "Zoom in", - "zoomOut": "Zoom out", - "zoomReset": "Reset zoom", - "stopGenerating": "Stop generating", - "openSettings": "Settings", - "openTodos": "To-dos", - "openTodosNew": "New to-do", - "openAutomations": "Automations", - "openCanvas": "Canvas", - "openAgents": "Agents", - "openSystemAdmin": "System admin", - "renameConversation": "Rename conversation", - "openSessionFolder": "Session files", - "cycleAgent": "Next agent", - "cycleFavoriteAgent": "Favorite agents" + "newConversation": "新增對話", + "toggleSidebar": "切換側欄", + "toggleSearch": "搜尋", + "toggleTerminal": "切換終端機", + "nextTab": "下一個分頁", + "prevTab": "上一個分頁", + "reopenClosedTab": "恢復關閉的分頁", + "openFolder": "開啟資料夾", + "zoomIn": "放大", + "zoomOut": "縮小", + "zoomReset": "重設縮放", + "stopGenerating": "停止生成", + "openSettings": "設定", + "openTodos": "待辦任務", + "openTodosNew": "新建待辦", + "openAutomations": "自動化", + "openCanvas": "無限畫布", + "openAgents": "智慧代理人", + "openSystemAdmin": "系統管理", + "renameConversation": "重新命名會話", + "openSessionFolder": "會話檔案", + "cycleAgent": "下一個代理人", + "cycleFavoriteAgent": "常用代理人", + "openBallBindings": "手勢綁定", + "cycleRunningSessions": "執行中的會話", + "cycleRecentSessions": "最近的會話", + "deleteCurrentConversation": "刪除當前會話", + "openImportSessions": "匯入會話", + "togglePinConversation": "置頂/取消置頂", + "focusModeSelector": "模式選擇", + "togglePlanFullAccess": "Plan/全權限切換", + "openCollaboration": "協作" } }, "SystemSettings": { @@ -1185,30 +1201,6 @@ "apiKeyHint": "以 KIRO_API_KEY 傳給 CLI。留空則使用 kiro-cli login 登入的帳號;只有無法用瀏覽器登入的環境(伺服器、Docker)才需要 API Key。", "saveConfig": "儲存 Kiro 設定" }, - "trae": { - "configManagement": "設定管理", - "configDescription": "TRAE CLI 2.0 僅開放給 TRAE 企業版旗艦版方案的客戶。Codeg 執行你安裝的 traecli,它把登入狀態、技能和 MCP 伺服器儲存在 ~/.trae 中。模型、模式和推理強度不在這裡設定:TRAE CLI 會透過 ACP 回報它們,由輸入框內建的選擇器依工作階段管理。", - "signInTitle": "登入", - "signInHint": "在終端機中執行一次下面的命令,用企業 SSO 登入。Codeg 裡的工作階段會沿用這次登入。", - "copyCommand": "複製命令", - "commandCopied": "命令已複製", - "patLabel": "個人存取權杖(選填)", - "patHint": "適用於無法用瀏覽器登入的機器,例如伺服器或 Docker 部署。Codeg 會以 TRAECLI_PERSONAL_ACCESS_TOKEN 傳給 TRAE CLI,並在每次啟動工作階段時用它登入。留空則使用 traecli login 的登入。", - "hostLabel": "企業 API 位址(選填)", - "hostHint": "僅當你的組織在自有網域上部署 TRAE 時填寫。以 TRAECLI_HOST 傳入;留空則使用 TRAE 官方服務。", - "saveConfig": "儲存 TRAE CLI 設定" - }, - "commandCode": { - "configManagement": "設定管理", - "configDescription": "Codeg 執行 command-code npm 套件提供的 Command Code CLI,它把登入狀態、技能和 MCP 伺服器儲存在 ~/.commandcode 中。模型、模式和推理強度不在這裡設定:Command Code 會透過 ACP 回報它們,由輸入框內建的選擇器依工作階段管理。", - "signInTitle": "登入", - "signInHint": "在終端機中執行一次下面的命令,登入你的 Command Code 帳號。Codeg 裡的工作階段會沿用這次登入。", - "copyCommand": "複製命令", - "commandCopied": "命令已複製", - "apiKeyLabel": "API 金鑰(選填)", - "apiKeyHint": "適用於無法用瀏覽器登入的機器,例如伺服器或 Docker 部署。以 COMMAND_CODE_API_KEY 傳給 Command Code,優先於 command-code login 的登入。留空則使用那次登入。", - "saveConfig": "儲存 Command Code 設定" - }, "deepseek": { "configManagement": "DeepSeek Harness 設定", "configDescription": "端點與金鑰是 deepseek-acp 啟動時讀取的環境變數。模型與推理等級是工作階段層級的選擇器,在輸入框裡切換。", @@ -1704,10 +1696,6 @@ "saveAntigravityConfigFailed": "保存 Antigravity 配置失败", "kiroSaved": "Kiro 設定已儲存", "saveKiroFailed": "儲存 Kiro 設定失敗", - "traeSaved": "TRAE CLI 設定已儲存", - "saveTraeFailed": "儲存 TRAE CLI 設定失敗", - "commandCodeSaved": "Command Code 設定已儲存", - "saveCommandCodeFailed": "儲存 Command Code 設定失敗", "systemAgentAlreadyLatest": "{name} 已是最新版本({version})" }, "distribution": { @@ -2479,10 +2467,7 @@ "commits": "提交" }, "noFolderTitle": "尚無開啟的資料夾", - "noFolderHint": "開啟一個資料夾後可在此查看", - "openFullPage": "在完整頁面中開啟", - "back": "返回", - "noAutomations": "此專案還沒有自動化" + "noFolderHint": "開啟一個資料夾後可在此查看" }, "windowControls": { "minimizeWindow": "最小化視窗", @@ -3381,7 +3366,6 @@ }, "messageInput": { "askAnything": "請開始輸入...", - "promptSuggestion": "{suggestion}(按 Tab 填入)", "removeAttachmentAria": "移除 {name}", "attachFiles": "附加檔案", "addActions": "新增", @@ -3411,10 +3395,6 @@ "searchModelAria": "搜尋模型", "modelListLabel": "模型", "recommendedBadge": "推薦", - "recommendedValue": "推薦:{value}", - "searchOptions": "搜尋選項...", - "searchOptionsAria": "搜尋選項", - "noOptions": "找不到選項", "noModels": "找不到模型", "cancel": "取消", "send": "傳送", @@ -4027,16 +4007,13 @@ }, "messageNav": { "title": "訊息導覽", + "collapse": "收合訊息導覽", "loadingEarlier": "正在載入更早的訊息…", "loadEarlier": "載入更早的訊息", + "collapsedSummary": "訊息 {count}", "fileCount": "{count} 個檔案", - "subAgentCount": "{count} 個子智能體", - "showDiff": "展開 {filePath} 的變更", - "hideDiff": "收合 {filePath} 的變更", - "openFile": "開啟 {filePath}", - "collapse": "收合", - "currentMessage": "目前訊息", - "scrollToCurrent": "捲動到目前訊息(#{ordinal})" + "remove": "移除", + "noDiffDataAvailable": "找不到 {filePath} 的差異資料" }, "replyArtifacts": { "title": "變更檔案", @@ -4094,9 +4071,6 @@ "resumeDelegation": "恢復任務 {task}", "resumeDelegationNoId": "恢復委派任務", "promptLabel": "指令", - "ackRecorded": "已記錄到待辦任務", - "ackNotRecorded": "未記錄到待辦任務", - "ackNotRecordedWithReason": "未記錄到待辦任務:{reason}", "verdict": { "success": "成功", "needs_review": "待複核", @@ -4214,18 +4188,6 @@ "stopDeclined": "無法停止該任務,它可能已經結束。", "stopFailed": "停止任務失敗:{error}" }, - "remoteControl": { - "title": "遠端控制已開啟", - "open": "在瀏覽器中開啟", - "copy": "複製連結", - "copied": "已複製", - "qr": "QR 碼", - "qrLabel": "本會話遠端控制連結的 QR 碼", - "qrHint": "用手機掃描,在 Claude App 或 claude.ai/code 上繼續本會話。", - "noUrl": "Claude Code 沒有傳回本會話的連結", - "turnOff": "關閉", - "turnOffBusy": "Claude 完成目前工作後可用" - }, "proposedPlan": { "title": "計劃提案", "planning": "規劃中…" @@ -7433,44 +7395,62 @@ "renameConversation": "重新命名會話", "openSessionFolder": "會話檔案", "cycleAgent": "下一個代理人", - "cycleFavoriteAgent": "常用代理人" + "cycleFavoriteAgent": "常用代理人", + "openBallBindings": "手勢綁定", + "cycleRunningSessions": "執行中的會話", + "cycleRecentSessions": "最近的會話", + "deleteCurrentConversation": "刪除當前會話", + "openImportSessions": "匯入會話", + "togglePinConversation": "置頂/取消置頂", + "focusModeSelector": "模式選擇", + "togglePlanFullAccess": "Plan/全權限切換", + "openCollaboration": "協作" }, "gestures": { "tap": "單擊", "doubleTap": "雙擊", "longPress": "長按", - "left": "左滑", - "right": "右滑", - "up": "上滑", - "upLeft": "左上滑", - "upRight": "右上滑", - "upThenLeft": "上滑後左滑", - "upThenRight": "上滑後右滑", - "upDown": "上滑後下滑", - "leftThenRight": "左滑後右滑", - "rightThenLeft": "右滑後左滑", - "upLeftThenDown": "左上滑後下滑", - "upRightThenDown": "右上滑後下滑", - "down": "下滑", - "downLeft": "左下", - "downRight": "右下", - "downUp": "下再上", - "upThenDownRight": "上再右下", - "downThenUpRight": "下再右上", - "rightThenUp": "右再上", - "rightThenDown": "右再下", - "downThenRight": "下再右", - "downThenLeft": "下再左", - "downRightThenUp": "右下再上", - "downLeftThenUp": "左下再上", - "upLeftThenRight": "左上再右", - "upRightThenLeft": "右上再左" + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ 然後 ←", + "upThenRight": "↑ 然後 →", + "upDown": "↑ 然後 ↓", + "leftThenRight": "← 然後 →", + "rightThenLeft": "→ 然後 ←", + "upLeftThenDown": "↖ 然後 ↓", + "upRightThenDown": "↗ 然後 ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ 然後 ↑", + "upThenDownRight": "↑ 然後 ↘", + "downThenUpRight": "↓ 然後 ↗", + "rightThenUp": "→ 然後 ↑", + "rightThenDown": "→ 然後 ↓", + "downThenRight": "↓ 然後 →", + "downThenLeft": "↓ 然後 ←", + "downRightThenUp": "↘ 然後 ↑", + "downLeftThenUp": "↙ 然後 ↑", + "upLeftThenRight": "↖ 然後 →", + "upRightThenLeft": "↗ 然後 ←" }, "favoriteAgentsEmpty": "暫無可用代理人", "toasts": { "renameNeedsSavedSession": "重新命名只對已儲存的會話生效——先發一則訊息。", "sessionFolderNeedsWorkspace": "請先開啟一個工作區資料夾。", - "cycleNeedsMoreAgents": "可用智慧型代理只有一個,無可切換。" - } + "cycleNeedsMoreAgents": "可用智慧型代理只有一個,無可切換。", + "noOtherRunning": "沒有其他執行中的會話", + "noOtherRecent": "沒有其他最近的會話", + "noPlanMode": "當前智能體沒有 Plan 模式", + "pinToggled": "置頂狀態已更新", + "noRunningSession": "還沒有執行中的會話", + "noModesAvailable": "該智慧代理沒有可用模式。", + "noSwitchTarget": "沒有可切換的模式(未找到 Plan 或完整存取模式)。" + }, + "recentSessionDepth": "最近會話切換深度", + "bindingCleared": "未綁定" } -} +} \ No newline at end of file diff --git a/src/lib/floating-ball-gestures.ts b/src/lib/floating-ball-gestures.ts index 30da2f4ac7..85ea893dae 100644 --- a/src/lib/floating-ball-gestures.ts +++ b/src/lib/floating-ball-gestures.ts @@ -75,6 +75,15 @@ export type FloatingBallAction = | "openSessionFolder" | "cycleAgent" | "cycleFavoriteAgent" + | "openBallBindings" + | "cycleRunningSessions" + | "cycleRecentSessions" + | "deleteCurrentConversation" + | "openImportSessions" + | "togglePinConversation" + | "focusModeSelector" + | "togglePlanFullAccess" + | "openCollaboration" export const FLOATING_BALL_GESTURES: readonly FloatingBallGesture[] = [ "tap", diff --git a/src/lib/floating-ball-session-events.test.ts b/src/lib/floating-ball-session-events.test.ts new file mode 100644 index 0000000000..ecea63d6ef --- /dev/null +++ b/src/lib/floating-ball-session-events.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, vi, beforeEach } from "vitest" +import { + requestDeleteConversation, + requestFocusModeSelector, + requestTogglePlanFull, + eventMatchesConversation, + DELETE_CONVERSATION_EVENT, + FOCUS_MODE_SELECTOR_EVENT, + TOGGLE_PLAN_FULL_EVENT, +} from "./floating-ball-session-events" + +describe("floating-ball-session-events", () => { + beforeEach(() => { + vi.restoreAllMocks() + }) + + it("dispatch carries the conversation id on the right event type", () => { + const seen: Array<{ type: string; id: number | null }> = [] + const onDelete = (e: Event) => + seen.push({ + type: e.type, + id: (e as CustomEvent<{ conversationId: number | null }>).detail + .conversationId, + }) + window.addEventListener(DELETE_CONVERSATION_EVENT, onDelete) + window.addEventListener(FOCUS_MODE_SELECTOR_EVENT, onDelete) + window.addEventListener(TOGGLE_PLAN_FULL_EVENT, onDelete) + + requestDeleteConversation(7) + requestFocusModeSelector(null) + requestTogglePlanFull(9) + + window.removeEventListener(DELETE_CONVERSATION_EVENT, onDelete) + window.removeEventListener(FOCUS_MODE_SELECTOR_EVENT, onDelete) + window.removeEventListener(TOGGLE_PLAN_FULL_EVENT, onDelete) + + expect(seen).toEqual([ + { type: DELETE_CONVERSATION_EVENT, id: 7 }, + { type: FOCUS_MODE_SELECTOR_EVENT, id: null }, + { type: TOGGLE_PLAN_FULL_EVENT, id: 9 }, + ]) + }) + + it("eventMatchesConversation filters by id (multi-tab safety)", () => { + expect(eventMatchesConversation({ conversationId: 3 }, 3)).toBe(true) + expect(eventMatchesConversation({ conversationId: 3 }, 4)).toBe(false) + // Draft (null) events only match draft instances. + expect(eventMatchesConversation({ conversationId: null }, null)).toBe(true) + expect(eventMatchesConversation({ conversationId: null }, 3)).toBe(false) + expect(eventMatchesConversation(undefined, 3)).toBe(false) + }) +}) diff --git a/src/lib/floating-ball-session-events.ts b/src/lib/floating-ball-session-events.ts new file mode 100644 index 0000000000..a72999d588 --- /dev/null +++ b/src/lib/floating-ball-session-events.ts @@ -0,0 +1,47 @@ +/** + * Floating-ball hand-off events for actions that must run inside a mounted + * conversation view (which owns the mode selector, pin state and the delete + * confirm dialog). Each event carries the DB conversation id and each + * per-tab instance compares it against its own — a tab whose conversation + * does not match ignores the event (multi-tab safe, same idiom as + * rename-conversation-events but synchronous). + */ + +export const DELETE_CONVERSATION_EVENT = "codeg:fb-delete-conversation" +export const FOCUS_MODE_SELECTOR_EVENT = "codeg:fb-focus-mode-selector" +export const TOGGLE_PLAN_FULL_EVENT = "codeg:fb-toggle-plan-full" + +export interface FloatingBallSessionDetail { + conversationId: number | null +} + +function dispatch(type: string, conversationId: number | null) { + window.dispatchEvent( + new CustomEvent(type, { + detail: { conversationId } satisfies FloatingBallSessionDetail, + }) + ) +} + +/** Open the delete confirmation for the current session (header-owned UI). */ +export function requestDeleteConversation(conversationId: number | null) { + dispatch(DELETE_CONVERSATION_EVENT, conversationId) +} + +/** Focus/open the composer's inline mode selector on the active tab. */ +export function requestFocusModeSelector(conversationId: number | null) { + dispatch(FOCUS_MODE_SELECTOR_EVENT, conversationId) +} + +/** Switch the active session between Plan and full-access mode. */ +export function requestTogglePlanFull(conversationId: number | null) { + dispatch(TOGGLE_PLAN_FULL_EVENT, conversationId) +} + +/** True when the event targets this instance's conversation (null = drafts). */ +export function eventMatchesConversation( + detail: FloatingBallSessionDetail | undefined, + conversationId: number | null +): boolean { + return detail?.conversationId === conversationId +} diff --git a/src/lib/recent-sessions-prefs.test.ts b/src/lib/recent-sessions-prefs.test.ts new file mode 100644 index 0000000000..a4e59ba6c5 --- /dev/null +++ b/src/lib/recent-sessions-prefs.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, beforeEach } from "vitest" +import { + rememberRecentConversation, + forgetRecentConversation, + nextRecentConversation, + readRecentSessionDepth, + writeRecentSessionDepth, + DEFAULT_RECENT_SESSION_DEPTH, +} from "./recent-sessions-prefs" + +describe("recent-sessions-prefs", () => { + beforeEach(() => { + window.localStorage.clear() + }) + + it("rememberRecentConversation dedupes and puts most recent first", () => { + rememberRecentConversation(1) + rememberRecentConversation(2) + rememberRecentConversation(1) + expect(nextRecentConversation(null, 4, () => true)).toBe(1) + // After re-remembering 1, the next candidate (not active) is 2. + rememberRecentConversation(1) + expect(nextRecentConversation(1, 4, () => true)).toBe(2) + }) + + it("caps the queue at 8 entries", () => { + for (let i = 1; i <= 12; i++) rememberRecentConversation(i) + // Only ids 5..12 survive. + for (let i = 1; i <= 4; i++) { + expect(nextRecentConversation(null, 8, (id) => id === i)).toBeNull() + } + expect(nextRecentConversation(null, 8, (id) => id === 5)).toBe(5) + }) + + it("nextRecentConversation respects depth - 1 candidates", () => { + rememberRecentConversation(1) + rememberRecentConversation(2) + rememberRecentConversation(3) + // Queue is [3,2,1]. Depth 2 → one candidate (3). Excluding active 3 → 2. + expect(nextRecentConversation(3, 2, () => true)).toBe(2) + // Depth 4 → candidates [3,2,1]; excluding active 2 → first is 3. + expect(nextRecentConversation(2, 4, () => true)).toBe(3) + }) + + it("nextRecentConversation filters unavailable ids", () => { + rememberRecentConversation(1) + rememberRecentConversation(2) + expect(nextRecentConversation(null, 4, (id) => id !== 2)).toBe(1) + expect(nextRecentConversation(null, 4, () => false)).toBeNull() + }) + + it("forgetRecentConversation removes a deleted conversation", () => { + rememberRecentConversation(1) + rememberRecentConversation(2) + forgetRecentConversation(1) + expect(nextRecentConversation(2, 4, () => true)).toBeNull() + expect(nextRecentConversation(1, 4, () => true)).toBe(2) + }) + + it("depth defaults to 2 and only accepts 3/4 overrides", () => { + expect(readRecentSessionDepth()).toBe(DEFAULT_RECENT_SESSION_DEPTH) + writeRecentSessionDepth(4) + expect(readRecentSessionDepth()).toBe(4) + writeRecentSessionDepth(3) + expect(readRecentSessionDepth()).toBe(3) + }) + + it("survives corrupted storage", () => { + window.localStorage.setItem("codeg:recent-conversations", "not-json{") + expect(nextRecentConversation(null, 4, () => true)).toBeNull() + window.localStorage.setItem( + "codeg:recent-conversations", + JSON.stringify(["a", 5, null, 7]) + ) + expect(nextRecentConversation(null, 4, () => true)).toBe(5) + }) +}) diff --git a/src/lib/recent-sessions-prefs.ts b/src/lib/recent-sessions-prefs.ts new file mode 100644 index 0000000000..2b48dbe5ad --- /dev/null +++ b/src/lib/recent-sessions-prefs.ts @@ -0,0 +1,92 @@ +/** + * "Recent sessions" LRU queue + switch-depth preference, backing the floating + * ball's cycle-recent-sessions gesture. + * + * The queue lives in localStorage (shared across windows of the same origin) + * and stores conversationIds only; consumers must filter to conversations + * their own window can actually open. Depth is 2-4 (default 2): with depth 2 + * the gesture toggles between the two most recently opened conversations. + */ +const STORAGE_KEY_RECENT_CONVERSATIONS = "codeg:recent-conversations" +const STORAGE_KEY_RECENT_SESSION_DEPTH = "codeg:recent-session-depth" + +/** Queue holds more than the max depth so switching survives a bit of drift. */ +const QUEUE_CAPACITY = 8 +export const MIN_RECENT_SESSION_DEPTH = 2 +export const MAX_RECENT_SESSION_DEPTH = 4 +export const DEFAULT_RECENT_SESSION_DEPTH = 2 + +export type RecentSessionDepth = 2 | 3 | 4 + +function readQueue(): number[] { + if (typeof window === "undefined") return [] + try { + const raw = window.localStorage.getItem(STORAGE_KEY_RECENT_CONVERSATIONS) + if (!raw) return [] + const parsed: unknown = JSON.parse(raw) + if (!Array.isArray(parsed)) return [] + return parsed.filter( + (id): id is number => typeof id === "number" && Number.isFinite(id) + ) + } catch { + return [] + } +} + +function writeQueue(queue: number[]): void { + if (typeof window === "undefined") return + try { + window.localStorage.setItem( + STORAGE_KEY_RECENT_CONVERSATIONS, + JSON.stringify(queue.slice(0, QUEUE_CAPACITY)) + ) + } catch { + // Storage full or unavailable: cycling just becomes a no-op, acceptable. + } +} + +/** Push a conversation as "just opened", deduped, most-recent first. */ +export function rememberRecentConversation(id: number): void { + const next = [id, ...readQueue().filter((v) => v !== id)] + writeQueue(next) +} + +/** Drop a deleted conversation so cycling never lands on it. */ +export function forgetRecentConversation(id: number): void { + const next = readQueue().filter((v) => v !== id) + if (next.length !== readQueue().length) writeQueue(next) +} + +/** + * Pick the next conversation to switch to: the most recent entry that isn't + * the active one, within the configured depth window. Returns null when there + * is nothing to switch to. Moving the picked id to the front is what makes + * repeated gestures toggle between two sessions at depth 2. + */ +export function nextRecentConversation( + activeId: number | null, + depth: number, + isAvailable: (id: number) => boolean +): number | null { + const candidates = readQueue() + .filter((id) => id !== activeId && isAvailable(id)) + .slice(0, Math.max(0, depth - 1)) + return candidates.length > 0 ? candidates[0] : null +} + +export function readRecentSessionDepth(): RecentSessionDepth { + if (typeof window === "undefined") return DEFAULT_RECENT_SESSION_DEPTH + const raw = window.localStorage.getItem(STORAGE_KEY_RECENT_SESSION_DEPTH) + return raw === "3" || raw === "4" + ? (Number(raw) as RecentSessionDepth) + : DEFAULT_RECENT_SESSION_DEPTH +} + +export function writeRecentSessionDepth(depth: RecentSessionDepth): void { + if (typeof window === "undefined") return + try { + window.localStorage.setItem(STORAGE_KEY_RECENT_SESSION_DEPTH, String(depth)) + } catch { + // Unavailable storage: the selector will read the default back. + } +} diff --git a/src/stores/tab-store.ts b/src/stores/tab-store.ts index ba806fe86b..6cb2a2cc58 100644 --- a/src/stores/tab-store.ts +++ b/src/stores/tab-store.ts @@ -36,6 +36,7 @@ import { sweepOrphanDraftKeys, } from "@/lib/message-input-draft" import { discardAskSelectionPrompts } from "@/lib/ask-selection-handoff" +import { rememberRecentConversation } from "@/lib/recent-sessions-prefs" import { batchCloseSlots, pushClosedTab, @@ -1056,6 +1057,13 @@ function focusTab(tabId: string) { if (useTabStore.getState().activeTabId !== tabId) { useTabStore.setState({ activeTabId: tabId }) } + // Every activation path funnels through here, so this is the one place the + // floating ball's "recent sessions" LRU needs to record a conversation as + // "just opened" (bound tabs only; drafts have no DB row yet). + const tab = useTabStore.getState().rawTabs.find((t) => t.id === tabId) + if (tab?.conversationId != null) { + rememberRecentConversation(tab.conversationId) + } applyGroupInvariants() } @@ -2101,6 +2109,11 @@ export const useTabStore = create()((set, get) => ({ ? prevState.activeTabId : (boundTab?.id ?? nextTabs[0]?.id ?? null), }) + // A draft becomes a real session the first time it persists a DB row — + // that is its "first open" for the recent-sessions LRU. + if (conversationId != null) { + rememberRecentConversation(conversationId) + } recomputeTabs() }, From dc9e1e4a2e36a49cb25817815e9de78aa056e6df Mon Sep 17 00:00:00 2001 From: fengx Date: Sun, 18 Oct 2026 16:07:30 +0800 Subject: [PATCH 9/9] =?UTF-8?q?chore(i18n):=20rebuild=20locale=20files=20o?= =?UTF-8?q?n=20upstream=20base=20=E2=80=94=20floating-ball=20sections=20me?= =?UTF-8?q?rged,=20upstream's=20new=20keys=20kept?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/i18n/messages/ar.json | 183 +++++++++++++++++++++------------- src/i18n/messages/de.json | 183 +++++++++++++++++++++------------- src/i18n/messages/en.json | 183 +++++++++++++++++++++------------- src/i18n/messages/es.json | 183 +++++++++++++++++++++------------- src/i18n/messages/fr.json | 183 +++++++++++++++++++++------------- src/i18n/messages/ja.json | 183 +++++++++++++++++++++------------- src/i18n/messages/ko.json | 183 +++++++++++++++++++++------------- src/i18n/messages/pt.json | 183 +++++++++++++++++++++------------- src/i18n/messages/zh-CN.json | 183 +++++++++++++++++++++------------- src/i18n/messages/zh-TW.json | 183 +++++++++++++++++++++------------- src/lib/default-mode-prefs.ts | 32 ++++++ 11 files changed, 1182 insertions(+), 680 deletions(-) create mode 100644 src/lib/default-mode-prefs.ts diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index a557656ba3..30a1b5f8e2 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -181,13 +181,6 @@ "sectionDescription": "حركات الطي/التوسيع واستدعاءات الأدوات وحالة العمل داخل منطقة المحادثة، إضافةً إلى التمرير السلس. عند الإيقاف تتحدّث الواجهة فورًا. تستمر مؤشرات التحميل في الدوران.", "enable": "تفعيل حركات المحادثة" }, - "messageNav": { - "sectionTitle": "مستكشف الرسائل", - "sectionDescription": "موضع مستكشف رسائل المحادثة. عند اختيار أسفل اليمين يصبح بطاقة مُدمجة بسطر واحد لكل رسالة مستخدم فوق حقل الإدخال.", - "positionStart": "أعلى اليسار (افتراضي)", - "positionBottomEnd": "أسفل اليمين (مُدمج)", - "hint": "تبقى لوحتا الخطة والوكلاء الفرعيين في أعلى اليسار في كل الأحوال." - }, "chatWidth": { "sectionTitle": "عرض المحادثة", "sectionDescription": "أقصى عرض لعمود المحادثة ومربع الإدخال. يمكنك أيضًا سحب حافتي منطقة المحادثة لضبطه.", @@ -247,54 +240,6 @@ "credit": "الصور من wallhaven.cc · محتوى آمن فقط" } }, - "floatingBall": { - "sectionTitle": "الكرة العائمة", - "sectionDescription": "كرة قابلة للسحب فوق مساحة العمل تحوّل السحب إلى اختصارات، مصممة للهاتف حيث لا توجد لوحة مفاتيح.", - "enable": "إظهار الكرة العائمة", - "hint": "اسحب للتحريك، اسحب بسرعة لإيماءة، اضغط مطوّلاً للوحة وقائمة الإيماءات.", - "resetPosition": "إعادة تعيين الموضع", - "ballSize": "حجم الكرة", - "sizeSmall": "صغير", - "sizeMedium": "متوسط", - "sizeLarge": "كبير", - "gestureBindings": "ربط الإيماءات", - "bindingCleared": "غير مربوط", - "favoriteAgents": "الوكلاء المفضلون", - "favoriteAgentsHint": "اختر 2-3 وكلاء للتنقل بينهم بإيماءة المفضلين.", - "favoriteAgentsEmpty": "لا يوجد وكلاء متاحون", - "panelTitle": "إجراءات سريعة" - }, - "gestures": { - "tap": "نقرة", - "doubleTap": "نقرة مزدوجة", - "longPress": "ضغط مطول", - "left": "←", - "right": "→", - "up": "↑", - "upLeft": "↖", - "upRight": "↗", - "upThenLeft": "↑ ثم ←", - "upThenRight": "↑ ثم →", - "upDown": "↑ ثم ↓", - "leftThenRight": "← ثم →", - "rightThenLeft": "→ ثم ←", - "upLeftThenDown": "↖ ثم ↓", - "upRightThenDown": "↗ ثم ↓", - "down": "↓", - "downLeft": "↙", - "downRight": "↘", - "downUp": "↓ ثم ↑", - "upThenDownRight": "↑ ثم ↘", - "downThenUpRight": "↓ ثم ↗", - "rightThenUp": "→ ثم ↑", - "rightThenDown": "→ ثم ↓", - "downThenRight": "↓ ثم →", - "downThenLeft": "↓ ثم ←", - "downRightThenUp": "↘ ثم ↑", - "downLeftThenUp": "↙ ثم ↑", - "upLeftThenRight": "↖ ثم →", - "upRightThenLeft": "↗ ثم ←" - }, "actions": { "newConversation": "محادثة جديدة", "toggleSidebar": "تبديل الشريط الجانبي", @@ -328,6 +273,54 @@ "focusModeSelector": "محدد الوضع", "togglePlanFullAccess": "خطة / وصول كامل", "openCollaboration": "التعاون" + }, + "gestures": { + "tap": "نقرة", + "doubleTap": "نقرة مزدوجة", + "longPress": "ضغط مطول", + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ ثم ←", + "upThenRight": "↑ ثم →", + "upDown": "↑ ثم ↓", + "leftThenRight": "← ثم →", + "rightThenLeft": "→ ثم ←", + "upLeftThenDown": "↖ ثم ↓", + "upRightThenDown": "↗ ثم ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ ثم ↑", + "upThenDownRight": "↑ ثم ↘", + "downThenUpRight": "↓ ثم ↗", + "rightThenUp": "→ ثم ↑", + "rightThenDown": "→ ثم ↓", + "downThenRight": "↓ ثم →", + "downThenLeft": "↓ ثم ←", + "downRightThenUp": "↘ ثم ↑", + "downLeftThenUp": "↙ ثم ↑", + "upLeftThenRight": "↖ ثم →", + "upRightThenLeft": "↗ ثم ←" + }, + "floatingBall": { + "sectionTitle": "الكرة العائمة", + "sectionDescription": "كرة قابلة للسحب فوق مساحة العمل تحوّل السحب إلى اختصارات، مصممة للهاتف حيث لا توجد لوحة مفاتيح.", + "enable": "إظهار الكرة العائمة", + "hint": "اسحب للتحريك، اسحب بسرعة لإيماءة، اضغط مطوّلاً للوحة وقائمة الإيماءات.", + "resetPosition": "إعادة تعيين الموضع", + "ballSize": "حجم الكرة", + "sizeSmall": "صغير", + "sizeMedium": "متوسط", + "sizeLarge": "كبير", + "gestureBindings": "ربط الإيماءات", + "bindingCleared": "غير مربوط", + "favoriteAgents": "الوكلاء المفضلون", + "favoriteAgentsHint": "اختر 2-3 وكلاء للتنقل بينهم بإيماءة المفضلين.", + "favoriteAgentsEmpty": "لا يوجد وكلاء متاحون", + "panelTitle": "إجراءات سريعة" } }, "SystemSettings": { @@ -1201,6 +1194,30 @@ "apiKeyHint": "يُمرَّر إلى CLI باسم KIRO_API_KEY. اتركه فارغاً لاستخدام الحساب الذي سجلت الدخول به عبر kiro-cli login؛ لا يلزم إلا حيث يتعذر تسجيل الدخول بالمتصفح (الخوادم، Docker).", "saveConfig": "حفظ إعدادات Kiro" }, + "trae": { + "configManagement": "إدارة الإعدادات", + "configDescription": "يتوفر TRAE CLI 2.0 لعملاء الخطة الرئيسية من TRAE Enterprise فقط. يشغّل Codeg أداة traecli التي ثبّتّها، وهي تحفظ تسجيل الدخول والمهارات وخوادم MCP في ~/.trae. النموذج والوضع ومستوى الاستدلال ليست هنا: يُبلغ عنها TRAE CLI عبر ACP، لذا تديرها محددات المحرّر لكل جلسة.", + "signInTitle": "تسجيل الدخول", + "signInHint": "شغّل هذا الأمر مرة واحدة في الطرفية لتسجيل الدخول عبر SSO الخاص بمؤسستك. تعيد جلسات Codeg استخدام تسجيل الدخول هذا.", + "copyCommand": "نسخ الأمر", + "commandCopied": "تم نسخ الأمر", + "patLabel": "رمز الوصول الشخصي (اختياري)", + "patHint": "للأجهزة التي يتعذر فيها تسجيل الدخول بالمتصفح، مثل الخوادم أو تثبيتات Docker. يمرّره Codeg إلى TRAE CLI باسم TRAECLI_PERSONAL_ACCESS_TOKEN ويسجّل الدخول به عند بدء كل جلسة. اتركه فارغاً لاستخدام تسجيل الدخول من traecli login.", + "hostLabel": "عنوان API الخاص بالمؤسسة (اختياري)", + "hostHint": "فقط إذا كانت مؤسستك تشغّل TRAE على نطاقها الخاص. يُمرَّر باسم TRAECLI_HOST؛ اتركه فارغاً لاستخدام خدمة TRAE نفسها.", + "saveConfig": "حفظ إعدادات TRAE CLI" + }, + "commandCode": { + "configManagement": "إدارة الإعدادات", + "configDescription": "يشغّل Codeg واجهة Command Code CLI من حزمة npm المسماة command-code، وهي تحفظ تسجيل الدخول والمهارات وخوادم MCP في ~/.commandcode. النموذج والوضع ومستوى الاستدلال ليست هنا: يُبلغ عنها Command Code عبر ACP، لذا تديرها محددات المحرّر لكل جلسة.", + "signInTitle": "تسجيل الدخول", + "signInHint": "شغّل هذا الأمر مرة واحدة في الطرفية لتسجيل الدخول بحساب Command Code الخاص بك. تعيد جلسات Codeg استخدام تسجيل الدخول هذا.", + "copyCommand": "نسخ الأمر", + "commandCopied": "تم نسخ الأمر", + "apiKeyLabel": "مفتاح API (اختياري)", + "apiKeyHint": "للأجهزة التي يتعذر فيها تسجيل الدخول بالمتصفح، مثل الخوادم أو تثبيتات Docker. يُمرَّر إلى Command Code باسم COMMAND_CODE_API_KEY ويتقدّم على تسجيل الدخول من command-code login. اتركه فارغاً لاستخدام تسجيل الدخول ذاك.", + "saveConfig": "حفظ إعدادات Command Code" + }, "deepseek": { "configManagement": "إعدادات DeepSeek Harness", "configDescription": "نقطة النهاية والمفتاح متغيّرا بيئة يقرأهما deepseek-acp عند الإقلاع. أما النموذج ومستوى الاستدلال فمحدِّدان على مستوى الجلسة، ويُختاران من حقل الإدخال.", @@ -1696,6 +1713,10 @@ "saveAntigravityConfigFailed": "Failed to save Antigravity config", "kiroSaved": "تم حفظ إعدادات Kiro", "saveKiroFailed": "تعذّر حفظ إعدادات Kiro", + "traeSaved": "تم حفظ إعدادات TRAE CLI", + "saveTraeFailed": "تعذّر حفظ إعدادات TRAE CLI", + "commandCodeSaved": "تم حفظ إعدادات Command Code", + "saveCommandCodeFailed": "تعذّر حفظ إعدادات Command Code", "systemAgentAlreadyLatest": "{name} محدَّث بالفعل ({version})" }, "distribution": { @@ -2467,7 +2488,10 @@ "commits": "الالتزامات" }, "noFolderTitle": "لا يوجد مجلد مفتوح", - "noFolderHint": "افتح مجلدا لعرض محتواه هنا" + "noFolderHint": "افتح مجلدا لعرض محتواه هنا", + "openFullPage": "فتح في صفحة كاملة", + "back": "رجوع", + "noAutomations": "لا توجد أتمتة لهذا المشروع بعد" }, "windowControls": { "minimizeWindow": "تصغير النافذة", @@ -3366,6 +3390,7 @@ }, "messageInput": { "askAnything": "اسأل أي شيء...", + "promptSuggestion": "{suggestion} (اضغط Tab للاستخدام)", "removeAttachmentAria": "إزالة {name}", "attachFiles": "إرفاق ملفات", "addActions": "إضافة", @@ -3395,6 +3420,10 @@ "searchModelAria": "البحث عن النماذج", "modelListLabel": "النماذج", "recommendedBadge": "موصى به", + "recommendedValue": "موصى به: {value}", + "searchOptions": "البحث في الخيارات...", + "searchOptionsAria": "البحث في الخيارات", + "noOptions": "لم يتم العثور على خيارات", "noModels": "لم يتم العثور على نماذج", "cancel": "إلغاء", "send": "إرسال", @@ -4007,13 +4036,16 @@ }, "messageNav": { "title": "تنقل الرسائل", - "collapse": "طي تنقل الرسائل", "loadingEarlier": "جارٍ تحميل الرسائل السابقة…", "loadEarlier": "تحميل الرسائل السابقة", - "collapsedSummary": "الرسائل {count}", "fileCount": "{count, plural, one {# ملف} other {# ملفات}}", - "remove": "إزالة", - "noDiffDataAvailable": "لا توجد بيانات diff متاحة لـ {filePath}" + "subAgentCount": "{count, plural, one {# وكيل فرعي} other {# وكلاء فرعيين}}", + "showDiff": "إظهار تغييرات {filePath}", + "hideDiff": "إخفاء تغييرات {filePath}", + "openFile": "فتح {filePath}", + "collapse": "طي", + "currentMessage": "الرسالة الحالية", + "scrollToCurrent": "التمرير إلى الرسالة الحالية (#{ordinal})" }, "replyArtifacts": { "title": "الملفات المتغيرة", @@ -4071,6 +4103,9 @@ "resumeDelegation": "استئناف المهمة {task}", "resumeDelegationNoId": "استئناف مهمة مفوضة", "promptLabel": "التعليمات", + "ackRecorded": "سُجِّل في مهمة العمل", + "ackNotRecorded": "لم يُسجَّل في مهمة العمل", + "ackNotRecordedWithReason": "لم يُسجَّل في مهمة العمل: {reason}", "verdict": { "success": "ناجحة", "needs_review": "بحاجة إلى مراجعة", @@ -4188,6 +4223,18 @@ "stopDeclined": "تعذّر إيقاف هذه المهمة، ربما تكون قد انتهت بالفعل.", "stopFailed": "فشل إيقاف المهمة: {error}" }, + "remoteControl": { + "title": "التحكم عن بُعد مُفعّل", + "open": "فتح في المتصفح", + "copy": "نسخ الرابط", + "copied": "تم النسخ", + "qr": "رمز QR", + "qrLabel": "رمز QR لرابط التحكم عن بُعد لهذه الجلسة", + "qrHint": "امسحه بهاتفك لمتابعة هذه الجلسة في تطبيق Claude أو على claude.ai/code.", + "noUrl": "لم يُرجع Claude Code رابطًا لهذه الجلسة", + "turnOff": "إيقاف", + "turnOffBusy": "متاح بعد أن ينتهي Claude" + }, "proposedPlan": { "title": "الخطة المقترحة", "planning": "جارٍ التخطيط…" @@ -7365,13 +7412,6 @@ } }, "FloatingBall": { - "label": "الكرة العائمة", - "panelTitle": "إجراءات سريعة", - "settingsTitle": "الكرة العائمة", - "close": "إغلاق", - "resetPosition": "إعادة تعيين الموضع", - "cheatSheetTitle": "الإيماءات", - "hint": "اسحب الكرة لتحريكها. السحب السريع يشغّل إيماءة، والضغط المطوّل يفتح هذه اللوحة.", "actions": { "newConversation": "محادثة جديدة", "toggleSidebar": "تبديل الشريط الجانبي", @@ -7437,7 +7477,6 @@ "upLeftThenRight": "↖ ثم →", "upRightThenLeft": "↗ ثم ←" }, - "favoriteAgentsEmpty": "لا يوجد وكلاء متاحون", "toasts": { "renameNeedsSavedSession": "إعادة التسمية تعمل على الجلسات المحفوظة فقط — أرسل رسالة أولاً.", "sessionFolderNeedsWorkspace": "افتح مجلد مساحة العمل أولاً.", @@ -7450,6 +7489,14 @@ "noModesAvailable": "لا يوفر هذا الوكيل أي أوضاع.", "noSwitchTarget": "لا يوجد وضع للتبديل إليه (لم يتم العثور على وضع Plan أو الوصول الكامل)." }, + "label": "الكرة العائمة", + "panelTitle": "إجراءات سريعة", + "settingsTitle": "الكرة العائمة", + "close": "إغلاق", + "resetPosition": "إعادة تعيين الموضع", + "cheatSheetTitle": "الإيماءات", + "hint": "اسحب الكرة لتحريكها. السحب السريع يشغّل إيماءة، والضغط المطوّل يفتح هذه اللوحة.", + "favoriteAgentsEmpty": "لا يوجد وكلاء متاحون", "recentSessionDepth": "عمق تبديل الجلسات الأخيرة", "bindingCleared": "غير مربوط" } diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index cf581c7fb1..e7fbca7e6b 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -181,13 +181,6 @@ "sectionDescription": "Animationen für Ein-/Ausklappen, Tool-Aufrufe und Arbeitsstatus im Unterhaltungsbereich sowie sanftes Scrollen. Wenn deaktiviert, werden Änderungen sofort angezeigt. Ladeanzeigen drehen sich weiterhin.", "enable": "Chat-Animationen aktivieren" }, - "messageNav": { - "sectionTitle": "Nachrichten-Navigator", - "sectionDescription": "Position des Nachrichten-Navigators. Unten rechts wird er zu einer kompakten Karte mit einer Zeile pro Nutzernachricht über dem Eingabefeld.", - "positionStart": "Oben links (Standard)", - "positionBottomEnd": "Unten rechts (kompakt)", - "hint": "Plan- und Subagenten-Panels bleiben in beiden Fällen oben links." - }, "chatWidth": { "sectionTitle": "Chat-Breite", "sectionDescription": "Maximale Breite der Unterhaltungsspalte und des Eingabefelds. Sie lässt sich auch durch Ziehen der Chat-Ränder anpassen.", @@ -247,54 +240,6 @@ "credit": "Bilder von wallhaven.cc · nur SFW" } }, - "floatingBall": { - "sectionTitle": "Schwebekugel", - "sectionDescription": "Eine verschiebbare Kugel über dem Arbeitsbereich, die Wischgesten in Tastenkürzel verwandelt – gedacht für das Telefon ohne Tastatur.", - "enable": "Schwebekugel anzeigen", - "hint": "Ziehen zum Verschieben, schnelles Wischen für eine Geste, langes Drücken für Panel und Gestenübersicht.", - "resetPosition": "Position zurücksetzen", - "ballSize": "Kugelgröße", - "sizeSmall": "Klein", - "sizeMedium": "Mittel", - "sizeLarge": "Groß", - "gestureBindings": "Gestenzuordnung", - "bindingCleared": "Nicht zugewiesen", - "favoriteAgents": "Favoriten-Agenten", - "favoriteAgentsHint": "Wähle 2-3 Agenten, um sie mit dem Favoriten-Gest zu durchlaufen.", - "favoriteAgentsEmpty": "Keine Agenten verfügbar", - "panelTitle": "Schnellaktionen" - }, - "gestures": { - "tap": "Tippen", - "doubleTap": "Doppelt tippen", - "longPress": "Lange drücken", - "left": "←", - "right": "→", - "up": "↑", - "upLeft": "↖", - "upRight": "↗", - "upThenLeft": "↑ dann ←", - "upThenRight": "↑ dann →", - "upDown": "↑ dann ↓", - "leftThenRight": "← dann →", - "rightThenLeft": "→ dann ←", - "upLeftThenDown": "↖ dann ↓", - "upRightThenDown": "↗ dann ↓", - "down": "↓", - "downLeft": "↙", - "downRight": "↘", - "downUp": "↓ dann ↑", - "upThenDownRight": "↑ dann ↘", - "downThenUpRight": "↓ dann ↗", - "rightThenUp": "→ dann ↑", - "rightThenDown": "→ dann ↓", - "downThenRight": "↓ dann →", - "downThenLeft": "↓ dann ←", - "downRightThenUp": "↘ dann ↑", - "downLeftThenUp": "↙ dann ↑", - "upLeftThenRight": "↖ dann →", - "upRightThenLeft": "↗ dann ←" - }, "actions": { "newConversation": "Neue Unterhaltung", "toggleSidebar": "Seitenleiste umschalten", @@ -328,6 +273,54 @@ "focusModeSelector": "Modus-Auswahl", "togglePlanFullAccess": "Plan / Vollzugriff", "openCollaboration": "Kollaboration" + }, + "gestures": { + "tap": "Tippen", + "doubleTap": "Doppelt tippen", + "longPress": "Lange drücken", + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ dann ←", + "upThenRight": "↑ dann →", + "upDown": "↑ dann ↓", + "leftThenRight": "← dann →", + "rightThenLeft": "→ dann ←", + "upLeftThenDown": "↖ dann ↓", + "upRightThenDown": "↗ dann ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ dann ↑", + "upThenDownRight": "↑ dann ↘", + "downThenUpRight": "↓ dann ↗", + "rightThenUp": "→ dann ↑", + "rightThenDown": "→ dann ↓", + "downThenRight": "↓ dann →", + "downThenLeft": "↓ dann ←", + "downRightThenUp": "↘ dann ↑", + "downLeftThenUp": "↙ dann ↑", + "upLeftThenRight": "↖ dann →", + "upRightThenLeft": "↗ dann ←" + }, + "floatingBall": { + "sectionTitle": "Schwebekugel", + "sectionDescription": "Eine verschiebbare Kugel über dem Arbeitsbereich, die Wischgesten in Tastenkürzel verwandelt – gedacht für das Telefon ohne Tastatur.", + "enable": "Schwebekugel anzeigen", + "hint": "Ziehen zum Verschieben, schnelles Wischen für eine Geste, langes Drücken für Panel und Gestenübersicht.", + "resetPosition": "Position zurücksetzen", + "ballSize": "Kugelgröße", + "sizeSmall": "Klein", + "sizeMedium": "Mittel", + "sizeLarge": "Groß", + "gestureBindings": "Gestenzuordnung", + "bindingCleared": "Nicht zugewiesen", + "favoriteAgents": "Favoriten-Agenten", + "favoriteAgentsHint": "Wähle 2-3 Agenten, um sie mit dem Favoriten-Gest zu durchlaufen.", + "favoriteAgentsEmpty": "Keine Agenten verfügbar", + "panelTitle": "Schnellaktionen" } }, "SystemSettings": { @@ -1201,6 +1194,30 @@ "apiKeyHint": "Wird der CLI als KIRO_API_KEY übergeben. Leer lassen, um das per kiro-cli login angemeldete Konto zu nutzen; nur nötig, wo keine Browser-Anmeldung möglich ist (Server, Docker).", "saveConfig": "Kiro-Konfiguration speichern" }, + "trae": { + "configManagement": "Konfiguration", + "configDescription": "TRAE CLI 2.0 steht nur Kunden des Flaggschiff-Tarifs von TRAE Enterprise zur Verfügung. Codeg startet das von dir installierte traecli, das Anmeldung, Skills und MCP-Server in ~/.trae speichert. Modell, Modus und Denkaufwand stehen nicht hier: TRAE CLI meldet sie über ACP, daher verwalten die Auswahlfelder im Eingabebereich sie pro Sitzung.", + "signInTitle": "Anmelden", + "signInHint": "Führe dies einmal in einem Terminal aus, um dich mit dem SSO deines Unternehmens anzumelden. Sitzungen in Codeg verwenden diese Anmeldung wieder.", + "copyCommand": "Befehl kopieren", + "commandCopied": "Befehl kopiert", + "patLabel": "Persönliches Zugriffstoken (optional)", + "patHint": "Für Rechner, auf denen keine Browser-Anmeldung möglich ist, etwa Server oder Docker-Installationen. Codeg übergibt es TRAE CLI als TRAECLI_PERSONAL_ACCESS_TOKEN und meldet sich bei jedem Sitzungsstart damit an. Leer lassen, um die Anmeldung aus traecli login zu nutzen.", + "hostLabel": "Enterprise-API-URL (optional)", + "hostHint": "Nur wenn deine Organisation TRAE unter einer eigenen Domain betreibt. Wird als TRAECLI_HOST übergeben; leer lassen für den Dienst von TRAE.", + "saveConfig": "TRAE CLI-Konfiguration speichern" + }, + "commandCode": { + "configManagement": "Konfiguration", + "configDescription": "Codeg startet die Command Code CLI aus dem npm-Paket command-code, die Anmeldung, Skills und MCP-Server in ~/.commandcode speichert. Modell, Modus und Denkaufwand stehen nicht hier: Command Code meldet sie über ACP, daher verwalten die Auswahlfelder im Eingabebereich sie pro Sitzung.", + "signInTitle": "Anmelden", + "signInHint": "Führe dies einmal in einem Terminal aus, um dich mit deinem Command Code-Konto anzumelden. Sitzungen in Codeg verwenden diese Anmeldung wieder.", + "copyCommand": "Befehl kopieren", + "commandCopied": "Befehl kopiert", + "apiKeyLabel": "API-Schlüssel (optional)", + "apiKeyHint": "Für Rechner, auf denen keine Browser-Anmeldung möglich ist, etwa Server oder Docker-Installationen. Wird Command Code als COMMAND_CODE_API_KEY übergeben und hat Vorrang vor der Anmeldung aus command-code login. Leer lassen, um diese Anmeldung zu nutzen.", + "saveConfig": "Command Code-Konfiguration speichern" + }, "deepseek": { "configManagement": "DeepSeek-Harness-Konfiguration", "configDescription": "Endpunkt und Schlüssel sind Umgebungsvariablen, die deepseek-acp beim Start liest. Modell und Denkstufe sind Auswahlfelder pro Sitzung — sie werden im Eingabefeld gewählt.", @@ -1696,6 +1713,10 @@ "saveAntigravityConfigFailed": "Failed to save Antigravity config", "kiroSaved": "Kiro-Konfiguration gespeichert", "saveKiroFailed": "Kiro-Konfiguration konnte nicht gespeichert werden", + "traeSaved": "TRAE CLI-Konfiguration gespeichert", + "saveTraeFailed": "TRAE CLI-Konfiguration konnte nicht gespeichert werden", + "commandCodeSaved": "Command Code-Konfiguration gespeichert", + "saveCommandCodeFailed": "Command Code-Konfiguration konnte nicht gespeichert werden", "systemAgentAlreadyLatest": "{name} ist bereits aktuell ({version})" }, "distribution": { @@ -2467,7 +2488,10 @@ "commits": "Einträge" }, "noFolderTitle": "Kein Ordner geöffnet", - "noFolderHint": "Öffnen Sie einen Ordner, um seinen Inhalt hier zu sehen" + "noFolderHint": "Öffnen Sie einen Ordner, um seinen Inhalt hier zu sehen", + "openFullPage": "Als ganze Seite öffnen", + "back": "Zurück", + "noAutomations": "Für dieses Projekt gibt es noch keine Automatisierungen" }, "windowControls": { "minimizeWindow": "Fenster minimieren", @@ -3366,6 +3390,7 @@ }, "messageInput": { "askAnything": "Fragen Sie alles...", + "promptSuggestion": "{suggestion} (Tab zum Übernehmen)", "removeAttachmentAria": "{name} entfernen", "attachFiles": "Dateien anhängen", "addActions": "Hinzufügen", @@ -3395,6 +3420,10 @@ "searchModelAria": "Modelle suchen", "modelListLabel": "Modelle", "recommendedBadge": "Empfohlen", + "recommendedValue": "Empfohlen: {value}", + "searchOptions": "Optionen suchen...", + "searchOptionsAria": "Optionen suchen", + "noOptions": "Keine Optionen gefunden", "noModels": "Keine Modelle gefunden", "cancel": "Abbrechen", "send": "Senden", @@ -4007,13 +4036,16 @@ }, "messageNav": { "title": "Nachrichtennavigation", - "collapse": "Nachrichtennavigation ausblenden", "loadingEarlier": "Frühere Nachrichten werden geladen…", "loadEarlier": "Frühere Nachrichten laden", - "collapsedSummary": "Nachrichten {count}", "fileCount": "{count, plural, one {# Datei} other {# Dateien}}", - "remove": "Entfernen", - "noDiffDataAvailable": "Keine Diff-Daten verfügbar für {filePath}" + "subAgentCount": "{count, plural, one {# Subagent} other {# Subagenten}}", + "showDiff": "Änderungen an {filePath} anzeigen", + "hideDiff": "Änderungen an {filePath} ausblenden", + "openFile": "{filePath} öffnen", + "collapse": "Einklappen", + "currentMessage": "Aktuelle Nachricht", + "scrollToCurrent": "Zur aktuellen Nachricht (#{ordinal}) scrollen" }, "replyArtifacts": { "title": "Geänderte Dateien", @@ -4071,6 +4103,9 @@ "resumeDelegation": "Aufgabe {task} wird fortgesetzt", "resumeDelegationNoId": "Delegierte Aufgabe wird fortgesetzt", "promptLabel": "Anweisung", + "ackRecorded": "In der Arbeitsaufgabe erfasst", + "ackNotRecorded": "Nicht in der Arbeitsaufgabe erfasst", + "ackNotRecordedWithReason": "Nicht in der Arbeitsaufgabe erfasst: {reason}", "verdict": { "success": "erfolgreich", "needs_review": "Prüfung nötig", @@ -4188,6 +4223,18 @@ "stopDeclined": "Diese Aufgabe konnte nicht gestoppt werden – sie ist möglicherweise bereits beendet.", "stopFailed": "Aufgabe konnte nicht gestoppt werden: {error}" }, + "remoteControl": { + "title": "Fernsteuerung aktiv", + "open": "Im Browser öffnen", + "copy": "Link kopieren", + "copied": "Kopiert", + "qr": "QR-Code", + "qrLabel": "QR-Code des Fernsteuerungs-Links dieser Sitzung", + "qrHint": "Mit dem Handy scannen, um diese Sitzung in der Claude-App oder auf claude.ai/code fortzusetzen.", + "noUrl": "Claude Code hat keinen Link für diese Sitzung zurückgegeben", + "turnOff": "Ausschalten", + "turnOffBusy": "Verfügbar, sobald Claude fertig ist" + }, "proposedPlan": { "title": "Vorgeschlagener Plan", "planning": "Wird geplant…" @@ -7365,13 +7412,6 @@ } }, "FloatingBall": { - "label": "Schwebekugel", - "panelTitle": "Schnellaktionen", - "settingsTitle": "Schwebekugel", - "close": "Schließen", - "resetPosition": "Position zurücksetzen", - "cheatSheetTitle": "Gesten", - "hint": "Ziehe die Kugel, um sie zu verschieben. Schnelles Wischen löst eine Geste aus, langes Drücken öffnet dieses Panel.", "actions": { "newConversation": "Neue Unterhaltung", "toggleSidebar": "Seitenleiste umschalten", @@ -7437,7 +7477,6 @@ "upLeftThenRight": "↖ dann →", "upRightThenLeft": "↗ dann ←" }, - "favoriteAgentsEmpty": "Keine Agenten verfügbar", "toasts": { "renameNeedsSavedSession": "Umbenennen gilt nur für gespeicherte Sitzungen – senden Sie zuerst eine Nachricht.", "sessionFolderNeedsWorkspace": "Öffnen Sie zuerst einen Arbeitsbereich-Ordner.", @@ -7450,6 +7489,14 @@ "noModesAvailable": "Dieser Agent stellt keine Modi bereit.", "noSwitchTarget": "Kein Modus zum Wechseln gefunden (Plan- oder Vollzugriffsmodus fehlt)." }, + "label": "Schwebekugel", + "panelTitle": "Schnellaktionen", + "settingsTitle": "Schwebekugel", + "close": "Schließen", + "resetPosition": "Position zurücksetzen", + "cheatSheetTitle": "Gesten", + "hint": "Ziehe die Kugel, um sie zu verschieben. Schnelles Wischen löst eine Geste aus, langes Drücken öffnet dieses Panel.", + "favoriteAgentsEmpty": "Keine Agenten verfügbar", "recentSessionDepth": "Umschalttiefe letzte Sitzungen", "bindingCleared": "Nicht zugewiesen" } diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 5a5cf2ac7b..6d48274d81 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -181,13 +181,6 @@ "sectionDescription": "Entrance, expand/collapse, tool-call and working-status animations in the conversation area, plus smooth scrolling. Turn off for instant, motion-free updates. Spinners keep rotating.", "enable": "Enable chat animations" }, - "messageNav": { - "sectionTitle": "Message navigator", - "sectionDescription": "Where the per-conversation message navigator sits. Bottom-right keeps it as a compact one-row-per-turn card above the composer, out of the reading column.", - "positionStart": "Top-left (default)", - "positionBottomEnd": "Bottom-right (compact)", - "hint": "Plan and sub-agent panels stay top-left either way." - }, "chatWidth": { "sectionTitle": "Chat width", "sectionDescription": "Maximum width of the conversation column and the composer. You can also drag the edges of the chat column to adjust it.", @@ -247,54 +240,6 @@ "credit": "Images from wallhaven.cc · SFW only" } }, - "floatingBall": { - "sectionTitle": "Floating Ball", - "sectionDescription": "A draggable ball over the workspace that turns swipes into shortcuts — built for the phone, where there is no keyboard.", - "enable": "Show floating ball", - "hint": "Drag to move; swipe quickly for a gesture; long press for the panel and cheat sheet.", - "resetPosition": "Reset position", - "ballSize": "Ball size", - "sizeSmall": "Small", - "sizeMedium": "Medium", - "sizeLarge": "Large", - "gestureBindings": "Gesture bindings", - "bindingCleared": "Unbound", - "favoriteAgents": "Favorite agents", - "favoriteAgentsHint": "Pick 2-3 agents to cycle through with the favorite-agents gesture.", - "favoriteAgentsEmpty": "No available agents", - "panelTitle": "Quick actions" - }, - "gestures": { - "tap": "Tap", - "doubleTap": "Double tap", - "longPress": "Long press", - "left": "←", - "right": "→", - "up": "↑", - "upLeft": "↖", - "upRight": "↗", - "upThenLeft": "↑ then ←", - "upThenRight": "↑ then →", - "upDown": "↑ then ↓", - "leftThenRight": "← then →", - "rightThenLeft": "→ then ←", - "upLeftThenDown": "↖ then ↓", - "upRightThenDown": "↗ then ↓", - "down": "↓", - "downLeft": "↙", - "downRight": "↘", - "downUp": "↓ then ↑", - "upThenDownRight": "↑ then ↘", - "downThenUpRight": "↓ then ↗", - "rightThenUp": "→ then ↑", - "rightThenDown": "→ then ↓", - "downThenRight": "↓ then →", - "downThenLeft": "↓ then ←", - "downRightThenUp": "↘ then ↑", - "downLeftThenUp": "↙ then ↑", - "upLeftThenRight": "↖ then →", - "upRightThenLeft": "↗ then ←" - }, "actions": { "newConversation": "New conversation", "toggleSidebar": "Toggle sidebar", @@ -328,6 +273,54 @@ "focusModeSelector": "Mode selector", "togglePlanFullAccess": "Plan / full access", "openCollaboration": "Collaboration" + }, + "gestures": { + "tap": "Tap", + "doubleTap": "Double tap", + "longPress": "Long press", + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ then ←", + "upThenRight": "↑ then →", + "upDown": "↑ then ↓", + "leftThenRight": "← then →", + "rightThenLeft": "→ then ←", + "upLeftThenDown": "↖ then ↓", + "upRightThenDown": "↗ then ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ then ↑", + "upThenDownRight": "↑ then ↘", + "downThenUpRight": "↓ then ↗", + "rightThenUp": "→ then ↑", + "rightThenDown": "→ then ↓", + "downThenRight": "↓ then →", + "downThenLeft": "↓ then ←", + "downRightThenUp": "↘ then ↑", + "downLeftThenUp": "↙ then ↑", + "upLeftThenRight": "↖ then →", + "upRightThenLeft": "↗ then ←" + }, + "floatingBall": { + "sectionTitle": "Floating Ball", + "sectionDescription": "A draggable ball over the workspace that turns swipes into shortcuts — built for the phone, where there is no keyboard.", + "enable": "Show floating ball", + "hint": "Drag to move; swipe quickly for a gesture; long press for the panel and cheat sheet.", + "resetPosition": "Reset position", + "ballSize": "Ball size", + "sizeSmall": "Small", + "sizeMedium": "Medium", + "sizeLarge": "Large", + "gestureBindings": "Gesture bindings", + "bindingCleared": "Unbound", + "favoriteAgents": "Favorite agents", + "favoriteAgentsHint": "Pick 2-3 agents to cycle through with the favorite-agents gesture.", + "favoriteAgentsEmpty": "No available agents", + "panelTitle": "Quick actions" } }, "SystemSettings": { @@ -1201,6 +1194,30 @@ "apiKeyHint": "Passed to the CLI as KIRO_API_KEY. Leave empty to use the account you signed in with via kiro-cli login; an API key is only needed where no browser sign-in is possible (servers, Docker).", "saveConfig": "Save Kiro Config" }, + "trae": { + "configManagement": "Configuration", + "configDescription": "TRAE CLI 2.0 is available to customers on the flagship TRAE Enterprise plan. Codeg runs the traecli you installed, which keeps its sign-in, skills and MCP servers in ~/.trae. Model, mode and reasoning effort are not here: TRAE CLI reports them over ACP, so the composer's own selectors own them per session.", + "signInTitle": "Sign in", + "signInHint": "Run this once in a terminal to sign in with your enterprise SSO. Sessions in Codeg reuse that login.", + "copyCommand": "Copy command", + "commandCopied": "Command copied", + "patLabel": "Personal access token (optional)", + "patHint": "For a machine where browser sign-in is not possible, such as a server or Docker install. Codeg passes it to TRAE CLI as TRAECLI_PERSONAL_ACCESS_TOKEN and signs in with it whenever a session starts. Leave empty to use the login from traecli login.", + "hostLabel": "Enterprise API URL (optional)", + "hostHint": "Only if your organization runs TRAE on its own domain. Passed as TRAECLI_HOST; leave empty for TRAE's own service.", + "saveConfig": "Save TRAE CLI Config" + }, + "commandCode": { + "configManagement": "Configuration", + "configDescription": "Codeg runs the Command Code CLI from the command-code npm package, which keeps its sign-in, skills and MCP servers in ~/.commandcode. Model, mode and reasoning effort are not here: Command Code reports them over ACP, so the composer's own selectors own them per session.", + "signInTitle": "Sign in", + "signInHint": "Run this once in a terminal to sign in with your Command Code account. Sessions in Codeg reuse that login.", + "copyCommand": "Copy command", + "commandCopied": "Command copied", + "apiKeyLabel": "API key (optional)", + "apiKeyHint": "For a machine where browser sign-in is not possible, such as a server or Docker install. Passed to Command Code as COMMAND_CODE_API_KEY, which takes precedence over the login from command-code login. Leave empty to use that login.", + "saveConfig": "Save Command Code Config" + }, "deepseek": { "configManagement": "DeepSeek Harness Configuration", "configDescription": "The endpoint and key are environment variables deepseek-acp reads at launch. Model and reasoning effort are per-session selectors — pick them in the composer.", @@ -1696,6 +1713,10 @@ "saveAntigravityConfigFailed": "Failed to save Antigravity config", "kiroSaved": "Kiro config saved", "saveKiroFailed": "Failed to save Kiro config", + "traeSaved": "TRAE CLI config saved", + "saveTraeFailed": "Failed to save TRAE CLI config", + "commandCodeSaved": "Command Code config saved", + "saveCommandCodeFailed": "Failed to save Command Code config", "systemAgentAlreadyLatest": "{name} is already up to date ({version})" }, "distribution": { @@ -2467,7 +2488,10 @@ "commits": "Commits" }, "noFolderTitle": "No folder open", - "noFolderHint": "Open a folder to see its contents here" + "noFolderHint": "Open a folder to see its contents here", + "openFullPage": "Open full page", + "back": "Back", + "noAutomations": "No automations for this project yet" }, "windowControls": { "minimizeWindow": "Minimize window", @@ -3366,6 +3390,7 @@ }, "messageInput": { "askAnything": "Ask anything...", + "promptSuggestion": "{suggestion} (Tab to use)", "removeAttachmentAria": "Remove {name}", "attachFiles": "Attach files", "attachLocalUpload": "Upload local file", @@ -3397,6 +3422,10 @@ "searchModelAria": "Search models", "modelListLabel": "Models", "recommendedBadge": "Recommended", + "recommendedValue": "Recommended: {value}", + "searchOptions": "Search options...", + "searchOptionsAria": "Search options", + "noOptions": "No options found", "noModels": "No models found", "cancel": "Cancel", "send": "Send", @@ -4007,13 +4036,16 @@ }, "messageNav": { "title": "Message navigation", - "collapse": "Collapse message navigation", "loadingEarlier": "Loading earlier messages…", "loadEarlier": "Load earlier messages", - "collapsedSummary": "Messages {count}", "fileCount": "{count, plural, one {# file} other {# files}}", - "remove": "Remove", - "noDiffDataAvailable": "No diff data available for {filePath}" + "subAgentCount": "{count, plural, one {# sub-agent} other {# sub-agents}}", + "showDiff": "Show the changes to {filePath}", + "hideDiff": "Hide the changes to {filePath}", + "openFile": "Open {filePath}", + "collapse": "Collapse", + "currentMessage": "Current message", + "scrollToCurrent": "Scroll to the current message (#{ordinal})" }, "replyArtifacts": { "title": "Files changed", @@ -4071,6 +4103,9 @@ "resumeDelegation": "Resuming task {task}", "resumeDelegationNoId": "Resuming a delegated task", "promptLabel": "Prompt", + "ackRecorded": "Recorded on the work task", + "ackNotRecorded": "Not recorded on the work task", + "ackNotRecordedWithReason": "Not recorded on the work task: {reason}", "verdict": { "success": "success", "needs_review": "needs review", @@ -4188,6 +4223,18 @@ "stopDeclined": "That task could not be stopped — it may have already finished.", "stopFailed": "Failed to stop the task: {error}" }, + "remoteControl": { + "title": "Remote Control is on", + "open": "Open in browser", + "copy": "Copy link", + "copied": "Copied", + "qr": "QR code", + "qrLabel": "QR code for this session's Remote Control link", + "qrHint": "Scan with your phone to continue this session in the Claude app or on claude.ai/code.", + "noUrl": "Claude Code returned no link for this session", + "turnOff": "Turn off", + "turnOffBusy": "Available once Claude has finished" + }, "proposedPlan": { "title": "Proposed plan", "planning": "Planning…" @@ -7365,13 +7412,6 @@ } }, "FloatingBall": { - "label": "Floating ball", - "panelTitle": "Quick actions", - "settingsTitle": "Floating ball", - "close": "Close", - "resetPosition": "Reset position", - "cheatSheetTitle": "Gestures", - "hint": "Drag the ball to move it. Swipe quickly for a gesture; press and hold to open this panel.", "actions": { "newConversation": "New conversation", "toggleSidebar": "Toggle sidebar", @@ -7437,7 +7477,6 @@ "upLeftThenRight": "↖ then →", "upRightThenLeft": "↗ then ←" }, - "favoriteAgentsEmpty": "No available agents", "toasts": { "renameNeedsSavedSession": "Rename works on a saved session — send a message first.", "sessionFolderNeedsWorkspace": "Open a workspace folder first.", @@ -7450,6 +7489,14 @@ "noModesAvailable": "This agent does not expose any modes.", "noSwitchTarget": "No mode to switch to (Plan or full-access mode not found)." }, + "label": "Floating ball", + "panelTitle": "Quick actions", + "settingsTitle": "Floating ball", + "close": "Close", + "resetPosition": "Reset position", + "cheatSheetTitle": "Gestures", + "hint": "Drag the ball to move it. Swipe quickly for a gesture; press and hold to open this panel.", + "favoriteAgentsEmpty": "No available agents", "recentSessionDepth": "Recent-session switch depth", "bindingCleared": "Unbound" } diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index fa70d2dc1b..e0e81639c7 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -181,13 +181,6 @@ "sectionDescription": "Animaciones de plegado/desplegado, llamadas a herramientas y estado de trabajo en el área de conversación, además del desplazamiento suave. Desactívalas para actualizaciones inmediatas. Los indicadores de carga siguen girando.", "enable": "Activar las animaciones del chat" }, - "messageNav": { - "sectionTitle": "Navegador de mensajes", - "sectionDescription": "Posición del navegador de mensajes. Abajo a la derecha se convierte en una tarjeta compacta con una fila por mensaje del usuario, sobre el cuadro de entrada.", - "positionStart": "Arriba a la izquierda (predeterminado)", - "positionBottomEnd": "Abajo a la derecha (compacto)", - "hint": "Los paneles de plan y de subagentes siguen arriba a la izquierda." - }, "chatWidth": { "sectionTitle": "Ancho del chat", "sectionDescription": "Ancho máximo de la columna de conversación y del cuadro de entrada. También puedes arrastrar los bordes del chat para ajustarlo.", @@ -247,54 +240,6 @@ "credit": "Imágenes de wallhaven.cc · solo SFW" } }, - "floatingBall": { - "sectionTitle": "Bola flotante", - "sectionDescription": "Una bola arrastrable sobre el espacio de trabajo que convierte los deslizamientos en atajos, pensada para el teléfono, donde no hay teclado.", - "enable": "Mostrar bola flotante", - "hint": "Arrastra para mover, desliza rápido para un gesto, mantén pulsado para el panel y la lista de gestos.", - "resetPosition": "Restablecer posición", - "ballSize": "Tamaño de la bola", - "sizeSmall": "Pequeño", - "sizeMedium": "Mediano", - "sizeLarge": "Grande", - "gestureBindings": "Asignación de gestos", - "bindingCleared": "Sin asignar", - "favoriteAgents": "Agentes favoritos", - "favoriteAgentsHint": "Elige 2-3 agentes para alternar con el gesto de favoritos.", - "favoriteAgentsEmpty": "No hay agentes disponibles", - "panelTitle": "Acciones rápidas" - }, - "gestures": { - "tap": "Toque", - "doubleTap": "Doble toque", - "longPress": "Mantener pulsado", - "left": "←", - "right": "→", - "up": "↑", - "upLeft": "↖", - "upRight": "↗", - "upThenLeft": "↑ luego ←", - "upThenRight": "↑ luego →", - "upDown": "↑ luego ↓", - "leftThenRight": "← luego →", - "rightThenLeft": "→ luego ←", - "upLeftThenDown": "↖ luego ↓", - "upRightThenDown": "↗ luego ↓", - "down": "↓", - "downLeft": "↙", - "downRight": "↘", - "downUp": "↓ luego ↑", - "upThenDownRight": "↑ luego ↘", - "downThenUpRight": "↓ luego ↗", - "rightThenUp": "→ luego ↑", - "rightThenDown": "→ luego ↓", - "downThenRight": "↓ luego →", - "downThenLeft": "↓ luego ←", - "downRightThenUp": "↘ luego ↑", - "downLeftThenUp": "↙ luego ↑", - "upLeftThenRight": "↖ luego →", - "upRightThenLeft": "↗ luego ←" - }, "actions": { "newConversation": "Nueva conversación", "toggleSidebar": "Mostrar u ocultar la barra lateral", @@ -328,6 +273,54 @@ "focusModeSelector": "Selector de modo", "togglePlanFullAccess": "Plan / acceso total", "openCollaboration": "Colaboración" + }, + "gestures": { + "tap": "Toque", + "doubleTap": "Doble toque", + "longPress": "Mantener pulsado", + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ luego ←", + "upThenRight": "↑ luego →", + "upDown": "↑ luego ↓", + "leftThenRight": "← luego →", + "rightThenLeft": "→ luego ←", + "upLeftThenDown": "↖ luego ↓", + "upRightThenDown": "↗ luego ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ luego ↑", + "upThenDownRight": "↑ luego ↘", + "downThenUpRight": "↓ luego ↗", + "rightThenUp": "→ luego ↑", + "rightThenDown": "→ luego ↓", + "downThenRight": "↓ luego →", + "downThenLeft": "↓ luego ←", + "downRightThenUp": "↘ luego ↑", + "downLeftThenUp": "↙ luego ↑", + "upLeftThenRight": "↖ luego →", + "upRightThenLeft": "↗ luego ←" + }, + "floatingBall": { + "sectionTitle": "Bola flotante", + "sectionDescription": "Una bola arrastrable sobre el espacio de trabajo que convierte los deslizamientos en atajos, pensada para el teléfono, donde no hay teclado.", + "enable": "Mostrar bola flotante", + "hint": "Arrastra para mover, desliza rápido para un gesto, mantén pulsado para el panel y la lista de gestos.", + "resetPosition": "Restablecer posición", + "ballSize": "Tamaño de la bola", + "sizeSmall": "Pequeño", + "sizeMedium": "Mediano", + "sizeLarge": "Grande", + "gestureBindings": "Asignación de gestos", + "bindingCleared": "Sin asignar", + "favoriteAgents": "Agentes favoritos", + "favoriteAgentsHint": "Elige 2-3 agentes para alternar con el gesto de favoritos.", + "favoriteAgentsEmpty": "No hay agentes disponibles", + "panelTitle": "Acciones rápidas" } }, "SystemSettings": { @@ -1201,6 +1194,30 @@ "apiKeyHint": "Se pasa a la CLI como KIRO_API_KEY. Déjala vacía para usar la cuenta con la que iniciaste sesión mediante kiro-cli login; solo hace falta donde no se puede iniciar sesión con el navegador (servidores, Docker).", "saveConfig": "Guardar configuración de Kiro" }, + "trae": { + "configManagement": "Configuración", + "configDescription": "TRAE CLI 2.0 solo está disponible para clientes del plan insignia de TRAE Enterprise. Codeg ejecuta el traecli que instalaste, que guarda su inicio de sesión, sus habilidades y sus servidores MCP en ~/.trae. El modelo, el modo y el esfuerzo de razonamiento no están aquí: TRAE CLI los informa por ACP, así que los selectores del compositor los gestionan en cada sesión.", + "signInTitle": "Iniciar sesión", + "signInHint": "Ejecuta esto una vez en una terminal para iniciar sesión con el SSO de tu empresa. Las sesiones de Codeg reutilizan ese inicio de sesión.", + "copyCommand": "Copiar comando", + "commandCopied": "Comando copiado", + "patLabel": "Token de acceso personal (opcional)", + "patHint": "Para una máquina donde no se puede iniciar sesión con el navegador, como un servidor o una instalación de Docker. Codeg lo pasa a TRAE CLI como TRAECLI_PERSONAL_ACCESS_TOKEN e inicia sesión con él cada vez que empieza una sesión. Déjalo vacío para usar el inicio de sesión de traecli login.", + "hostLabel": "URL de la API empresarial (opcional)", + "hostHint": "Solo si tu organización ejecuta TRAE en su propio dominio. Se pasa como TRAECLI_HOST; déjala vacía para usar el servicio de TRAE.", + "saveConfig": "Guardar configuración de TRAE CLI" + }, + "commandCode": { + "configManagement": "Configuración", + "configDescription": "Codeg ejecuta la CLI de Command Code del paquete npm command-code, que guarda su inicio de sesión, sus habilidades y sus servidores MCP en ~/.commandcode. El modelo, el modo y el esfuerzo de razonamiento no están aquí: Command Code los informa por ACP, así que los selectores del compositor los gestionan en cada sesión.", + "signInTitle": "Iniciar sesión", + "signInHint": "Ejecuta esto una vez en una terminal para iniciar sesión con tu cuenta de Command Code. Las sesiones de Codeg reutilizan ese inicio de sesión.", + "copyCommand": "Copiar comando", + "commandCopied": "Comando copiado", + "apiKeyLabel": "Clave de API (opcional)", + "apiKeyHint": "Para una máquina donde no se puede iniciar sesión con el navegador, como un servidor o una instalación de Docker. Se pasa a Command Code como COMMAND_CODE_API_KEY, que tiene prioridad sobre el inicio de sesión de command-code login. Déjala vacía para usar ese inicio de sesión.", + "saveConfig": "Guardar configuración de Command Code" + }, "deepseek": { "configManagement": "Configuración de DeepSeek Harness", "configDescription": "El endpoint y la clave son variables de entorno que deepseek-acp lee al arrancar. El modelo y el esfuerzo de razonamiento son selectores por sesión: se eligen en el campo de mensaje.", @@ -1696,6 +1713,10 @@ "saveAntigravityConfigFailed": "Failed to save Antigravity config", "kiroSaved": "Configuración de Kiro guardada", "saveKiroFailed": "No se pudo guardar la configuración de Kiro", + "traeSaved": "Configuración de TRAE CLI guardada", + "saveTraeFailed": "No se pudo guardar la configuración de TRAE CLI", + "commandCodeSaved": "Configuración de Command Code guardada", + "saveCommandCodeFailed": "No se pudo guardar la configuración de Command Code", "systemAgentAlreadyLatest": "{name} ya está al día ({version})" }, "distribution": { @@ -2467,7 +2488,10 @@ "commits": "Confirmaciones" }, "noFolderTitle": "No hay carpeta abierta", - "noFolderHint": "Abre una carpeta para ver su contenido aquí" + "noFolderHint": "Abre una carpeta para ver su contenido aquí", + "openFullPage": "Abrir en página completa", + "back": "Atrás", + "noAutomations": "Este proyecto aún no tiene automatizaciones" }, "windowControls": { "minimizeWindow": "Minimizar ventana", @@ -3366,6 +3390,7 @@ }, "messageInput": { "askAnything": "Pregunta lo que sea...", + "promptSuggestion": "{suggestion} (Tab para usarla)", "removeAttachmentAria": "Quitar {name}", "attachFiles": "Adjuntar archivos", "addActions": "Añadir", @@ -3395,6 +3420,10 @@ "searchModelAria": "Buscar modelos", "modelListLabel": "Modelos", "recommendedBadge": "Recomendado", + "recommendedValue": "Recomendado: {value}", + "searchOptions": "Buscar opciones...", + "searchOptionsAria": "Buscar opciones", + "noOptions": "No se encontraron opciones", "noModels": "No se encontraron modelos", "cancel": "Cancelar", "send": "Enviar", @@ -4007,13 +4036,16 @@ }, "messageNav": { "title": "Navegación de mensajes", - "collapse": "Contraer navegación de mensajes", "loadingEarlier": "Cargando mensajes anteriores…", "loadEarlier": "Cargar mensajes anteriores", - "collapsedSummary": "Mensajes {count}", "fileCount": "{count, plural, one {# archivo} other {# archivos}}", - "remove": "Quitar", - "noDiffDataAvailable": "No hay datos de diff disponibles para {filePath}" + "subAgentCount": "{count, plural, one {# subagente} other {# subagentes}}", + "showDiff": "Mostrar los cambios de {filePath}", + "hideDiff": "Ocultar los cambios de {filePath}", + "openFile": "Abrir {filePath}", + "collapse": "Contraer", + "currentMessage": "Mensaje actual", + "scrollToCurrent": "Desplazarse al mensaje actual (#{ordinal})" }, "replyArtifacts": { "title": "Archivos modificados", @@ -4071,6 +4103,9 @@ "resumeDelegation": "Reanudando la tarea {task}", "resumeDelegationNoId": "Reanudando una tarea delegada", "promptLabel": "Instrucción", + "ackRecorded": "Registrado en la tarea de trabajo", + "ackNotRecorded": "No registrado en la tarea de trabajo", + "ackNotRecordedWithReason": "No registrado en la tarea de trabajo: {reason}", "verdict": { "success": "correcta", "needs_review": "requiere revisión", @@ -4188,6 +4223,18 @@ "stopDeclined": "No se pudo detener esa tarea; puede que ya haya terminado.", "stopFailed": "No se pudo detener la tarea: {error}" }, + "remoteControl": { + "title": "Control remoto activado", + "open": "Abrir en el navegador", + "copy": "Copiar enlace", + "copied": "Copiado", + "qr": "Código QR", + "qrLabel": "Código QR del enlace de control remoto de esta sesión", + "qrHint": "Escanéalo con el teléfono para continuar esta sesión en la app de Claude o en claude.ai/code.", + "noUrl": "Claude Code no devolvió ningún enlace para esta sesión", + "turnOff": "Desactivar", + "turnOffBusy": "Disponible cuando Claude termine" + }, "proposedPlan": { "title": "Plan propuesto", "planning": "Planificando…" @@ -7365,13 +7412,6 @@ } }, "FloatingBall": { - "label": "Bola flotante", - "panelTitle": "Acciones rápidas", - "settingsTitle": "Bola flotante", - "close": "Cerrar", - "resetPosition": "Restablecer posición", - "cheatSheetTitle": "Gestos", - "hint": "Arrastra la bola para moverla. Un deslizamiento rápido activa un gesto; una pulsación larga abre este panel.", "actions": { "newConversation": "Nueva conversación", "toggleSidebar": "Mostrar u ocultar la barra lateral", @@ -7437,7 +7477,6 @@ "upLeftThenRight": "↖ luego →", "upRightThenLeft": "↗ luego ←" }, - "favoriteAgentsEmpty": "No hay agentes disponibles", "toasts": { "renameNeedsSavedSession": "Renombrar solo funciona en sesiones guardadas: envía un mensaje primero.", "sessionFolderNeedsWorkspace": "Abre antes una carpeta de espacio de trabajo.", @@ -7450,6 +7489,14 @@ "noModesAvailable": "Este agente no expone ningún modo.", "noSwitchTarget": "No hay modo al que cambiar (no se encontró modo Plan ni de acceso completo)." }, + "label": "Bola flotante", + "panelTitle": "Acciones rápidas", + "settingsTitle": "Bola flotante", + "close": "Cerrar", + "resetPosition": "Restablecer posición", + "cheatSheetTitle": "Gestos", + "hint": "Arrastra la bola para moverla. Un deslizamiento rápido activa un gesto; una pulsación larga abre este panel.", + "favoriteAgentsEmpty": "No hay agentes disponibles", "recentSessionDepth": "Profundidad de sesiones recientes", "bindingCleared": "Sin asignar" } diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 7a4b3a2930..9aa339486c 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -181,13 +181,6 @@ "sectionDescription": "Animations de pliage/dépliage, d'appels d'outils et d'état de travail dans la zone de conversation, ainsi que le défilement fluide. Désactivez-les pour des mises à jour instantanées. Les indicateurs de chargement continuent de tourner.", "enable": "Activer les animations du chat" }, - "messageNav": { - "sectionTitle": "Navigateur de messages", - "sectionDescription": "Position du navigateur de messages. En bas à droite, il devient une carte compacte avec une ligne par message utilisateur, au-dessus de la zone de saisie.", - "positionStart": "En haut à gauche (par défaut)", - "positionBottomEnd": "En bas à droite (compact)", - "hint": "Les panneaux de plan et de sous-agents restent en haut à gauche." - }, "chatWidth": { "sectionTitle": "Largeur du chat", "sectionDescription": "Largeur maximale de la colonne de conversation et de la zone de saisie. Vous pouvez aussi faire glisser les bords du chat pour l'ajuster.", @@ -247,54 +240,6 @@ "credit": "Images de wallhaven.cc · SFW uniquement" } }, - "floatingBall": { - "sectionTitle": "Boule flottante", - "sectionDescription": "Une boule déplaçable au-dessus de l’espace de travail qui transforme les balayages en raccourcis — pensée pour le téléphone, où il n’y a pas de clavier.", - "enable": "Afficher la boule flottante", - "hint": "Faites glisser pour déplacer, balayez rapidement pour un geste, appuyez longuement pour le panneau et la liste des gestes.", - "resetPosition": "Réinitialiser la position", - "ballSize": "Taille de la bille", - "sizeSmall": "Petite", - "sizeMedium": "Moyenne", - "sizeLarge": "Grande", - "gestureBindings": "Affectation des gestes", - "bindingCleared": "Non attribué", - "favoriteAgents": "Agents favoris", - "favoriteAgentsHint": "Choisissez 2-3 agents à parcourir avec le geste des favoris.", - "favoriteAgentsEmpty": "Aucun agent disponible", - "panelTitle": "Actions rapides" - }, - "gestures": { - "tap": "Touche", - "doubleTap": "Double touche", - "longPress": "Appui long", - "left": "←", - "right": "→", - "up": "↑", - "upLeft": "↖", - "upRight": "↗", - "upThenLeft": "↑ puis ←", - "upThenRight": "↑ puis →", - "upDown": "↑ puis ↓", - "leftThenRight": "← puis →", - "rightThenLeft": "→ puis ←", - "upLeftThenDown": "↖ puis ↓", - "upRightThenDown": "↗ puis ↓", - "down": "↓", - "downLeft": "↙", - "downRight": "↘", - "downUp": "↓ puis ↑", - "upThenDownRight": "↑ puis ↘", - "downThenUpRight": "↓ puis ↗", - "rightThenUp": "→ puis ↑", - "rightThenDown": "→ puis ↓", - "downThenRight": "↓ puis →", - "downThenLeft": "↓ puis ←", - "downRightThenUp": "↘ puis ↑", - "downLeftThenUp": "↙ puis ↑", - "upLeftThenRight": "↖ puis →", - "upRightThenLeft": "↗ puis ←" - }, "actions": { "newConversation": "Nouvelle conversation", "toggleSidebar": "Afficher ou masquer la barre latérale", @@ -328,6 +273,54 @@ "focusModeSelector": "Sélecteur de mode", "togglePlanFullAccess": "Plan / accès complet", "openCollaboration": "Collaboration" + }, + "gestures": { + "tap": "Touche", + "doubleTap": "Double touche", + "longPress": "Appui long", + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ puis ←", + "upThenRight": "↑ puis →", + "upDown": "↑ puis ↓", + "leftThenRight": "← puis →", + "rightThenLeft": "→ puis ←", + "upLeftThenDown": "↖ puis ↓", + "upRightThenDown": "↗ puis ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ puis ↑", + "upThenDownRight": "↑ puis ↘", + "downThenUpRight": "↓ puis ↗", + "rightThenUp": "→ puis ↑", + "rightThenDown": "→ puis ↓", + "downThenRight": "↓ puis →", + "downThenLeft": "↓ puis ←", + "downRightThenUp": "↘ puis ↑", + "downLeftThenUp": "↙ puis ↑", + "upLeftThenRight": "↖ puis →", + "upRightThenLeft": "↗ puis ←" + }, + "floatingBall": { + "sectionTitle": "Boule flottante", + "sectionDescription": "Une boule déplaçable au-dessus de l’espace de travail qui transforme les balayages en raccourcis — pensée pour le téléphone, où il n’y a pas de clavier.", + "enable": "Afficher la boule flottante", + "hint": "Faites glisser pour déplacer, balayez rapidement pour un geste, appuyez longuement pour le panneau et la liste des gestes.", + "resetPosition": "Réinitialiser la position", + "ballSize": "Taille de la bille", + "sizeSmall": "Petite", + "sizeMedium": "Moyenne", + "sizeLarge": "Grande", + "gestureBindings": "Affectation des gestes", + "bindingCleared": "Non attribué", + "favoriteAgents": "Agents favoris", + "favoriteAgentsHint": "Choisissez 2-3 agents à parcourir avec le geste des favoris.", + "favoriteAgentsEmpty": "Aucun agent disponible", + "panelTitle": "Actions rapides" } }, "SystemSettings": { @@ -1201,6 +1194,30 @@ "apiKeyHint": "Transmise à la CLI en tant que KIRO_API_KEY. Laissez vide pour utiliser le compte connecté via kiro-cli login ; nécessaire seulement là où la connexion par navigateur est impossible (serveurs, Docker).", "saveConfig": "Enregistrer la configuration Kiro" }, + "trae": { + "configManagement": "Configuration", + "configDescription": "TRAE CLI 2.0 est réservé aux clients de l'offre phare de TRAE Enterprise. Codeg lance le traecli que vous avez installé, qui conserve sa connexion, ses compétences et ses serveurs MCP dans ~/.trae. Le modèle, le mode et l'effort de raisonnement ne sont pas ici : TRAE CLI les communique via ACP, donc les sélecteurs du compositeur les gèrent pour chaque session.", + "signInTitle": "Connexion", + "signInHint": "Exécutez cette commande une fois dans un terminal pour vous connecter avec le SSO de votre entreprise. Les sessions de Codeg réutilisent cette connexion.", + "copyCommand": "Copier la commande", + "commandCopied": "Commande copiée", + "patLabel": "Jeton d'accès personnel (facultatif)", + "patHint": "Pour une machine où la connexion par navigateur est impossible, comme un serveur ou une installation Docker. Codeg le transmet à TRAE CLI en tant que TRAECLI_PERSONAL_ACCESS_TOKEN et se connecte avec lui au démarrage de chaque session. Laissez vide pour utiliser la connexion de traecli login.", + "hostLabel": "URL de l'API d'entreprise (facultative)", + "hostHint": "Uniquement si votre organisation exploite TRAE sur son propre domaine. Transmise en tant que TRAECLI_HOST ; laissez vide pour le service de TRAE.", + "saveConfig": "Enregistrer la configuration TRAE CLI" + }, + "commandCode": { + "configManagement": "Configuration", + "configDescription": "Codeg lance la CLI Command Code du paquet npm command-code, qui conserve sa connexion, ses compétences et ses serveurs MCP dans ~/.commandcode. Le modèle, le mode et l'effort de raisonnement ne sont pas ici : Command Code les communique via ACP, donc les sélecteurs du compositeur les gèrent pour chaque session.", + "signInTitle": "Connexion", + "signInHint": "Exécutez cette commande une fois dans un terminal pour vous connecter avec votre compte Command Code. Les sessions de Codeg réutilisent cette connexion.", + "copyCommand": "Copier la commande", + "commandCopied": "Commande copiée", + "apiKeyLabel": "Clé d'API (facultative)", + "apiKeyHint": "Pour une machine où la connexion par navigateur est impossible, comme un serveur ou une installation Docker. Transmise à Command Code en tant que COMMAND_CODE_API_KEY, prioritaire sur la connexion de command-code login. Laissez vide pour utiliser cette connexion.", + "saveConfig": "Enregistrer la configuration Command Code" + }, "deepseek": { "configManagement": "Configuration de DeepSeek Harness", "configDescription": "Le point de terminaison et la clé sont des variables d'environnement que deepseek-acp lit au démarrage. Le modèle et le niveau de raisonnement sont des sélecteurs par session : ils se choisissent dans le champ de saisie.", @@ -1696,6 +1713,10 @@ "saveAntigravityConfigFailed": "Failed to save Antigravity config", "kiroSaved": "Configuration Kiro enregistrée", "saveKiroFailed": "Impossible d'enregistrer la configuration Kiro", + "traeSaved": "Configuration TRAE CLI enregistrée", + "saveTraeFailed": "Impossible d'enregistrer la configuration TRAE CLI", + "commandCodeSaved": "Configuration Command Code enregistrée", + "saveCommandCodeFailed": "Impossible d'enregistrer la configuration Command Code", "systemAgentAlreadyLatest": "{name} est déjà à jour ({version})" }, "distribution": { @@ -2467,7 +2488,10 @@ "commits": "Validations" }, "noFolderTitle": "Aucun dossier ouvert", - "noFolderHint": "Ouvrez un dossier pour voir son contenu ici" + "noFolderHint": "Ouvrez un dossier pour voir son contenu ici", + "openFullPage": "Ouvrir en pleine page", + "back": "Retour", + "noAutomations": "Aucune automatisation pour ce projet pour l’instant" }, "windowControls": { "minimizeWindow": "Minimiser la fenêtre", @@ -3366,6 +3390,7 @@ }, "messageInput": { "askAnything": "Posez n'importe quelle question...", + "promptSuggestion": "{suggestion} (Tab pour l’utiliser)", "removeAttachmentAria": "Retirer {name}", "attachFiles": "Joindre des fichiers", "addActions": "Ajouter", @@ -3395,6 +3420,10 @@ "searchModelAria": "Rechercher des modèles", "modelListLabel": "Modèles", "recommendedBadge": "Recommandé", + "recommendedValue": "Recommandé : {value}", + "searchOptions": "Rechercher des options...", + "searchOptionsAria": "Rechercher des options", + "noOptions": "Aucune option trouvée", "noModels": "Aucun modèle trouvé", "cancel": "Annuler", "send": "Envoyer", @@ -4007,13 +4036,16 @@ }, "messageNav": { "title": "Navigation des messages", - "collapse": "Réduire la navigation des messages", "loadingEarlier": "Chargement des messages précédents…", "loadEarlier": "Charger les messages précédents", - "collapsedSummary": "Messages {count}", "fileCount": "{count, plural, one {# fichier} other {# fichiers}}", - "remove": "Retirer", - "noDiffDataAvailable": "Aucune donnée de diff disponible pour {filePath}" + "subAgentCount": "{count, plural, one {# sous-agent} other {# sous-agents}}", + "showDiff": "Afficher les modifications de {filePath}", + "hideDiff": "Masquer les modifications de {filePath}", + "openFile": "Ouvrir {filePath}", + "collapse": "Réduire", + "currentMessage": "Message actuel", + "scrollToCurrent": "Faire défiler jusqu’au message actuel (#{ordinal})" }, "replyArtifacts": { "title": "Fichiers modifiés", @@ -4071,6 +4103,9 @@ "resumeDelegation": "Reprise de la tâche {task}", "resumeDelegationNoId": "Reprise d'une tâche déléguée", "promptLabel": "Instruction", + "ackRecorded": "Enregistré dans la tâche", + "ackNotRecorded": "Non enregistré dans la tâche", + "ackNotRecordedWithReason": "Non enregistré dans la tâche : {reason}", "verdict": { "success": "réussie", "needs_review": "à relire", @@ -4188,6 +4223,18 @@ "stopDeclined": "Impossible d'arrêter cette tâche : elle est peut-être déjà terminée.", "stopFailed": "Échec de l'arrêt de la tâche : {error}" }, + "remoteControl": { + "title": "Contrôle à distance activé", + "open": "Ouvrir dans le navigateur", + "copy": "Copier le lien", + "copied": "Copié", + "qr": "Code QR", + "qrLabel": "Code QR du lien de contrôle à distance de cette session", + "qrHint": "Scannez-le avec votre téléphone pour poursuivre cette session dans l'app Claude ou sur claude.ai/code.", + "noUrl": "Claude Code n'a renvoyé aucun lien pour cette session", + "turnOff": "Désactiver", + "turnOffBusy": "Disponible une fois que Claude a terminé" + }, "proposedPlan": { "title": "Plan proposé", "planning": "Planification…" @@ -7365,13 +7412,6 @@ } }, "FloatingBall": { - "label": "Boule flottante", - "panelTitle": "Actions rapides", - "settingsTitle": "Boule flottante", - "close": "Fermer", - "resetPosition": "Réinitialiser la position", - "cheatSheetTitle": "Gestes", - "hint": "Faites glisser la boule pour la déplacer. Un balayage rapide déclenche un geste, un appui long ouvre ce panneau.", "actions": { "newConversation": "Nouvelle conversation", "toggleSidebar": "Afficher ou masquer la barre latérale", @@ -7437,7 +7477,6 @@ "upLeftThenRight": "↖ puis →", "upRightThenLeft": "↗ puis ←" }, - "favoriteAgentsEmpty": "Aucun agent disponible", "toasts": { "renameNeedsSavedSession": "Renommer ne s'applique qu'aux sessions enregistrées — envoyez d'abord un message.", "sessionFolderNeedsWorkspace": "Ouvrez d'abord un dossier d'espace de travail.", @@ -7450,6 +7489,14 @@ "noModesAvailable": "Cet agent n'expose aucun mode.", "noSwitchTarget": "Aucun mode vers lequel basculer (mode Plan ou accès complet introuvable)." }, + "label": "Boule flottante", + "panelTitle": "Actions rapides", + "settingsTitle": "Boule flottante", + "close": "Fermer", + "resetPosition": "Réinitialiser la position", + "cheatSheetTitle": "Gestes", + "hint": "Faites glisser la boule pour la déplacer. Un balayage rapide déclenche un geste, un appui long ouvre ce panneau.", + "favoriteAgentsEmpty": "Aucun agent disponible", "recentSessionDepth": "Profondeur des sessions récentes", "bindingCleared": "Non attribué" } diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index d7e7556308..65081f0b20 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -181,13 +181,6 @@ "sectionDescription": "会話エリア内の折りたたみ/展開、ツール呼び出し、作業状態などのアニメーションとスムーズスクロールを制御します。オフにすると即時に更新されます。スピナーは回転し続けます。", "enable": "チャットのアニメーションを有効にする" }, - "messageNav": { - "sectionTitle": "メッセージナビ", - "sectionDescription": "会話のメッセージナビの位置。右下を選ぶと、ユーザーの発話ごとに 1 行のコンパクトなカードになり、入力欄の上に表示されます。", - "positionStart": "左上(既定)", - "positionBottomEnd": "右下(コンパクト)", - "hint": "プランパネルとサブエージェントパネルは常に左上に残ります。" - }, "chatWidth": { "sectionTitle": "チャット幅", "sectionDescription": "会話列と入力欄の最大幅です。チャット領域の左右の端をドラッグして調整することもできます。", @@ -247,54 +240,6 @@ "credit": "画像提供 wallhaven.cc · SFW のみ" } }, - "floatingBall": { - "sectionTitle": "フローティングボール", - "sectionDescription": "ワークスペース上に置くドラッグ可能なボール。スワイプをショートカットに変えます。キーボードのないスマートフォン向けです。", - "enable": "フローティングボールを表示", - "hint": "ドラッグで移動、素早いスワイプでジェスチャー、長押しでパネルとジェスチャー一覧を開きます。", - "resetPosition": "位置をリセット", - "ballSize": "ボールサイズ", - "sizeSmall": "小", - "sizeMedium": "中", - "sizeLarge": "大", - "gestureBindings": "ジェスチャー割り当て", - "bindingCleared": "未割り当て", - "favoriteAgents": "お気に入りエージェント", - "favoriteAgentsHint": "2〜3個のエージェントを選ぶと、お気に入りジェスチャーで切り替えられます。", - "favoriteAgentsEmpty": "利用可能なエージェントがありません", - "panelTitle": "クイック操作" - }, - "gestures": { - "tap": "タップ", - "doubleTap": "ダブルタップ", - "longPress": "長押し", - "left": "←", - "right": "→", - "up": "↑", - "upLeft": "↖", - "upRight": "↗", - "upThenLeft": "↑ その後 ←", - "upThenRight": "↑ その後 →", - "upDown": "↑ その後 ↓", - "leftThenRight": "← その後 →", - "rightThenLeft": "→ その後 ←", - "upLeftThenDown": "↖ その後 ↓", - "upRightThenDown": "↗ その後 ↓", - "down": "↓", - "downLeft": "↙", - "downRight": "↘", - "downUp": "↓ その後 ↑", - "upThenDownRight": "↑ その後 ↘", - "downThenUpRight": "↓ その後 ↗", - "rightThenUp": "→ その後 ↑", - "rightThenDown": "→ その後 ↓", - "downThenRight": "↓ その後 →", - "downThenLeft": "↓ その後 ←", - "downRightThenUp": "↘ その後 ↑", - "downLeftThenUp": "↙ その後 ↑", - "upLeftThenRight": "↖ その後 →", - "upRightThenLeft": "↗ その後 ←" - }, "actions": { "newConversation": "新しい会話", "toggleSidebar": "サイドバーの切り替え", @@ -328,6 +273,54 @@ "focusModeSelector": "モード選択", "togglePlanFullAccess": "Plan/フルアクセス切替", "openCollaboration": "コラボレーション" + }, + "gestures": { + "tap": "タップ", + "doubleTap": "ダブルタップ", + "longPress": "長押し", + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ その後 ←", + "upThenRight": "↑ その後 →", + "upDown": "↑ その後 ↓", + "leftThenRight": "← その後 →", + "rightThenLeft": "→ その後 ←", + "upLeftThenDown": "↖ その後 ↓", + "upRightThenDown": "↗ その後 ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ その後 ↑", + "upThenDownRight": "↑ その後 ↘", + "downThenUpRight": "↓ その後 ↗", + "rightThenUp": "→ その後 ↑", + "rightThenDown": "→ その後 ↓", + "downThenRight": "↓ その後 →", + "downThenLeft": "↓ その後 ←", + "downRightThenUp": "↘ その後 ↑", + "downLeftThenUp": "↙ その後 ↑", + "upLeftThenRight": "↖ その後 →", + "upRightThenLeft": "↗ その後 ←" + }, + "floatingBall": { + "sectionTitle": "フローティングボール", + "sectionDescription": "ワークスペース上に置くドラッグ可能なボール。スワイプをショートカットに変えます。キーボードのないスマートフォン向けです。", + "enable": "フローティングボールを表示", + "hint": "ドラッグで移動、素早いスワイプでジェスチャー、長押しでパネルとジェスチャー一覧を開きます。", + "resetPosition": "位置をリセット", + "ballSize": "ボールサイズ", + "sizeSmall": "小", + "sizeMedium": "中", + "sizeLarge": "大", + "gestureBindings": "ジェスチャー割り当て", + "bindingCleared": "未割り当て", + "favoriteAgents": "お気に入りエージェント", + "favoriteAgentsHint": "2〜3個のエージェントを選ぶと、お気に入りジェスチャーで切り替えられます。", + "favoriteAgentsEmpty": "利用可能なエージェントがありません", + "panelTitle": "クイック操作" } }, "SystemSettings": { @@ -1201,6 +1194,30 @@ "apiKeyHint": "KIRO_API_KEY として CLI に渡されます。空欄なら kiro-cli login でサインインしたアカウントを使います。ブラウザでサインインできない環境(サーバー、Docker)でのみ必要です。", "saveConfig": "Kiro 設定を保存" }, + "trae": { + "configManagement": "設定管理", + "configDescription": "TRAE CLI 2.0 は TRAE エンタープライズ版のフラッグシッププランのお客様のみ利用できます。Codeg はインストール済みの traecli を実行し、サインイン情報・スキル・MCP サーバーは ~/.trae に保存されます。モデル、モード、推論レベルはここにはありません。TRAE CLI が ACP で報告するため、入力欄のセレクターがセッションごとに管理します。", + "signInTitle": "サインイン", + "signInHint": "ターミナルでこのコマンドを一度実行し、エンタープライズ SSO でサインインしてください。Codeg のセッションはそのサインインを再利用します。", + "copyCommand": "コマンドをコピー", + "commandCopied": "コマンドをコピーしました", + "patLabel": "個人アクセストークン(任意)", + "patHint": "サーバーや Docker など、ブラウザでサインインできないマシン向けです。Codeg は TRAECLI_PERSONAL_ACCESS_TOKEN として TRAE CLI に渡し、セッション開始時にこのトークンでサインインします。空欄なら traecli login のサインインを使います。", + "hostLabel": "エンタープライズ API URL(任意)", + "hostHint": "組織が独自ドメインで TRAE を運用している場合のみ入力します。TRAECLI_HOST として渡されます。空欄なら TRAE の公式サービスを使います。", + "saveConfig": "TRAE CLI 設定を保存" + }, + "commandCode": { + "configManagement": "設定管理", + "configDescription": "Codeg は command-code npm パッケージの Command Code CLI を実行し、サインイン情報・スキル・MCP サーバーは ~/.commandcode に保存されます。モデル、モード、推論レベルはここにはありません。Command Code が ACP で報告するため、入力欄のセレクターがセッションごとに管理します。", + "signInTitle": "サインイン", + "signInHint": "ターミナルでこのコマンドを一度実行し、Command Code アカウントでサインインしてください。Codeg のセッションはそのサインインを再利用します。", + "copyCommand": "コマンドをコピー", + "commandCopied": "コマンドをコピーしました", + "apiKeyLabel": "API キー(任意)", + "apiKeyHint": "サーバーや Docker など、ブラウザでサインインできないマシン向けです。COMMAND_CODE_API_KEY として Command Code に渡され、command-code login のサインインより優先されます。空欄ならそのサインインを使います。", + "saveConfig": "Command Code 設定を保存" + }, "deepseek": { "configManagement": "DeepSeek Harness の設定", "configDescription": "エンドポイントと API キーは deepseek-acp が起動時に読み込む環境変数です。モデルと推論レベルはセッションごとのセレクターなので、入力欄で切り替えます。", @@ -1696,6 +1713,10 @@ "saveAntigravityConfigFailed": "Failed to save Antigravity config", "kiroSaved": "Kiro 設定を保存しました", "saveKiroFailed": "Kiro 設定の保存に失敗しました", + "traeSaved": "TRAE CLI 設定を保存しました", + "saveTraeFailed": "TRAE CLI 設定の保存に失敗しました", + "commandCodeSaved": "Command Code 設定を保存しました", + "saveCommandCodeFailed": "Command Code 設定の保存に失敗しました", "systemAgentAlreadyLatest": "{name} は最新です({version})" }, "distribution": { @@ -2467,7 +2488,10 @@ "commits": "コミット" }, "noFolderTitle": "開いているフォルダがありません", - "noFolderHint": "フォルダを開くとここに表示されます" + "noFolderHint": "フォルダを開くとここに表示されます", + "openFullPage": "フルページで開く", + "back": "戻る", + "noAutomations": "このプロジェクトにはまだオートメーションがありません" }, "windowControls": { "minimizeWindow": "ウィンドウを最小化", @@ -3366,6 +3390,7 @@ }, "messageInput": { "askAnything": "何でも質問してください...", + "promptSuggestion": "{suggestion}(Tab で入力)", "removeAttachmentAria": "{name} を削除", "attachFiles": "ファイルを添付", "addActions": "追加", @@ -3395,6 +3420,10 @@ "searchModelAria": "モデルを検索", "modelListLabel": "モデル", "recommendedBadge": "推奨", + "recommendedValue": "推奨: {value}", + "searchOptions": "オプションを検索...", + "searchOptionsAria": "オプションを検索", + "noOptions": "オプションが見つかりません", "noModels": "モデルが見つかりません", "cancel": "キャンセル", "send": "送信", @@ -4007,13 +4036,16 @@ }, "messageNav": { "title": "メッセージナビ", - "collapse": "メッセージナビを閉じる", "loadingEarlier": "以前のメッセージを読み込み中…", "loadEarlier": "以前のメッセージを読み込む", - "collapsedSummary": "メッセージ {count}", "fileCount": "{count, plural, one {# 個のファイル} other {# 個のファイル}}", - "remove": "削除", - "noDiffDataAvailable": "{filePath} の差分データがありません" + "subAgentCount": "{count, plural, one {# 個のサブエージェント} other {# 個のサブエージェント}}", + "showDiff": "{filePath} の変更を表示", + "hideDiff": "{filePath} の変更を隠す", + "openFile": "{filePath} を開く", + "collapse": "折りたたむ", + "currentMessage": "現在のメッセージ", + "scrollToCurrent": "現在のメッセージ(#{ordinal})へスクロール" }, "replyArtifacts": { "title": "変更されたファイル", @@ -4071,6 +4103,9 @@ "resumeDelegation": "タスク {task} を再開中", "resumeDelegationNoId": "委任タスクを再開中", "promptLabel": "プロンプト", + "ackRecorded": "作業タスクに記録しました", + "ackNotRecorded": "作業タスクに記録されていません", + "ackNotRecordedWithReason": "作業タスクに記録されていません: {reason}", "verdict": { "success": "成功", "needs_review": "要確認", @@ -4188,6 +4223,18 @@ "stopDeclined": "このタスクは停止できませんでした。すでに終了している可能性があります。", "stopFailed": "タスクの停止に失敗しました: {error}" }, + "remoteControl": { + "title": "リモートコントロール有効", + "open": "ブラウザで開く", + "copy": "リンクをコピー", + "copied": "コピーしました", + "qr": "QR コード", + "qrLabel": "このセッションのリモートコントロール用リンクの QR コード", + "qrHint": "スマートフォンでスキャンすると、Claude アプリまたは claude.ai/code でこのセッションを続けられます。", + "noUrl": "Claude Code はこのセッションのリンクを返しませんでした", + "turnOff": "オフにする", + "turnOffBusy": "Claude の作業が終わると使えます" + }, "proposedPlan": { "title": "提案された計画", "planning": "計画中…" @@ -7365,13 +7412,6 @@ } }, "FloatingBall": { - "label": "フローティングボール", - "panelTitle": "クイック操作", - "settingsTitle": "フローティングボール", - "close": "閉じる", - "resetPosition": "位置をリセット", - "cheatSheetTitle": "ジェスチャー", - "hint": "ボールをドラッグして移動します。素早くスワイプするとジェスチャー、長押しでこのパネルが開きます。", "actions": { "newConversation": "新しい会話", "toggleSidebar": "サイドバーの切り替え", @@ -7437,7 +7477,6 @@ "upLeftThenRight": "↖ その後 →", "upRightThenLeft": "↗ その後 ←" }, - "favoriteAgentsEmpty": "利用可能なエージェントがありません", "toasts": { "renameNeedsSavedSession": "名前の変更は保存済みセッションのみ有効です。まずメッセージを送信してください。", "sessionFolderNeedsWorkspace": "先にワークスペースフォルダを開いてください。", @@ -7450,6 +7489,14 @@ "noModesAvailable": "このエージェントには利用可能なモードがありません。", "noSwitchTarget": "切り替え先のモードがありません(Plan または完全アクセスモードが見つかりません)。" }, + "label": "フローティングボール", + "panelTitle": "クイック操作", + "settingsTitle": "フローティングボール", + "close": "閉じる", + "resetPosition": "位置をリセット", + "cheatSheetTitle": "ジェスチャー", + "hint": "ボールをドラッグして移動します。素早くスワイプするとジェスチャー、長押しでこのパネルが開きます。", + "favoriteAgentsEmpty": "利用可能なエージェントがありません", "recentSessionDepth": "最近セッション切替数", "bindingCleared": "未割り当て" } diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 6819fb8e8d..ace14b54c7 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -181,13 +181,6 @@ "sectionDescription": "대화 영역의 접기/펼치기, 도구 호출, 작업 상태 등의 애니메이션과 부드러운 스크롤을 제어합니다. 끄면 즉시 갱신됩니다. 스피너는 계속 회전합니다.", "enable": "채팅 애니메이션 사용" }, - "messageNav": { - "sectionTitle": "메시지 탐색기", - "sectionDescription": "대화 메시지 탐색기의 위치입니다. 오른쪽 아래를 선택하면 사용자 발화마다 한 줄인 컴팩트 카드로 입력창 위에 표시됩니다.", - "positionStart": "왼쪽 위(기본)", - "positionBottomEnd": "오른쪽 아래(컴팩트)", - "hint": "계획 및 하위 에이전트 패널은 항상 왼쪽 위에 남습니다." - }, "chatWidth": { "sectionTitle": "채팅 너비", "sectionDescription": "대화 영역과 입력창의 최대 너비입니다. 채팅 영역의 좌우 가장자리를 드래그해서 조정할 수도 있습니다.", @@ -247,54 +240,6 @@ "credit": "이미지 제공 wallhaven.cc · SFW만 표시" } }, - "floatingBall": { - "sectionTitle": "플로팅 볼", - "sectionDescription": "작업 공간 위의 드래그 가능한 볼로, 스와이프를 단축키로 바꿉니다. 키보드가 없는 휴대폰을 위한 기능입니다.", - "enable": "플로팅 볼 표시", - "hint": "드래그로 이동, 빠른 스와이프로 제스처, 길게 눌러 패널과 제스처 목록을 엽니다.", - "resetPosition": "위치 초기화", - "ballSize": "공 크기", - "sizeSmall": "작게", - "sizeMedium": "보통", - "sizeLarge": "크게", - "gestureBindings": "제스처 바인딩", - "bindingCleared": "미지정", - "favoriteAgents": "즐겨쓰는 에이전트", - "favoriteAgentsHint": "2~3개의 에이전트를 선택하면 즐겨쓰기 제스처로 순환 전환됩니다.", - "favoriteAgentsEmpty": "사용 가능한 에이전트가 없습니다", - "panelTitle": "빠른 작업" - }, - "gestures": { - "tap": "탭", - "doubleTap": "더블 탭", - "longPress": "길게 누름", - "left": "←", - "right": "→", - "up": "↑", - "upLeft": "↖", - "upRight": "↗", - "upThenLeft": "↑ 후 ←", - "upThenRight": "↑ 후 →", - "upDown": "↑ 후 ↓", - "leftThenRight": "← 후 →", - "rightThenLeft": "→ 후 ←", - "upLeftThenDown": "↖ 후 ↓", - "upRightThenDown": "↗ 후 ↓", - "down": "↓", - "downLeft": "↙", - "downRight": "↘", - "downUp": "↓ 후 ↑", - "upThenDownRight": "↑ 후 ↘", - "downThenUpRight": "↓ 후 ↗", - "rightThenUp": "→ 후 ↑", - "rightThenDown": "→ 후 ↓", - "downThenRight": "↓ 후 →", - "downThenLeft": "↓ 후 ←", - "downRightThenUp": "↘ 후 ↑", - "downLeftThenUp": "↙ 후 ↑", - "upLeftThenRight": "↖ 후 →", - "upRightThenLeft": "↗ 후 ←" - }, "actions": { "newConversation": "새 대화", "toggleSidebar": "사이드바 전환", @@ -328,6 +273,54 @@ "focusModeSelector": "모드 선택", "togglePlanFullAccess": "Plan/전체 권한 전환", "openCollaboration": "협업" + }, + "gestures": { + "tap": "탭", + "doubleTap": "더블 탭", + "longPress": "길게 누름", + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ 후 ←", + "upThenRight": "↑ 후 →", + "upDown": "↑ 후 ↓", + "leftThenRight": "← 후 →", + "rightThenLeft": "→ 후 ←", + "upLeftThenDown": "↖ 후 ↓", + "upRightThenDown": "↗ 후 ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ 후 ↑", + "upThenDownRight": "↑ 후 ↘", + "downThenUpRight": "↓ 후 ↗", + "rightThenUp": "→ 후 ↑", + "rightThenDown": "→ 후 ↓", + "downThenRight": "↓ 후 →", + "downThenLeft": "↓ 후 ←", + "downRightThenUp": "↘ 후 ↑", + "downLeftThenUp": "↙ 후 ↑", + "upLeftThenRight": "↖ 후 →", + "upRightThenLeft": "↗ 후 ←" + }, + "floatingBall": { + "sectionTitle": "플로팅 볼", + "sectionDescription": "작업 공간 위의 드래그 가능한 볼로, 스와이프를 단축키로 바꿉니다. 키보드가 없는 휴대폰을 위한 기능입니다.", + "enable": "플로팅 볼 표시", + "hint": "드래그로 이동, 빠른 스와이프로 제스처, 길게 눌러 패널과 제스처 목록을 엽니다.", + "resetPosition": "위치 초기화", + "ballSize": "공 크기", + "sizeSmall": "작게", + "sizeMedium": "보통", + "sizeLarge": "크게", + "gestureBindings": "제스처 바인딩", + "bindingCleared": "미지정", + "favoriteAgents": "즐겨쓰는 에이전트", + "favoriteAgentsHint": "2~3개의 에이전트를 선택하면 즐겨쓰기 제스처로 순환 전환됩니다.", + "favoriteAgentsEmpty": "사용 가능한 에이전트가 없습니다", + "panelTitle": "빠른 작업" } }, "SystemSettings": { @@ -1201,6 +1194,30 @@ "apiKeyHint": "KIRO_API_KEY로 CLI에 전달됩니다. 비워 두면 kiro-cli login으로 로그인한 계정을 사용합니다. 브라우저 로그인이 불가능한 환경(서버, Docker)에서만 필요합니다.", "saveConfig": "Kiro 구성 저장" }, + "trae": { + "configManagement": "구성 관리", + "configDescription": "TRAE CLI 2.0은 TRAE 엔터프라이즈 플래그십 요금제 고객만 사용할 수 있습니다. Codeg는 설치된 traecli를 실행하며, 로그인 정보·스킬·MCP 서버는 ~/.trae에 저장됩니다. 모델, 모드, 추론 강도는 여기에 없습니다. TRAE CLI가 ACP로 보고하므로 입력창의 선택기가 세션별로 관리합니다.", + "signInTitle": "로그인", + "signInHint": "터미널에서 이 명령을 한 번 실행해 엔터프라이즈 SSO로 로그인하세요. Codeg의 세션은 그 로그인을 재사용합니다.", + "copyCommand": "명령 복사", + "commandCopied": "명령을 복사했습니다", + "patLabel": "개인 액세스 토큰(선택)", + "patHint": "서버나 Docker처럼 브라우저 로그인이 불가능한 머신용입니다. Codeg는 TRAECLI_PERSONAL_ACCESS_TOKEN으로 TRAE CLI에 전달하고 세션을 시작할 때마다 이 토큰으로 로그인합니다. 비워 두면 traecli login의 로그인을 사용합니다.", + "hostLabel": "엔터프라이즈 API URL(선택)", + "hostHint": "조직이 자체 도메인에서 TRAE를 운영하는 경우에만 입력하세요. TRAECLI_HOST로 전달되며, 비워 두면 TRAE 공식 서비스를 사용합니다.", + "saveConfig": "TRAE CLI 구성 저장" + }, + "commandCode": { + "configManagement": "구성 관리", + "configDescription": "Codeg는 command-code npm 패키지의 Command Code CLI를 실행하며, 로그인 정보·스킬·MCP 서버는 ~/.commandcode에 저장됩니다. 모델, 모드, 추론 강도는 여기에 없습니다. Command Code가 ACP로 보고하므로 입력창의 선택기가 세션별로 관리합니다.", + "signInTitle": "로그인", + "signInHint": "터미널에서 이 명령을 한 번 실행해 Command Code 계정으로 로그인하세요. Codeg의 세션은 그 로그인을 재사용합니다.", + "copyCommand": "명령 복사", + "commandCopied": "명령을 복사했습니다", + "apiKeyLabel": "API 키(선택)", + "apiKeyHint": "서버나 Docker처럼 브라우저 로그인이 불가능한 머신용입니다. COMMAND_CODE_API_KEY로 Command Code에 전달되며 command-code login의 로그인보다 우선합니다. 비워 두면 그 로그인을 사용합니다.", + "saveConfig": "Command Code 구성 저장" + }, "deepseek": { "configManagement": "DeepSeek Harness 구성", "configDescription": "엔드포인트와 API 키는 deepseek-acp가 시작할 때 읽는 환경 변수입니다. 모델과 추론 강도는 세션별 선택기이므로 입력창에서 바꿉니다.", @@ -1696,6 +1713,10 @@ "saveAntigravityConfigFailed": "Failed to save Antigravity config", "kiroSaved": "Kiro 구성을 저장했습니다", "saveKiroFailed": "Kiro 구성을 저장하지 못했습니다", + "traeSaved": "TRAE CLI 구성을 저장했습니다", + "saveTraeFailed": "TRAE CLI 구성을 저장하지 못했습니다", + "commandCodeSaved": "Command Code 구성을 저장했습니다", + "saveCommandCodeFailed": "Command Code 구성을 저장하지 못했습니다", "systemAgentAlreadyLatest": "{name}은(는) 이미 최신 버전입니다({version})" }, "distribution": { @@ -2467,7 +2488,10 @@ "commits": "커밋" }, "noFolderTitle": "열린 폴더 없음", - "noFolderHint": "폴더를 열면 여기에 표시됩니다" + "noFolderHint": "폴더를 열면 여기에 표시됩니다", + "openFullPage": "전체 페이지에서 열기", + "back": "뒤로", + "noAutomations": "이 프로젝트에는 아직 자동화가 없습니다" }, "windowControls": { "minimizeWindow": "창 최소화", @@ -3366,6 +3390,7 @@ }, "messageInput": { "askAnything": "무엇이든 물어보세요...", + "promptSuggestion": "{suggestion} (Tab 키로 입력)", "removeAttachmentAria": "{name} 제거", "attachFiles": "파일 첨부", "addActions": "추가", @@ -3395,6 +3420,10 @@ "searchModelAria": "모델 검색", "modelListLabel": "모델", "recommendedBadge": "권장", + "recommendedValue": "권장: {value}", + "searchOptions": "옵션 검색...", + "searchOptionsAria": "옵션 검색", + "noOptions": "옵션을 찾을 수 없습니다", "noModels": "모델을 찾을 수 없습니다", "cancel": "취소", "send": "보내기", @@ -4007,13 +4036,16 @@ }, "messageNav": { "title": "메시지 탐색", - "collapse": "메시지 탐색 접기", "loadingEarlier": "이전 메시지를 불러오는 중…", "loadEarlier": "이전 메시지 불러오기", - "collapsedSummary": "메시지 {count}", "fileCount": "{count, plural, one {#개 파일} other {#개 파일}}", - "remove": "제거", - "noDiffDataAvailable": "{filePath}에 대한 diff 데이터가 없습니다" + "subAgentCount": "{count, plural, one {서브 에이전트 #개} other {서브 에이전트 #개}}", + "showDiff": "{filePath}의 변경 사항 보기", + "hideDiff": "{filePath}의 변경 사항 숨기기", + "openFile": "{filePath} 열기", + "collapse": "접기", + "currentMessage": "현재 메시지", + "scrollToCurrent": "현재 메시지(#{ordinal})로 스크롤" }, "replyArtifacts": { "title": "변경된 파일", @@ -4071,6 +4103,9 @@ "resumeDelegation": "작업 {task} 재개 중", "resumeDelegationNoId": "위임 작업 재개 중", "promptLabel": "프롬프트", + "ackRecorded": "작업 항목에 기록됨", + "ackNotRecorded": "작업 항목에 기록되지 않음", + "ackNotRecordedWithReason": "작업 항목에 기록되지 않음: {reason}", "verdict": { "success": "성공", "needs_review": "검토 필요", @@ -4188,6 +4223,18 @@ "stopDeclined": "해당 작업을 중지할 수 없습니다. 이미 완료되었을 수 있습니다.", "stopFailed": "작업 중지에 실패했습니다: {error}" }, + "remoteControl": { + "title": "원격 제어 켜짐", + "open": "브라우저에서 열기", + "copy": "링크 복사", + "copied": "복사됨", + "qr": "QR 코드", + "qrLabel": "이 세션의 원격 제어 링크 QR 코드", + "qrHint": "휴대폰으로 스캔하면 Claude 앱이나 claude.ai/code에서 이 세션을 이어갈 수 있습니다.", + "noUrl": "Claude Code가 이 세션의 링크를 반환하지 않았습니다", + "turnOff": "끄기", + "turnOffBusy": "Claude가 작업을 마치면 사용할 수 있습니다" + }, "proposedPlan": { "title": "제안된 계획", "planning": "계획 중…" @@ -7365,13 +7412,6 @@ } }, "FloatingBall": { - "label": "플로팅 볼", - "panelTitle": "빠른 작업", - "settingsTitle": "플로팅 볼", - "close": "닫기", - "resetPosition": "위치 초기화", - "cheatSheetTitle": "제스처", - "hint": "볼을 드래그해 이동하세요. 빠르게 스와이프하면 제스처, 길게 누르면 이 패널이 열립니다.", "actions": { "newConversation": "새 대화", "toggleSidebar": "사이드바 전환", @@ -7437,7 +7477,6 @@ "upLeftThenRight": "↖ 후 →", "upRightThenLeft": "↗ 후 ←" }, - "favoriteAgentsEmpty": "사용 가능한 에이전트가 없습니다", "toasts": { "renameNeedsSavedSession": "이름 바꾸기는 저장된 세션에만 적용됩니다. 먼저 메시지를 보내세요.", "sessionFolderNeedsWorkspace": "먼저 작업 공간 폴더를 여세요.", @@ -7450,6 +7489,14 @@ "noModesAvailable": "이 에이전트에는 사용 가능한 모드가 없습니다.", "noSwitchTarget": "전환할 모드가 없습니다(Plan 또는 전체 접근 모드를 찾을 수 없음)." }, + "label": "플로팅 볼", + "panelTitle": "빠른 작업", + "settingsTitle": "플로팅 볼", + "close": "닫기", + "resetPosition": "위치 초기화", + "cheatSheetTitle": "제스처", + "hint": "볼을 드래그해 이동하세요. 빠르게 스와이프하면 제스처, 길게 누르면 이 패널이 열립니다.", + "favoriteAgentsEmpty": "사용 가능한 에이전트가 없습니다", "recentSessionDepth": "최근 세션 전환 깊이", "bindingCleared": "미지정" } diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 9e1bc0c918..d4a6d6c288 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -181,13 +181,6 @@ "sectionDescription": "Animações de recolher/expandir, chamadas de ferramentas e estado de trabalho na área de conversa, além da rolagem suave. Desative para atualizações instantâneas. Os indicadores de carregamento continuam girando.", "enable": "Ativar as animações do chat" }, - "messageNav": { - "sectionTitle": "Navegador de mensagens", - "sectionDescription": "Posição do navegador de mensagens. No canto inferior direito vira um cartão compacto com uma linha por mensagem do usuário, acima do campo de entrada.", - "positionStart": "Canto superior esquerdo (padrão)", - "positionBottomEnd": "Canto inferior direito (compacto)", - "hint": "Os painéis de plano e de subagentes permanecem no canto superior esquerdo." - }, "chatWidth": { "sectionTitle": "Largura do chat", "sectionDescription": "Largura máxima da coluna de conversa e da caixa de entrada. Você também pode arrastar as bordas do chat para ajustá-la.", @@ -247,54 +240,6 @@ "credit": "Imagens de wallhaven.cc · apenas SFW" } }, - "floatingBall": { - "sectionTitle": "Bola flutuante", - "sectionDescription": "Uma bola arrastável sobre o espaço de trabalho que transforma deslizes em atalhos — feita para o telefone, onde não há teclado.", - "enable": "Mostrar bola flutuante", - "hint": "Arraste para mover, deslize rápido para um gesto, toque e segure para o painel e a lista de gestos.", - "resetPosition": "Redefinir posição", - "ballSize": "Tamanho da bola", - "sizeSmall": "Pequeno", - "sizeMedium": "Médio", - "sizeLarge": "Grande", - "gestureBindings": "Atribuição de gestos", - "bindingCleared": "Sem atribuição", - "favoriteAgents": "Agentes favoritos", - "favoriteAgentsHint": "Escolha 2-3 agentes para alternar com o gesto de favoritos.", - "favoriteAgentsEmpty": "Nenhum agente disponível", - "panelTitle": "Ações rápidas" - }, - "gestures": { - "tap": "Toque", - "doubleTap": "Toque duplo", - "longPress": "Pressão longa", - "left": "←", - "right": "→", - "up": "↑", - "upLeft": "↖", - "upRight": "↗", - "upThenLeft": "↑ depois ←", - "upThenRight": "↑ depois →", - "upDown": "↑ depois ↓", - "leftThenRight": "← depois →", - "rightThenLeft": "→ depois ←", - "upLeftThenDown": "↖ depois ↓", - "upRightThenDown": "↗ depois ↓", - "down": "↓", - "downLeft": "↙", - "downRight": "↘", - "downUp": "↓ depois ↑", - "upThenDownRight": "↑ depois ↘", - "downThenUpRight": "↓ depois ↗", - "rightThenUp": "→ depois ↑", - "rightThenDown": "→ depois ↓", - "downThenRight": "↓ depois →", - "downThenLeft": "↓ depois ←", - "downRightThenUp": "↘ depois ↑", - "downLeftThenUp": "↙ depois ↑", - "upLeftThenRight": "↖ depois →", - "upRightThenLeft": "↗ depois ←" - }, "actions": { "newConversation": "Nova conversa", "toggleSidebar": "Alternar barra lateral", @@ -328,6 +273,54 @@ "focusModeSelector": "Seletor de modo", "togglePlanFullAccess": "Plano / acesso total", "openCollaboration": "Colaboração" + }, + "gestures": { + "tap": "Toque", + "doubleTap": "Toque duplo", + "longPress": "Pressão longa", + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ depois ←", + "upThenRight": "↑ depois →", + "upDown": "↑ depois ↓", + "leftThenRight": "← depois →", + "rightThenLeft": "→ depois ←", + "upLeftThenDown": "↖ depois ↓", + "upRightThenDown": "↗ depois ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ depois ↑", + "upThenDownRight": "↑ depois ↘", + "downThenUpRight": "↓ depois ↗", + "rightThenUp": "→ depois ↑", + "rightThenDown": "→ depois ↓", + "downThenRight": "↓ depois →", + "downThenLeft": "↓ depois ←", + "downRightThenUp": "↘ depois ↑", + "downLeftThenUp": "↙ depois ↑", + "upLeftThenRight": "↖ depois →", + "upRightThenLeft": "↗ depois ←" + }, + "floatingBall": { + "sectionTitle": "Bola flutuante", + "sectionDescription": "Uma bola arrastável sobre o espaço de trabalho que transforma deslizes em atalhos — feita para o telefone, onde não há teclado.", + "enable": "Mostrar bola flutuante", + "hint": "Arraste para mover, deslize rápido para um gesto, toque e segure para o painel e a lista de gestos.", + "resetPosition": "Redefinir posição", + "ballSize": "Tamanho da bola", + "sizeSmall": "Pequeno", + "sizeMedium": "Médio", + "sizeLarge": "Grande", + "gestureBindings": "Atribuição de gestos", + "bindingCleared": "Sem atribuição", + "favoriteAgents": "Agentes favoritos", + "favoriteAgentsHint": "Escolha 2-3 agentes para alternar com o gesto de favoritos.", + "favoriteAgentsEmpty": "Nenhum agente disponível", + "panelTitle": "Ações rápidas" } }, "SystemSettings": { @@ -1201,6 +1194,30 @@ "apiKeyHint": "Passada à CLI como KIRO_API_KEY. Deixe vazia para usar a conta conectada via kiro-cli login; só é necessária onde não há login pelo navegador (servidores, Docker).", "saveConfig": "Salvar configuração do Kiro" }, + "trae": { + "configManagement": "Configuração", + "configDescription": "O TRAE CLI 2.0 está disponível apenas para clientes do plano principal do TRAE Enterprise. O Codeg executa o traecli que você instalou, que guarda o login, as skills e os servidores MCP em ~/.trae. Modelo, modo e esforço de raciocínio não ficam aqui: o TRAE CLI os informa via ACP, então os seletores do compositor os gerenciam em cada sessão.", + "signInTitle": "Entrar", + "signInHint": "Execute isto uma vez em um terminal para entrar com o SSO da sua empresa. As sessões do Codeg reutilizam esse login.", + "copyCommand": "Copiar comando", + "commandCopied": "Comando copiado", + "patLabel": "Token de acesso pessoal (opcional)", + "patHint": "Para uma máquina em que não é possível entrar pelo navegador, como um servidor ou uma instalação Docker. O Codeg o passa ao TRAE CLI como TRAECLI_PERSONAL_ACCESS_TOKEN e entra com ele sempre que uma sessão começa. Deixe vazio para usar o login do traecli login.", + "hostLabel": "URL da API corporativa (opcional)", + "hostHint": "Somente se a sua organização executa o TRAE em um domínio próprio. É passada como TRAECLI_HOST; deixe vazia para usar o serviço do TRAE.", + "saveConfig": "Salvar configuração do TRAE CLI" + }, + "commandCode": { + "configManagement": "Configuração", + "configDescription": "O Codeg executa a CLI do Command Code do pacote npm command-code, que guarda o login, as skills e os servidores MCP em ~/.commandcode. Modelo, modo e esforço de raciocínio não ficam aqui: o Command Code os informa via ACP, então os seletores do compositor os gerenciam em cada sessão.", + "signInTitle": "Entrar", + "signInHint": "Execute isto uma vez em um terminal para entrar com sua conta do Command Code. As sessões do Codeg reutilizam esse login.", + "copyCommand": "Copiar comando", + "commandCopied": "Comando copiado", + "apiKeyLabel": "Chave de API (opcional)", + "apiKeyHint": "Para uma máquina em que não é possível entrar pelo navegador, como um servidor ou uma instalação Docker. Passada ao Command Code como COMMAND_CODE_API_KEY, que tem prioridade sobre o login do command-code login. Deixe vazia para usar esse login.", + "saveConfig": "Salvar configuração do Command Code" + }, "deepseek": { "configManagement": "Configuração do DeepSeek Harness", "configDescription": "O endpoint e a chave são variáveis de ambiente que o deepseek-acp lê ao iniciar. O modelo e o nível de raciocínio são seletores por sessão — escolha-os no campo de mensagem.", @@ -1696,6 +1713,10 @@ "saveAntigravityConfigFailed": "Failed to save Antigravity config", "kiroSaved": "Configuração do Kiro salva", "saveKiroFailed": "Falha ao salvar a configuração do Kiro", + "traeSaved": "Configuração do TRAE CLI salva", + "saveTraeFailed": "Falha ao salvar a configuração do TRAE CLI", + "commandCodeSaved": "Configuração do Command Code salva", + "saveCommandCodeFailed": "Falha ao salvar a configuração do Command Code", "systemAgentAlreadyLatest": "{name} já está atualizado ({version})" }, "distribution": { @@ -2467,7 +2488,10 @@ "commits": "Confirmações" }, "noFolderTitle": "Nenhuma pasta aberta", - "noFolderHint": "Abra uma pasta para ver seu conteúdo aqui" + "noFolderHint": "Abra uma pasta para ver seu conteúdo aqui", + "openFullPage": "Abrir em página inteira", + "back": "Voltar", + "noAutomations": "Este projeto ainda não tem automações" }, "windowControls": { "minimizeWindow": "Minimizar janela", @@ -3366,6 +3390,7 @@ }, "messageInput": { "askAnything": "Pergunte qualquer coisa...", + "promptSuggestion": "{suggestion} (Tab para usar)", "removeAttachmentAria": "Remover {name}", "attachFiles": "Anexar arquivos", "addActions": "Adicionar", @@ -3395,6 +3420,10 @@ "searchModelAria": "Buscar modelos", "modelListLabel": "Modelos", "recommendedBadge": "Recomendado", + "recommendedValue": "Recomendado: {value}", + "searchOptions": "Buscar opções...", + "searchOptionsAria": "Buscar opções", + "noOptions": "Nenhuma opção encontrada", "noModels": "Nenhum modelo encontrado", "cancel": "Cancelar", "send": "Enviar", @@ -4007,13 +4036,16 @@ }, "messageNav": { "title": "Navegação de mensagens", - "collapse": "Recolher navegação de mensagens", "loadingEarlier": "Carregando mensagens anteriores…", "loadEarlier": "Carregar mensagens anteriores", - "collapsedSummary": "Mensagens {count}", "fileCount": "{count, plural, one {# arquivo} other {# arquivos}}", - "remove": "Remover", - "noDiffDataAvailable": "Nenhum dado de diff disponível para {filePath}" + "subAgentCount": "{count, plural, one {# subagente} other {# subagentes}}", + "showDiff": "Mostrar as alterações de {filePath}", + "hideDiff": "Ocultar as alterações de {filePath}", + "openFile": "Abrir {filePath}", + "collapse": "Recolher", + "currentMessage": "Mensagem atual", + "scrollToCurrent": "Rolar até a mensagem atual (#{ordinal})" }, "replyArtifacts": { "title": "Arquivos alterados", @@ -4071,6 +4103,9 @@ "resumeDelegation": "Retomando a tarefa {task}", "resumeDelegationNoId": "Retomando uma tarefa delegada", "promptLabel": "Instrução", + "ackRecorded": "Registrado na tarefa de trabalho", + "ackNotRecorded": "Não registrado na tarefa de trabalho", + "ackNotRecordedWithReason": "Não registrado na tarefa de trabalho: {reason}", "verdict": { "success": "sucesso", "needs_review": "requer revisão", @@ -4188,6 +4223,18 @@ "stopDeclined": "Não foi possível parar essa tarefa — ela pode já ter terminado.", "stopFailed": "Falha ao parar a tarefa: {error}" }, + "remoteControl": { + "title": "Controle remoto ativado", + "open": "Abrir no navegador", + "copy": "Copiar link", + "copied": "Copiado", + "qr": "Código QR", + "qrLabel": "Código QR do link de controle remoto desta sessão", + "qrHint": "Escaneie com o celular para continuar esta sessão no app do Claude ou em claude.ai/code.", + "noUrl": "O Claude Code não retornou nenhum link para esta sessão", + "turnOff": "Desativar", + "turnOffBusy": "Disponível quando o Claude terminar" + }, "proposedPlan": { "title": "Plano proposto", "planning": "A planear…" @@ -7365,13 +7412,6 @@ } }, "FloatingBall": { - "label": "Bola flutuante", - "panelTitle": "Ações rápidas", - "settingsTitle": "Bola flutuante", - "close": "Fechar", - "resetPosition": "Redefinir posição", - "cheatSheetTitle": "Gestos", - "hint": "Arraste a bola para movê-la. Um deslize rápido dispara um gesto; um toque longo abre este painel.", "actions": { "newConversation": "Nova conversa", "toggleSidebar": "Alternar barra lateral", @@ -7437,7 +7477,6 @@ "upLeftThenRight": "↖ depois →", "upRightThenLeft": "↗ depois ←" }, - "favoriteAgentsEmpty": "Nenhum agente disponível", "toasts": { "renameNeedsSavedSession": "Renomear só funciona em sessões salvas — envie uma mensagem primeiro.", "sessionFolderNeedsWorkspace": "Abra antes uma pasta de espaço de trabalho.", @@ -7450,6 +7489,14 @@ "noModesAvailable": "Este agente não expõe nenhum modo.", "noSwitchTarget": "Nenhum modo para alternar (modo Plan ou de acesso completo não encontrado)." }, + "label": "Bola flutuante", + "panelTitle": "Ações rápidas", + "settingsTitle": "Bola flutuante", + "close": "Fechar", + "resetPosition": "Redefinir posição", + "cheatSheetTitle": "Gestos", + "hint": "Arraste a bola para movê-la. Um deslize rápido dispara um gesto; um toque longo abre este painel.", + "favoriteAgentsEmpty": "Nenhum agente disponível", "recentSessionDepth": "Profundidade de sessões recentes", "bindingCleared": "Sem atribuição" } diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index db78c4acfc..5cd823eff0 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -181,13 +181,6 @@ "sectionDescription": "会话区域内的折叠/展开、工具调用、工作状态等动画,以及平滑滚动。关闭后界面立即更新、不再有过渡动画;加载转圈图标仍会旋转。", "enable": "启用聊天区域动画" }, - "messageNav": { - "sectionTitle": "消息导航", - "sectionDescription": "会话消息导航卡的位置。右下角会变成紧凑卡片:每次用户回话一行,贴在输入框上方,不挡阅读区。", - "positionStart": "左上(默认)", - "positionBottomEnd": "右下(紧凑)", - "hint": "计划面板与子智能体面板始终留在左上。" - }, "chatWidth": { "sectionTitle": "聊天区域宽度", "sectionDescription": "会话列与输入框的最大宽度。也可以直接拖动聊天区域左右两侧的边缘来调整。", @@ -247,54 +240,6 @@ "credit": "图片来源 wallhaven.cc · 仅 SFW 内容" } }, - "floatingBall": { - "sectionTitle": "悬浮球", - "sectionDescription": "工作区上的可拖动悬浮球,把滑动手势变成快捷键——为没有键盘的手机而设。", - "enable": "显示悬浮球", - "hint": "拖动可移动;快速滑动触发手势;长按打开面板与手势表。", - "resetPosition": "重置位置", - "ballSize": "球体大小", - "sizeSmall": "小", - "sizeMedium": "中", - "sizeLarge": "大", - "gestureBindings": "手势绑定", - "bindingCleared": "未绑定", - "favoriteAgents": "常用智能体", - "favoriteAgentsHint": "选择 2-3 个智能体,供「常用智能体」手势循环快切。", - "favoriteAgentsEmpty": "暂无可用智能体", - "panelTitle": "快捷操作" - }, - "gestures": { - "tap": "单击", - "doubleTap": "双击", - "longPress": "长按", - "left": "←", - "right": "→", - "up": "↑", - "upLeft": "↖", - "upRight": "↗", - "upThenLeft": "↑ 然后 ←", - "upThenRight": "↑ 然后 →", - "upDown": "↑ 然后 ↓", - "leftThenRight": "← 然后 →", - "rightThenLeft": "→ 然后 ←", - "upLeftThenDown": "↖ 然后 ↓", - "upRightThenDown": "↗ 然后 ↓", - "down": "↓", - "downLeft": "↙", - "downRight": "↘", - "downUp": "↓ 然后 ↑", - "upThenDownRight": "↑ 然后 ↘", - "downThenUpRight": "↓ 然后 ↗", - "rightThenUp": "→ 然后 ↑", - "rightThenDown": "→ 然后 ↓", - "downThenRight": "↓ 然后 →", - "downThenLeft": "↓ 然后 ←", - "downRightThenUp": "↘ 然后 ↑", - "downLeftThenUp": "↙ 然后 ↑", - "upLeftThenRight": "↖ 然后 →", - "upRightThenLeft": "↗ 然后 ←" - }, "actions": { "newConversation": "新建会话", "toggleSidebar": "切换侧栏", @@ -328,6 +273,54 @@ "focusModeSelector": "模式选择", "togglePlanFullAccess": "Plan/全权限切换", "openCollaboration": "协作" + }, + "gestures": { + "tap": "单击", + "doubleTap": "双击", + "longPress": "长按", + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ 然后 ←", + "upThenRight": "↑ 然后 →", + "upDown": "↑ 然后 ↓", + "leftThenRight": "← 然后 →", + "rightThenLeft": "→ 然后 ←", + "upLeftThenDown": "↖ 然后 ↓", + "upRightThenDown": "↗ 然后 ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ 然后 ↑", + "upThenDownRight": "↑ 然后 ↘", + "downThenUpRight": "↓ 然后 ↗", + "rightThenUp": "→ 然后 ↑", + "rightThenDown": "→ 然后 ↓", + "downThenRight": "↓ 然后 →", + "downThenLeft": "↓ 然后 ←", + "downRightThenUp": "↘ 然后 ↑", + "downLeftThenUp": "↙ 然后 ↑", + "upLeftThenRight": "↖ 然后 →", + "upRightThenLeft": "↗ 然后 ←" + }, + "floatingBall": { + "sectionTitle": "悬浮球", + "sectionDescription": "工作区上的可拖动悬浮球,把滑动手势变成快捷键——为没有键盘的手机而设。", + "enable": "显示悬浮球", + "hint": "拖动可移动;快速滑动触发手势;长按打开面板与手势表。", + "resetPosition": "重置位置", + "ballSize": "球体大小", + "sizeSmall": "小", + "sizeMedium": "中", + "sizeLarge": "大", + "gestureBindings": "手势绑定", + "bindingCleared": "未绑定", + "favoriteAgents": "常用智能体", + "favoriteAgentsHint": "选择 2-3 个智能体,供「常用智能体」手势循环快切。", + "favoriteAgentsEmpty": "暂无可用智能体", + "panelTitle": "快捷操作" } }, "SystemSettings": { @@ -1201,6 +1194,30 @@ "apiKeyHint": "以 KIRO_API_KEY 传给 CLI。留空则使用 kiro-cli login 登录的账号;只有无法用浏览器登录的环境(服务器、Docker)才需要 API Key。", "saveConfig": "保存 Kiro 配置" }, + "trae": { + "configManagement": "配置管理", + "configDescription": "TRAE CLI 2.0 仅向 TRAE 企业版旗舰版套餐客户开放。Codeg 运行你安装的 traecli,它把登录状态、技能和 MCP 服务器保存在 ~/.trae 中。模型、模式和推理强度不在这里设置:TRAE CLI 会通过 ACP 上报它们,由输入框自带的选择器按会话管理。", + "signInTitle": "登录", + "signInHint": "在终端中运行一次下面的命令,用企业 SSO 登录。Codeg 里的会话会复用这次登录。", + "copyCommand": "复制命令", + "commandCopied": "命令已复制", + "patLabel": "个人访问令牌(可选)", + "patHint": "适用于无法用浏览器登录的机器,例如服务器或 Docker 部署。Codeg 会以 TRAECLI_PERSONAL_ACCESS_TOKEN 传给 TRAE CLI,并在每次启动会话时用它登录。留空则使用 traecli login 的登录。", + "hostLabel": "企业 API 地址(可选)", + "hostHint": "仅当你的组织在自有域名上部署 TRAE 时填写。以 TRAECLI_HOST 传入;留空则使用 TRAE 官方服务。", + "saveConfig": "保存 TRAE CLI 配置" + }, + "commandCode": { + "configManagement": "配置管理", + "configDescription": "Codeg 运行 command-code npm 包提供的 Command Code CLI,它把登录状态、技能和 MCP 服务器保存在 ~/.commandcode 中。模型、模式和推理强度不在这里设置:Command Code 会通过 ACP 上报它们,由输入框自带的选择器按会话管理。", + "signInTitle": "登录", + "signInHint": "在终端中运行一次下面的命令,登录你的 Command Code 账号。Codeg 里的会话会复用这次登录。", + "copyCommand": "复制命令", + "commandCopied": "命令已复制", + "apiKeyLabel": "API 密钥(可选)", + "apiKeyHint": "适用于无法用浏览器登录的机器,例如服务器或 Docker 部署。以 COMMAND_CODE_API_KEY 传给 Command Code,优先于 command-code login 的登录。留空则使用那次登录。", + "saveConfig": "保存 Command Code 配置" + }, "deepseek": { "configManagement": "DeepSeek Harness 配置", "configDescription": "端点与密钥是 deepseek-acp 启动时读取的环境变量。模型和推理档位是会话级选择器,在输入框里切换。", @@ -1696,6 +1713,10 @@ "saveAntigravityConfigFailed": "保存 Antigravity 配置失败", "kiroSaved": "Kiro 配置已保存", "saveKiroFailed": "保存 Kiro 配置失败", + "traeSaved": "TRAE CLI 配置已保存", + "saveTraeFailed": "保存 TRAE CLI 配置失败", + "commandCodeSaved": "Command Code 配置已保存", + "saveCommandCodeFailed": "保存 Command Code 配置失败", "systemAgentAlreadyLatest": "{name} 已是最新版本({version})" }, "distribution": { @@ -2467,7 +2488,10 @@ "commits": "提交" }, "noFolderTitle": "暂无打开的文件夹", - "noFolderHint": "打开一个文件夹后在这里查看" + "noFolderHint": "打开一个文件夹后在这里查看", + "openFullPage": "在完整页面中打开", + "back": "返回", + "noAutomations": "此项目还没有自动化" }, "windowControls": { "minimizeWindow": "最小化窗口", @@ -3366,6 +3390,7 @@ }, "messageInput": { "askAnything": "请开始输入...", + "promptSuggestion": "{suggestion}(按 Tab 填入)", "removeAttachmentAria": "移除 {name}", "attachFiles": "附加文件", "attachLocalUpload": "附加本地文件", @@ -3397,6 +3422,10 @@ "searchModelAria": "搜索模型", "modelListLabel": "模型", "recommendedBadge": "推荐", + "recommendedValue": "推荐:{value}", + "searchOptions": "搜索选项...", + "searchOptionsAria": "搜索选项", + "noOptions": "未找到选项", "noModels": "未找到模型", "cancel": "取消", "send": "发送", @@ -4007,13 +4036,16 @@ }, "messageNav": { "title": "消息导航", - "collapse": "收起消息导航", "loadingEarlier": "正在加载更早的消息…", "loadEarlier": "加载更早的消息", - "collapsedSummary": "消息 {count}", "fileCount": "{count} 个文件", - "remove": "移除", - "noDiffDataAvailable": "未找到 {filePath} 的差异数据" + "subAgentCount": "{count} 个子智能体", + "showDiff": "展开 {filePath} 的改动", + "hideDiff": "收起 {filePath} 的改动", + "openFile": "打开 {filePath}", + "collapse": "收起", + "currentMessage": "当前消息", + "scrollToCurrent": "滚动到当前消息(#{ordinal})" }, "replyArtifacts": { "title": "改动文件", @@ -4071,6 +4103,9 @@ "resumeDelegation": "恢复任务 {task}", "resumeDelegationNoId": "恢复委派任务", "promptLabel": "指令", + "ackRecorded": "已记录到待办任务", + "ackNotRecorded": "未记录到待办任务", + "ackNotRecordedWithReason": "未记录到待办任务:{reason}", "verdict": { "success": "成功", "needs_review": "待复核", @@ -4188,6 +4223,18 @@ "stopDeclined": "无法停止该任务,它可能已经结束。", "stopFailed": "停止任务失败:{error}" }, + "remoteControl": { + "title": "远程控制已开启", + "open": "在浏览器中打开", + "copy": "复制链接", + "copied": "已复制", + "qr": "二维码", + "qrLabel": "本会话远程控制链接的二维码", + "qrHint": "用手机扫码,在 Claude App 或 claude.ai/code 上继续本会话。", + "noUrl": "Claude Code 没有返回本会话的链接", + "turnOff": "关闭", + "turnOffBusy": "Claude 完成当前工作后可用" + }, "proposedPlan": { "title": "计划提案", "planning": "规划中…" @@ -7365,13 +7412,6 @@ } }, "FloatingBall": { - "label": "悬浮球", - "panelTitle": "快捷操作", - "settingsTitle": "悬浮球", - "close": "关闭", - "resetPosition": "重置位置", - "cheatSheetTitle": "手势", - "hint": "拖动悬浮球可移动位置。快速滑动触发手势,长按打开此面板。", "actions": { "newConversation": "新建会话", "toggleSidebar": "切换侧栏", @@ -7437,7 +7477,6 @@ "upLeftThenRight": "↖ 然后 →", "upRightThenLeft": "↗ 然后 ←" }, - "favoriteAgentsEmpty": "暂无可用智能体", "toasts": { "renameNeedsSavedSession": "重命名只对已保存的会话生效——先发一条消息。", "sessionFolderNeedsWorkspace": "请先打开一个工作区文件夹。", @@ -7450,6 +7489,14 @@ "noModesAvailable": "该智能体没有可用模式。", "noSwitchTarget": "没有可切换的模式(未找到 Plan 或完整访问模式)。" }, + "label": "悬浮球", + "panelTitle": "快捷操作", + "settingsTitle": "悬浮球", + "close": "关闭", + "resetPosition": "重置位置", + "cheatSheetTitle": "手势", + "hint": "拖动悬浮球可移动位置。快速滑动触发手势,长按打开此面板。", + "favoriteAgentsEmpty": "暂无可用智能体", "recentSessionDepth": "最近会话切换深度", "bindingCleared": "未绑定" } diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 5abd210c9e..0b064127e8 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -181,13 +181,6 @@ "sectionDescription": "對話區域內的摺疊/展開、工具呼叫、工作狀態等動畫,以及平滑捲動。關閉後介面會立即更新、不再有過渡動畫;載入轉圈圖示仍會旋轉。", "enable": "啟用聊天區域動畫" }, - "messageNav": { - "sectionTitle": "訊息導覽", - "sectionDescription": "對話訊息導覽卡的位置。右下角會變成緊湊卡片:每次使用者回話一行,貼在輸入框上方,不擋閱讀區。", - "positionStart": "左上(預設)", - "positionBottomEnd": "右下(緊湊)", - "hint": "計畫面板與子代理面板始終留在左上。" - }, "chatWidth": { "sectionTitle": "聊天區域寬度", "sectionDescription": "對話列與輸入框的最大寬度。也可以直接拖曳聊天區域左右兩側的邊緣來調整。", @@ -247,54 +240,6 @@ "credit": "圖片來源 wallhaven.cc · 僅 SFW 內容" } }, - "floatingBall": { - "sectionTitle": "懸浮球", - "sectionDescription": "工作區上的可拖動懸浮球,把滑動手勢變成快捷鍵——為沒有鍵盤的手機而設。", - "enable": "顯示懸浮球", - "hint": "拖動可移動;快速滑動觸發手勢;長按打開面板與手勢表。", - "resetPosition": "重設位置", - "ballSize": "球體大小", - "sizeSmall": "小", - "sizeMedium": "中", - "sizeLarge": "大", - "gestureBindings": "手勢綁定", - "bindingCleared": "未綁定", - "favoriteAgents": "常用代理人", - "favoriteAgentsHint": "選擇 2-3 個代理人,供「常用代理人」手勢循環快切。", - "favoriteAgentsEmpty": "暫無可用代理人", - "panelTitle": "快捷操作" - }, - "gestures": { - "tap": "單擊", - "doubleTap": "雙擊", - "longPress": "長按", - "left": "←", - "right": "→", - "up": "↑", - "upLeft": "↖", - "upRight": "↗", - "upThenLeft": "↑ 然後 ←", - "upThenRight": "↑ 然後 →", - "upDown": "↑ 然後 ↓", - "leftThenRight": "← 然後 →", - "rightThenLeft": "→ 然後 ←", - "upLeftThenDown": "↖ 然後 ↓", - "upRightThenDown": "↗ 然後 ↓", - "down": "↓", - "downLeft": "↙", - "downRight": "↘", - "downUp": "↓ 然後 ↑", - "upThenDownRight": "↑ 然後 ↘", - "downThenUpRight": "↓ 然後 ↗", - "rightThenUp": "→ 然後 ↑", - "rightThenDown": "→ 然後 ↓", - "downThenRight": "↓ 然後 →", - "downThenLeft": "↓ 然後 ←", - "downRightThenUp": "↘ 然後 ↑", - "downLeftThenUp": "↙ 然後 ↑", - "upLeftThenRight": "↖ 然後 →", - "upRightThenLeft": "↗ 然後 ←" - }, "actions": { "newConversation": "新增對話", "toggleSidebar": "切換側欄", @@ -328,6 +273,54 @@ "focusModeSelector": "模式選擇", "togglePlanFullAccess": "Plan/全權限切換", "openCollaboration": "協作" + }, + "gestures": { + "tap": "單擊", + "doubleTap": "雙擊", + "longPress": "長按", + "left": "←", + "right": "→", + "up": "↑", + "upLeft": "↖", + "upRight": "↗", + "upThenLeft": "↑ 然後 ←", + "upThenRight": "↑ 然後 →", + "upDown": "↑ 然後 ↓", + "leftThenRight": "← 然後 →", + "rightThenLeft": "→ 然後 ←", + "upLeftThenDown": "↖ 然後 ↓", + "upRightThenDown": "↗ 然後 ↓", + "down": "↓", + "downLeft": "↙", + "downRight": "↘", + "downUp": "↓ 然後 ↑", + "upThenDownRight": "↑ 然後 ↘", + "downThenUpRight": "↓ 然後 ↗", + "rightThenUp": "→ 然後 ↑", + "rightThenDown": "→ 然後 ↓", + "downThenRight": "↓ 然後 →", + "downThenLeft": "↓ 然後 ←", + "downRightThenUp": "↘ 然後 ↑", + "downLeftThenUp": "↙ 然後 ↑", + "upLeftThenRight": "↖ 然後 →", + "upRightThenLeft": "↗ 然後 ←" + }, + "floatingBall": { + "sectionTitle": "懸浮球", + "sectionDescription": "工作區上的可拖動懸浮球,把滑動手勢變成快捷鍵——為沒有鍵盤的手機而設。", + "enable": "顯示懸浮球", + "hint": "拖動可移動;快速滑動觸發手勢;長按打開面板與手勢表。", + "resetPosition": "重設位置", + "ballSize": "球體大小", + "sizeSmall": "小", + "sizeMedium": "中", + "sizeLarge": "大", + "gestureBindings": "手勢綁定", + "bindingCleared": "未綁定", + "favoriteAgents": "常用代理人", + "favoriteAgentsHint": "選擇 2-3 個代理人,供「常用代理人」手勢循環快切。", + "favoriteAgentsEmpty": "暫無可用代理人", + "panelTitle": "快捷操作" } }, "SystemSettings": { @@ -1201,6 +1194,30 @@ "apiKeyHint": "以 KIRO_API_KEY 傳給 CLI。留空則使用 kiro-cli login 登入的帳號;只有無法用瀏覽器登入的環境(伺服器、Docker)才需要 API Key。", "saveConfig": "儲存 Kiro 設定" }, + "trae": { + "configManagement": "設定管理", + "configDescription": "TRAE CLI 2.0 僅開放給 TRAE 企業版旗艦版方案的客戶。Codeg 執行你安裝的 traecli,它把登入狀態、技能和 MCP 伺服器儲存在 ~/.trae 中。模型、模式和推理強度不在這裡設定:TRAE CLI 會透過 ACP 回報它們,由輸入框內建的選擇器依工作階段管理。", + "signInTitle": "登入", + "signInHint": "在終端機中執行一次下面的命令,用企業 SSO 登入。Codeg 裡的工作階段會沿用這次登入。", + "copyCommand": "複製命令", + "commandCopied": "命令已複製", + "patLabel": "個人存取權杖(選填)", + "patHint": "適用於無法用瀏覽器登入的機器,例如伺服器或 Docker 部署。Codeg 會以 TRAECLI_PERSONAL_ACCESS_TOKEN 傳給 TRAE CLI,並在每次啟動工作階段時用它登入。留空則使用 traecli login 的登入。", + "hostLabel": "企業 API 位址(選填)", + "hostHint": "僅當你的組織在自有網域上部署 TRAE 時填寫。以 TRAECLI_HOST 傳入;留空則使用 TRAE 官方服務。", + "saveConfig": "儲存 TRAE CLI 設定" + }, + "commandCode": { + "configManagement": "設定管理", + "configDescription": "Codeg 執行 command-code npm 套件提供的 Command Code CLI,它把登入狀態、技能和 MCP 伺服器儲存在 ~/.commandcode 中。模型、模式和推理強度不在這裡設定:Command Code 會透過 ACP 回報它們,由輸入框內建的選擇器依工作階段管理。", + "signInTitle": "登入", + "signInHint": "在終端機中執行一次下面的命令,登入你的 Command Code 帳號。Codeg 裡的工作階段會沿用這次登入。", + "copyCommand": "複製命令", + "commandCopied": "命令已複製", + "apiKeyLabel": "API 金鑰(選填)", + "apiKeyHint": "適用於無法用瀏覽器登入的機器,例如伺服器或 Docker 部署。以 COMMAND_CODE_API_KEY 傳給 Command Code,優先於 command-code login 的登入。留空則使用那次登入。", + "saveConfig": "儲存 Command Code 設定" + }, "deepseek": { "configManagement": "DeepSeek Harness 設定", "configDescription": "端點與金鑰是 deepseek-acp 啟動時讀取的環境變數。模型與推理等級是工作階段層級的選擇器,在輸入框裡切換。", @@ -1696,6 +1713,10 @@ "saveAntigravityConfigFailed": "保存 Antigravity 配置失败", "kiroSaved": "Kiro 設定已儲存", "saveKiroFailed": "儲存 Kiro 設定失敗", + "traeSaved": "TRAE CLI 設定已儲存", + "saveTraeFailed": "儲存 TRAE CLI 設定失敗", + "commandCodeSaved": "Command Code 設定已儲存", + "saveCommandCodeFailed": "儲存 Command Code 設定失敗", "systemAgentAlreadyLatest": "{name} 已是最新版本({version})" }, "distribution": { @@ -2467,7 +2488,10 @@ "commits": "提交" }, "noFolderTitle": "尚無開啟的資料夾", - "noFolderHint": "開啟一個資料夾後可在此查看" + "noFolderHint": "開啟一個資料夾後可在此查看", + "openFullPage": "在完整頁面中開啟", + "back": "返回", + "noAutomations": "此專案還沒有自動化" }, "windowControls": { "minimizeWindow": "最小化視窗", @@ -3366,6 +3390,7 @@ }, "messageInput": { "askAnything": "請開始輸入...", + "promptSuggestion": "{suggestion}(按 Tab 填入)", "removeAttachmentAria": "移除 {name}", "attachFiles": "附加檔案", "addActions": "新增", @@ -3395,6 +3420,10 @@ "searchModelAria": "搜尋模型", "modelListLabel": "模型", "recommendedBadge": "推薦", + "recommendedValue": "推薦:{value}", + "searchOptions": "搜尋選項...", + "searchOptionsAria": "搜尋選項", + "noOptions": "找不到選項", "noModels": "找不到模型", "cancel": "取消", "send": "傳送", @@ -4007,13 +4036,16 @@ }, "messageNav": { "title": "訊息導覽", - "collapse": "收合訊息導覽", "loadingEarlier": "正在載入更早的訊息…", "loadEarlier": "載入更早的訊息", - "collapsedSummary": "訊息 {count}", "fileCount": "{count} 個檔案", - "remove": "移除", - "noDiffDataAvailable": "找不到 {filePath} 的差異資料" + "subAgentCount": "{count} 個子智能體", + "showDiff": "展開 {filePath} 的變更", + "hideDiff": "收合 {filePath} 的變更", + "openFile": "開啟 {filePath}", + "collapse": "收合", + "currentMessage": "目前訊息", + "scrollToCurrent": "捲動到目前訊息(#{ordinal})" }, "replyArtifacts": { "title": "變更檔案", @@ -4071,6 +4103,9 @@ "resumeDelegation": "恢復任務 {task}", "resumeDelegationNoId": "恢復委派任務", "promptLabel": "指令", + "ackRecorded": "已記錄到待辦任務", + "ackNotRecorded": "未記錄到待辦任務", + "ackNotRecordedWithReason": "未記錄到待辦任務:{reason}", "verdict": { "success": "成功", "needs_review": "待複核", @@ -4188,6 +4223,18 @@ "stopDeclined": "無法停止該任務,它可能已經結束。", "stopFailed": "停止任務失敗:{error}" }, + "remoteControl": { + "title": "遠端控制已開啟", + "open": "在瀏覽器中開啟", + "copy": "複製連結", + "copied": "已複製", + "qr": "QR 碼", + "qrLabel": "本會話遠端控制連結的 QR 碼", + "qrHint": "用手機掃描,在 Claude App 或 claude.ai/code 上繼續本會話。", + "noUrl": "Claude Code 沒有傳回本會話的連結", + "turnOff": "關閉", + "turnOffBusy": "Claude 完成目前工作後可用" + }, "proposedPlan": { "title": "計劃提案", "planning": "規劃中…" @@ -7365,13 +7412,6 @@ } }, "FloatingBall": { - "label": "懸浮球", - "panelTitle": "快捷操作", - "settingsTitle": "懸浮球", - "close": "關閉", - "resetPosition": "重設位置", - "cheatSheetTitle": "手勢", - "hint": "拖動懸浮球可移動位置。快速滑動觸發手勢,長按打開此面板。", "actions": { "newConversation": "新增對話", "toggleSidebar": "切換側欄", @@ -7437,7 +7477,6 @@ "upLeftThenRight": "↖ 然後 →", "upRightThenLeft": "↗ 然後 ←" }, - "favoriteAgentsEmpty": "暫無可用代理人", "toasts": { "renameNeedsSavedSession": "重新命名只對已儲存的會話生效——先發一則訊息。", "sessionFolderNeedsWorkspace": "請先開啟一個工作區資料夾。", @@ -7450,6 +7489,14 @@ "noModesAvailable": "該智慧代理沒有可用模式。", "noSwitchTarget": "沒有可切換的模式(未找到 Plan 或完整存取模式)。" }, + "label": "懸浮球", + "panelTitle": "快捷操作", + "settingsTitle": "懸浮球", + "close": "關閉", + "resetPosition": "重設位置", + "cheatSheetTitle": "手勢", + "hint": "拖動懸浮球可移動位置。快速滑動觸發手勢,長按打開此面板。", + "favoriteAgentsEmpty": "暫無可用代理人", "recentSessionDepth": "最近會話切換深度", "bindingCleared": "未綁定" } diff --git a/src/lib/default-mode-prefs.ts b/src/lib/default-mode-prefs.ts new file mode 100644 index 0000000000..b78fc61d23 --- /dev/null +++ b/src/lib/default-mode-prefs.ts @@ -0,0 +1,32 @@ +/** + * Classification helper shared by the floating ball's "Plan ⇄ full access" + * toggle: identify the full-access mode among an agent's advertised mode ids. + * Each CLI calls full access something else (zcode `yolo`, codex + * `danger-full-access`, claude `bypassPermissions`), so the match runs a fixed + * weight order of well-known substrings — no blind id guessing, the backend + * would silently drop a wrong preferred id. + */ + +/** Substrings that identify a full-access mode, in match priority order. */ +export const FULL_ACCESS_WEIGHTS = [ + "danger", + "bypass", + "yolo", + "full", + "access", +] as const + +/** + * Match a full-access mode among the advertised ids. Weight order decides + * between multiple hits (an agent offering both `yolo` and + * `danger-full-access` gets the `danger` one); ties resolve by list order so + * the result is stable across restarts. + */ +export function matchFullAccessMode(modeIds: readonly string[]): string | null { + for (const weight of FULL_ACCESS_WEIGHTS) { + for (const id of modeIds) { + if (id.toLowerCase().includes(weight)) return id + } + } + return null +}