mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Merge pull request #74734 from NousResearch/bb/tab-context-menu
Every tab strip gets the standard right-click tab menu
This commit is contained in:
commit
206eda50a5
9 changed files with 89 additions and 283 deletions
|
|
@ -320,91 +320,6 @@ export function groupLeafIds(node: LayoutNode): string[] {
|
|||
return node.type === 'group' ? [node.id] : node.children.flatMap(groupLeafIds)
|
||||
}
|
||||
|
||||
function pathToGroup(node: LayoutNode, groupId: string): LayoutNode[] | null {
|
||||
if (node.type === 'group') {
|
||||
return node.id === groupId ? [node] : null
|
||||
}
|
||||
|
||||
for (const child of node.children) {
|
||||
const sub = pathToGroup(child, groupId)
|
||||
|
||||
if (sub) {
|
||||
return [node, ...sub]
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const OPPOSITE_EDGE: Record<RootEdge, RootEdge> = { bottom: 'top', left: 'right', right: 'left', top: 'bottom' }
|
||||
|
||||
/** The viable group touching `edge` of this subtree. Along the edge's axis
|
||||
* children are scanned edge-first — a non-viable zone is display:none, so the
|
||||
* next sibling IS the visual edge; across it, every child touches the edge. */
|
||||
function edgeGroup(node: LayoutNode, edge: RootEdge, viable: (g: GroupNode) => boolean): GroupNode | null {
|
||||
if (node.type === 'group') {
|
||||
return viable(node) ? node : null
|
||||
}
|
||||
|
||||
const along = (node.orientation === 'row') === (edge === 'left' || edge === 'right')
|
||||
const children = along && (edge === 'right' || edge === 'bottom') ? [...node.children].reverse() : node.children
|
||||
|
||||
for (const child of children) {
|
||||
const hit = edgeGroup(child, edge, viable)
|
||||
|
||||
if (hit) {
|
||||
return hit
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* The viable zone VISUALLY adjacent to `groupId` on `side` (the target of the
|
||||
* zone menu's "Move left/right/up/down"). Walks up to the nearest ancestor
|
||||
* split running along that axis with a sibling on that side, then descends to
|
||||
* the sibling's closest viable leaf; subtrees whose every zone fails `viable`
|
||||
* (all panes hidden) are skipped, matching their collapsed rendering.
|
||||
*/
|
||||
export function adjacentGroup(
|
||||
root: LayoutNode,
|
||||
groupId: string,
|
||||
side: RootEdge,
|
||||
viable: (g: GroupNode) => boolean
|
||||
): GroupNode | null {
|
||||
const path = pathToGroup(root, groupId)
|
||||
|
||||
if (!path) {
|
||||
return null
|
||||
}
|
||||
|
||||
const orientation: Orientation = side === 'left' || side === 'right' ? 'row' : 'column'
|
||||
const forward = side === 'right' || side === 'bottom'
|
||||
|
||||
for (let i = path.length - 2; i >= 0; i--) {
|
||||
const parent = path[i]
|
||||
|
||||
if (parent.type !== 'split' || parent.orientation !== orientation) {
|
||||
continue
|
||||
}
|
||||
|
||||
const index = parent.children.indexOf(path[i + 1])
|
||||
|
||||
const siblings = forward ? parent.children.slice(index + 1) : parent.children.slice(0, index).reverse()
|
||||
|
||||
for (const sibling of siblings) {
|
||||
const hit = edgeGroup(sibling, OPPOSITE_EDGE[side], viable)
|
||||
|
||||
if (hit) {
|
||||
return hit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function sameSet(ids: string[], set: Set<string>): boolean {
|
||||
return ids.length === set.size && ids.every(id => set.has(id))
|
||||
}
|
||||
|
|
@ -523,32 +438,6 @@ function replaceNode(node: LayoutNode, id: string, make: (g: GroupNode) => Layou
|
|||
return { ...node, children: node.children.map(c => replaceNode(c, id, make)) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a zone: `movePaneId` (one of SEVERAL panes in the group) moves into
|
||||
* the new zone on `side` — VS Code "split right", split and move in one
|
||||
* gesture. A lone pane can't split away from itself: no-op (normalize prunes
|
||||
* the empty zone the split would have minted).
|
||||
*/
|
||||
export function splitGroupZone(root: LayoutNode, groupId: string, side: RootEdge, movePaneId: string): LayoutNode {
|
||||
const orientation: Orientation = side === 'left' || side === 'right' ? 'row' : 'column'
|
||||
const before = side === 'left' || side === 'top'
|
||||
|
||||
return (
|
||||
normalize(
|
||||
replaceNode(root, groupId, g => {
|
||||
if (g.panes.length < 2 || !g.panes.includes(movePaneId)) {
|
||||
return g
|
||||
}
|
||||
|
||||
const added = group([movePaneId])
|
||||
const remaining = { ...g, panes: g.panes.filter(p => p !== movePaneId) }
|
||||
|
||||
return split(orientation, before ? [added, remaining] : [remaining, added], [1, 1])
|
||||
})
|
||||
) ?? root
|
||||
)
|
||||
}
|
||||
|
||||
/** Mirror the layout HORIZONTALLY (the titlebar flip toggle / ⌘\): reverse
|
||||
* every ROW split's child order at EVERY depth, so left↔right flips
|
||||
* everywhere. A right rail lands on the left with its OWN internal order
|
||||
|
|
|
|||
|
|
@ -6,14 +6,14 @@
|
|||
*
|
||||
* Dragging is FancyZones-style (drag-session.ts): the layout stays fixed and
|
||||
* every zone lights up as a whole-region drop target. Right-click opens the
|
||||
* contextual zone menu (split/move + header/minimize toggles).
|
||||
* contextual zone menu (tab close verbs + header/minimize toggles).
|
||||
*/
|
||||
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { type CSSProperties, Fragment, type ReactNode, type RefObject, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { ActionsContextMenu, type MenuKit, renderActionItem } from '@/components/ui/actions-menu'
|
||||
import { Codicon } from '@/components/ui/codicon'
|
||||
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu'
|
||||
import { DecodeText } from '@/components/ui/decode-text'
|
||||
import { DROP_SHEET_BLUR_CLASS, DROP_SHEET_CLASS } from '@/components/ui/drop-affordance'
|
||||
import { PANE_TAB_STRIP_LINE_LEFT, PANE_TAB_STRIP_LINE_RIGHT, PaneTab, PaneTabLabel } from '@/components/ui/pane-tab'
|
||||
|
|
@ -25,28 +25,28 @@ import { cn } from '@/lib/utils'
|
|||
import { $layoutEditMode } from '../../edit-mode'
|
||||
import { useWindowControlsOverlap } from '../../geometry'
|
||||
import { hiddenPaneProps, PaneGroupContext, PaneVisibleContext } from '../../pane-visibility'
|
||||
import type { DropPosition, GroupNode, RootEdge } from '../model'
|
||||
import { adjacentGroup } from '../model'
|
||||
import type { DropPosition, GroupNode } from '../model'
|
||||
import {
|
||||
$dropHint,
|
||||
$hiddenTreePanes,
|
||||
$layoutTree,
|
||||
$narrowViewport,
|
||||
$newSessionTabAction,
|
||||
$treeDragging,
|
||||
activateTreePane,
|
||||
closeAllTreeTabs,
|
||||
closeOtherTreeTabs,
|
||||
closeTreePane,
|
||||
closeTreeTabsToRight,
|
||||
collapseTreePane,
|
||||
dismissTreePane,
|
||||
isCollapsePane,
|
||||
isSessionStripPane,
|
||||
moveTreePane,
|
||||
noteActiveTreeGroup,
|
||||
restoreTreePane,
|
||||
SESSION_TILE_DRAG,
|
||||
setTreeGroupHeaderHidden,
|
||||
setTreeGroupMinimized,
|
||||
splitTreeZone
|
||||
treeTabCloseTargets
|
||||
} from '../store'
|
||||
|
||||
import { type DoubleTapContext, startPaneDrag } from './drag-session'
|
||||
|
|
@ -54,30 +54,19 @@ import { forceLoneHeaderForPanes } from './lone-header'
|
|||
import { useActiveTabVisible } from './tab-strip-scroll'
|
||||
import { paneChrome } from './track-model'
|
||||
|
||||
/** A directional action in the zone menu (computed per group state). */
|
||||
interface ZoneMenuDirection {
|
||||
side: RootEdge
|
||||
label: string
|
||||
run: () => void
|
||||
}
|
||||
|
||||
const DIRECTION_ORDER: readonly RootEdge[] = ['right', 'bottom', 'left', 'top']
|
||||
const DIRECTION_ARROW: Record<RootEdge, string> = { bottom: '↓', left: '←', right: '→', top: '↑' }
|
||||
|
||||
/** Right-click zone menu: directional actions + header toggle + minimize.
|
||||
* The directions are CONTEXTUAL (computed by TreeGroup): a stacked group
|
||||
* offers "Split <dir>" (carve a new zone with the clicked pane — VS Code
|
||||
* split-and-move in one gesture); a single-pane group offers "Move <dir>"
|
||||
* into the zone actually sitting on that side — directions with no visible
|
||||
* neighbor aren't offered, so no action ever appears to do nothing. */
|
||||
/** Right-click zone menu: the tab verbs (close this / others / to the right /
|
||||
* all) plus the strip's own chrome toggles. Same items and icons as a session
|
||||
* tab's menu, so every tab in a strip answers a right-click the same way —
|
||||
* a pane with no domain menu of its own (the file tree, a terminal, the main
|
||||
* tab on a fresh draft) falls through to this one. */
|
||||
function ZoneMenu({
|
||||
children,
|
||||
closable,
|
||||
minimizable = true,
|
||||
directions,
|
||||
headerHidden,
|
||||
minimized,
|
||||
nodeId
|
||||
nodeId,
|
||||
targetPane
|
||||
}: {
|
||||
children: ReactNode
|
||||
/** The pane the menu closes (the right-clicked chip / the active pane);
|
||||
|
|
@ -86,52 +75,71 @@ function ZoneMenu({
|
|||
/** False for the zone hosting the uncloseable workspace — collapsing the
|
||||
* MAIN pane strands the app behind a strip. */
|
||||
minimizable?: boolean
|
||||
/** Called when the menu renders, not on every zone re-render: resolving the
|
||||
* neighbor zones has to read the layout tree, and subscribing every zone to
|
||||
* it made a sash drag re-render every mounted pane. Same lazy shape as
|
||||
* `closable`. */
|
||||
directions: () => ZoneMenuDirection[]
|
||||
headerHidden?: boolean
|
||||
minimized?: boolean
|
||||
nodeId: string
|
||||
/** The right-clicked chip (else the active pane) — what the close-others /
|
||||
* to-the-right / all verbs measure from. Called when the menu RENDERS, not
|
||||
* on every zone re-render: resolving the siblings reads the layout tree,
|
||||
* and subscribing every zone to it made a sash drag re-render every
|
||||
* mounted pane. */
|
||||
targetPane: () => string
|
||||
}) {
|
||||
const { t } = useI18n()
|
||||
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
{directions().map(direction => (
|
||||
<ContextMenuItem key={direction.side} onSelect={direction.run}>
|
||||
{direction.label}
|
||||
</ContextMenuItem>
|
||||
))}
|
||||
<ContextMenuItem onSelect={() => setTreeGroupHeaderHidden(nodeId, !headerHidden)}>
|
||||
{headerHidden ? t.zones.showHeader : t.zones.hideHeader}
|
||||
</ContextMenuItem>
|
||||
{minimizable && (
|
||||
<ContextMenuItem onSelect={() => setTreeGroupMinimized(nodeId, !minimized)}>
|
||||
{minimized ? t.zones.restore : t.zones.minimize}
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
{/* Resolved at render: the menu mounts on open, after the right-click
|
||||
set menuPane — so an uncloseable target hides the item instead
|
||||
of offering a dead action. */}
|
||||
{closable?.() !== undefined && (
|
||||
<ContextMenuItem
|
||||
onSelect={() => {
|
||||
const paneId = closable?.()
|
||||
// Resolved at render: the menu mounts on open, after the right-click set
|
||||
// menuPane — so an uncloseable target hides Close instead of offering a
|
||||
// dead action, and the counts describe the chip actually clicked.
|
||||
const items = (kit: MenuKit) => {
|
||||
const paneId = closable?.()
|
||||
const targets = treeTabCloseTargets(targetPane())
|
||||
|
||||
if (paneId) {
|
||||
closeTreePane(paneId)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t.common.close}
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
return (
|
||||
<>
|
||||
{paneId !== undefined &&
|
||||
renderActionItem(kit, {
|
||||
icon: 'close',
|
||||
label: t.common.close,
|
||||
onSelect: () => closeTreePane(paneId)
|
||||
})}
|
||||
{renderActionItem(kit, {
|
||||
disabled: !targets.others,
|
||||
icon: 'close-all',
|
||||
label: t.zones.closeOthers,
|
||||
onSelect: () => closeOtherTreeTabs(targetPane())
|
||||
})}
|
||||
{renderActionItem(kit, {
|
||||
disabled: !targets.right,
|
||||
icon: 'arrow-right',
|
||||
label: t.zones.closeToRight,
|
||||
onSelect: () => closeTreeTabsToRight(targetPane())
|
||||
})}
|
||||
{renderActionItem(kit, {
|
||||
disabled: !targets.all,
|
||||
icon: 'clear-all',
|
||||
label: t.zones.closeAll,
|
||||
onSelect: () => closeAllTreeTabs(targetPane())
|
||||
})}
|
||||
<kit.Separator />
|
||||
{renderActionItem(kit, {
|
||||
icon: headerHidden ? 'eye' : 'eye-closed',
|
||||
label: headerHidden ? t.zones.showHeader : t.zones.hideHeader,
|
||||
onSelect: () => setTreeGroupHeaderHidden(nodeId, !headerHidden)
|
||||
})}
|
||||
{minimizable &&
|
||||
renderActionItem(kit, {
|
||||
icon: minimized ? 'chevron-down' : 'chevron-up',
|
||||
label: minimized ? t.zones.restore : t.zones.minimize,
|
||||
onSelect: () => setTreeGroupMinimized(nodeId, !minimized)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionsContextMenu contentClassName="w-40" items={items}>
|
||||
{children}
|
||||
</ActionsContextMenu>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -255,61 +263,20 @@ export function TreeGroup({
|
|||
}
|
||||
}
|
||||
|
||||
const dirWord: Record<RootEdge, string> = {
|
||||
bottom: t.zones.dirDown,
|
||||
left: t.zones.dirLeft,
|
||||
right: t.zones.dirRight,
|
||||
top: t.zones.dirUp
|
||||
}
|
||||
|
||||
// Zone-menu directions, contextual to this group's state:
|
||||
// - stacked panes -> "Split <dir>": carve a new zone on that side with the
|
||||
// right-clicked chip's pane in it (split + move, one gesture);
|
||||
// - a single pane -> "Move <dir>": join the zone visually adjacent on that
|
||||
// side (splitting here would only make an invisible empty zone). Sides
|
||||
// with no visible neighbor are omitted entirely.
|
||||
// NOT `useStore($layoutTree)`: this subscribes every zone — and therefore
|
||||
// every mounted pane and its whole transcript — to the entire layout tree.
|
||||
// A sash drag rewrites the tree once per frame, so dragging the sidebar
|
||||
// re-rendered all five tiles' message lists on every pointermove (measured:
|
||||
// TreeGroup 180 renders cascading into ChatView/Thread/TileChat at ~4.5s
|
||||
// each, holding the drag at ~3fps).
|
||||
//
|
||||
// The tree is only read to build the zone context menu's move/split
|
||||
// directions, which are consumed when the menu OPENS — so read it at that
|
||||
// moment with `.get()` instead of subscribing to every intermediate frame.
|
||||
const menuDirections = (): ZoneMenuDirection[] => {
|
||||
if (shown.length > 1) {
|
||||
return DIRECTION_ORDER.map(side => ({
|
||||
side,
|
||||
label: `${t.zones.split(dirWord[side])} ${DIRECTION_ARROW[side]}`,
|
||||
run: () => splitTreeZone(node.id, side, menuPane ?? activeId)
|
||||
}))
|
||||
}
|
||||
|
||||
const tree = $layoutTree.get()
|
||||
|
||||
return DIRECTION_ORDER.flatMap(side => {
|
||||
const neighbor = tree ? adjacentGroup(tree, node.id, side, g => g.panes.some(paneShown)) : null
|
||||
|
||||
if (!neighbor || neighbor.id === node.id) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
side,
|
||||
label: `${t.zones.move(dirWord[side])} ${DIRECTION_ARROW[side]}`,
|
||||
run: () => moveTreePane(activeId, { groupId: neighbor.id, pos: 'center' })
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
// Zone-menu close targets read the layout tree, but this component must NOT
|
||||
// subscribe to it: `useStore($layoutTree)` here wires every zone — and
|
||||
// therefore every mounted pane and its whole transcript — to the entire
|
||||
// tree. A sash drag rewrites the tree once per frame, so dragging the
|
||||
// sidebar re-rendered all five tiles' message lists on every pointermove
|
||||
// (measured: TreeGroup 180 renders cascading into ChatView/Thread/TileChat
|
||||
// at ~4.5s each, holding the drag at ~3fps). The menu's items are resolved
|
||||
// when it OPENS, so they read the tree with `.get()` at that moment instead.
|
||||
const targetPane = () => menuPane ?? activeId
|
||||
|
||||
// Close targets the right-clicked chip (falling back to the active pane);
|
||||
// only panes that declare `uncloseable` (the main workspace) are exempt.
|
||||
const closable = () => {
|
||||
const paneId = menuPane ?? activeId
|
||||
const paneId = targetPane()
|
||||
|
||||
return paneChrome(paneFor(paneId)).uncloseable ? undefined : paneId
|
||||
}
|
||||
|
|
@ -330,11 +297,11 @@ export function TreeGroup({
|
|||
// Same menu on the header strip and the edit veil — one prop bag.
|
||||
const zoneMenu = {
|
||||
closable,
|
||||
directions: menuDirections,
|
||||
headerHidden,
|
||||
minimizable,
|
||||
minimized: node.minimized,
|
||||
nodeId: node.id
|
||||
nodeId: node.id,
|
||||
targetPane
|
||||
}
|
||||
|
||||
// NO body double-click toggle: virtualized content (the thread) recreates
|
||||
|
|
|
|||
|
|
@ -31,12 +31,10 @@ import {
|
|||
normalize,
|
||||
removePane,
|
||||
reorderPaneInGroup as reorderPaneInGroupOp,
|
||||
type RootEdge,
|
||||
setActivePane as setActivePaneOp,
|
||||
setGroupHeaderHidden as setGroupHeaderHiddenOp,
|
||||
setGroupMinimized,
|
||||
setSplitWeights as setSplitWeightsOp,
|
||||
splitGroupZone as splitGroupZoneOp,
|
||||
type SplitNode
|
||||
} from './model'
|
||||
import { FLOATING_PLACEMENT } from './renderer/floating-rect'
|
||||
|
|
@ -1205,18 +1203,6 @@ export function reorderTreePane(groupId: string, paneId: string, toIndex: number
|
|||
}
|
||||
}
|
||||
|
||||
/** Split a zone on `side`, moving `movePaneId` out of its stack into the new
|
||||
* zone (VS Code split-and-move — the zone menu's Split actions). */
|
||||
export function splitTreeZone(groupId: string, side: RootEdge, movePaneId: string) {
|
||||
const tree = $layoutTree.get()
|
||||
|
||||
if (tree) {
|
||||
commit(splitGroupZoneOp(tree, groupId, side, movePaneId))
|
||||
markActivePreset('custom')
|
||||
markPaneUserPlaced(movePaneId)
|
||||
}
|
||||
}
|
||||
|
||||
export function setTreeGroupMinimized(groupId: string, minimized: boolean) {
|
||||
const tree = $layoutTree.get()
|
||||
|
||||
|
|
|
|||
|
|
@ -2237,17 +2237,11 @@ export const ar = defineLocale({
|
|||
closeOthers: 'إغلاق الأخرى',
|
||||
closeToRight: 'إغلاق ما على اليمين',
|
||||
closeAll: 'إغلاق الكل',
|
||||
split: dir => `تقسيم ${dir}`,
|
||||
move: dir => `نقل ${dir}`,
|
||||
dirUp: 'للأعلى',
|
||||
dirDown: 'للأسفل',
|
||||
dirLeft: 'لليسار',
|
||||
dirRight: 'لليمين',
|
||||
pluginDisabled: pluginId => `الإضافة "${pluginId}" معطلة`,
|
||||
pluginDisabledBody: 'أعد تفعيلها من الإعدادات ← الإضافات لإرجاع اللوحة.',
|
||||
missingPane: paneId => `لوحة مفقودة: ${paneId}`,
|
||||
editTitle: 'التخطيطات',
|
||||
editHint: 'اختر تخطيطا، أو اسحب اللوحات بين المناطق. انقر بزر الفأرة الأيمن على منطقة لتقسيمها.',
|
||||
editHint: 'اختر تخطيطا، أو اسحب اللوحات بين المناطق.',
|
||||
reset: 'إعادة ضبط',
|
||||
templates: 'القوالب',
|
||||
custom: 'مخصص',
|
||||
|
|
|
|||
|
|
@ -2657,17 +2657,11 @@ export const en: Translations = {
|
|||
closeToRight: 'Close to the right',
|
||||
closeAll: 'Close all',
|
||||
newSessionTab: 'New session tab',
|
||||
split: dir => `Split ${dir}`,
|
||||
move: dir => `Move ${dir}`,
|
||||
dirUp: 'up',
|
||||
dirDown: 'down',
|
||||
dirLeft: 'left',
|
||||
dirRight: 'right',
|
||||
pluginDisabled: pluginId => `Plugin "${pluginId}" disabled`,
|
||||
pluginDisabledBody: 'Re-enable it in Settings → Plugins to bring the pane back.',
|
||||
missingPane: paneId => `missing pane: ${paneId}`,
|
||||
editTitle: 'Layouts',
|
||||
editHint: 'Pick a layout, or drag panes between zones. Right-click a zone to split.',
|
||||
editHint: 'Pick a layout, or drag panes between zones.',
|
||||
reset: 'Reset',
|
||||
templates: 'Templates',
|
||||
custom: 'Custom',
|
||||
|
|
|
|||
|
|
@ -2483,17 +2483,11 @@ export const ja = defineLocale({
|
|||
closeToRight: '右側を閉じる',
|
||||
closeAll: 'すべて閉じる',
|
||||
newSessionTab: '新しいセッションタブ',
|
||||
split: dir => `${dir}に分割`,
|
||||
move: dir => `${dir}へ移動`,
|
||||
dirUp: '上',
|
||||
dirDown: '下',
|
||||
dirLeft: '左',
|
||||
dirRight: '右',
|
||||
pluginDisabled: pluginId => `プラグイン「${pluginId}」を無効化しました`,
|
||||
pluginDisabledBody: '設定 → プラグイン で再有効化するとペインが戻ります。',
|
||||
missingPane: paneId => `ペインが見つかりません: ${paneId}`,
|
||||
editTitle: 'レイアウト',
|
||||
editHint: 'レイアウトを選ぶか、ペインをゾーン間へドラッグ。ゾーンを右クリックで分割。',
|
||||
editHint: 'レイアウトを選ぶか、ペインをゾーン間へドラッグ。',
|
||||
reset: 'リセット',
|
||||
templates: 'テンプレート',
|
||||
custom: 'カスタム',
|
||||
|
|
|
|||
|
|
@ -2257,12 +2257,6 @@ export interface Translations {
|
|||
closeToRight: string
|
||||
closeAll: string
|
||||
newSessionTab: string
|
||||
split: (dir: string) => string
|
||||
move: (dir: string) => string
|
||||
dirUp: string
|
||||
dirDown: string
|
||||
dirLeft: string
|
||||
dirRight: string
|
||||
pluginDisabled: (pluginId: string) => string
|
||||
pluginDisabledBody: string
|
||||
missingPane: (paneId: string) => string
|
||||
|
|
|
|||
|
|
@ -2403,17 +2403,11 @@ export const zhHant = defineLocale({
|
|||
closeToRight: '關閉右側',
|
||||
closeAll: '全部關閉',
|
||||
newSessionTab: '新增工作階段分頁',
|
||||
split: dir => `向${dir}分割`,
|
||||
move: dir => `向${dir}移動`,
|
||||
dirUp: '上',
|
||||
dirDown: '下',
|
||||
dirLeft: '左',
|
||||
dirRight: '右',
|
||||
pluginDisabled: pluginId => `外掛「${pluginId}」已停用`,
|
||||
pluginDisabledBody: '在 設定 → 外掛 中重新啟用即可恢復面板。',
|
||||
missingPane: paneId => `缺少面板:${paneId}`,
|
||||
editTitle: '版面配置',
|
||||
editHint: '選擇一個版面配置,或在區域之間拖曳面板。右鍵點擊區域可分割。',
|
||||
editHint: '選擇一個版面配置,或在區域之間拖曳面板。',
|
||||
reset: '重設',
|
||||
templates: '範本',
|
||||
custom: '自訂',
|
||||
|
|
|
|||
|
|
@ -2835,17 +2835,11 @@ export const zh: Translations = {
|
|||
closeToRight: '关闭右侧',
|
||||
closeAll: '全部关闭',
|
||||
newSessionTab: '新建会话标签',
|
||||
split: dir => `向${dir}拆分`,
|
||||
move: dir => `向${dir}移动`,
|
||||
dirUp: '上',
|
||||
dirDown: '下',
|
||||
dirLeft: '左',
|
||||
dirRight: '右',
|
||||
pluginDisabled: pluginId => `插件“${pluginId}”已禁用`,
|
||||
pluginDisabledBody: '在 设置 → 插件 中重新启用即可恢复面板。',
|
||||
missingPane: paneId => `缺少面板:${paneId}`,
|
||||
editTitle: '布局',
|
||||
editHint: '选择一个布局,或在区域之间拖动面板。右键点击区域可拆分。',
|
||||
editHint: '选择一个布局,或在区域之间拖动面板。',
|
||||
reset: '重置',
|
||||
templates: '模板',
|
||||
custom: '自定义',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue