From fe111bfe54cd7e2f38ab0033f3d6b7c6647ed06b Mon Sep 17 00:00:00 2001 From: leegisang Date: Tue, 16 Jun 2026 03:05:11 +0900 Subject: [PATCH 1/7] Fix workspace group header shortcuts --- .../ControlWorkspaceGroupContext.swift | 6 +- ...ontrolWorkspaceGroupCreateResolution.swift | 15 +-- ...ControlWorkspaceGroupFocusResolution.swift | 2 +- .../MobileSyncWorkspaceListResponse.swift | 3 +- .../MobileWorkspaceGroupPreview.swift | 12 +- .../MobileWorkspaceListItem.swift | 41 +++--- .../MobileWorkspaceListItemTests.swift | 27 ++-- .../WorkspaceGroupHeaderRow.swift | 15 ++- .../WorkspaceGroupCoordinator.swift | 52 +++----- .../Coordinators/WorkspaceGroupHosting.swift | 16 +-- .../Model/WorkspaceTabRepresenting.swift | 4 +- .../WorkspacesModel+GroupInvariants.swift | 8 +- .../Values/WorkspaceGroup.swift | 13 +- .../WorkspaceCoordinatorTests.swift | 39 +++--- .../App/TerminalDirectoryOpenSupport.swift | 36 ++++++ Sources/AppDelegate.swift | 17 ++- Sources/ContentView.swift | 9 +- Sources/SidebarWorkspaceGroupHeaderView.swift | 2 +- Sources/SidebarWorkspaceRenderItem.swift | 20 ++- Sources/TabManager+WindowTitle.swift | 10 -- Sources/TabManager.swift | 23 +--- ...troller+ControlWorkspaceGroupContext.swift | 13 +- .../VerticalTabsSidebar+WorkspaceGroups.swift | 23 ++-- Sources/cmuxApp.swift | 10 +- .../MobileWorkspaceListFidelityTests.swift | 9 +- cmuxTests/WorkspaceGroupTests.swift | 117 +++++++++++------- docs/workspace-groups.md | 12 +- web/messages/en.json | 14 +-- web/messages/ja.json | 14 +-- 29 files changed, 289 insertions(+), 293 deletions(-) diff --git a/Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/WorkspaceGroup/ControlWorkspaceGroupContext.swift b/Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/WorkspaceGroup/ControlWorkspaceGroupContext.swift index b5386766ded6..153945841a48 100644 --- a/Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/WorkspaceGroup/ControlWorkspaceGroupContext.swift +++ b/Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/WorkspaceGroup/ControlWorkspaceGroupContext.swift @@ -36,7 +36,7 @@ public protocol ControlWorkspaceGroupContext: AnyObject { /// Creates a workspace group for `workspace.group.create`. /// - /// The coordinator has already parsed `name` / `cwd`, resolved the child + /// The coordinator has already parsed `name` / legacy `cwd`, resolved the child /// handles to UUIDs, and surfaced the param-shape `invalid_params` failures; /// this runs the live-state remainder (fallback child selection, the /// target-window existence check, the all-children-are-anchors guard, and @@ -46,7 +46,9 @@ public protocol ControlWorkspaceGroupContext: AnyObject { /// - routing: The routing selectors used for TabManager resolution and the /// caller-workspace fallback. /// - name: The group name (already defaulted to "" when absent). - /// - cwd: The anchor working directory, if provided. + /// - cwd: Legacy create-time anchor working directory, if provided. + /// Group creation no longer creates a terminal workspace for the header, + /// so conformers may ignore it. /// - childWorkspaceIDs: The resolved child workspace ids, in request order /// (empty when none provided/resolved). /// - childrenExplicit: Whether the caller explicitly listed diff --git a/Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/WorkspaceGroup/ControlWorkspaceGroupCreateResolution.swift b/Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/WorkspaceGroup/ControlWorkspaceGroupCreateResolution.swift index 58e1c5ce3f46..a90540e6fc7f 100644 --- a/Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/WorkspaceGroup/ControlWorkspaceGroupCreateResolution.swift +++ b/Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/WorkspaceGroup/ControlWorkspaceGroupCreateResolution.swift @@ -3,10 +3,11 @@ public import Foundation /// The outcome of `workspace.group.create`, preserving the legacy body's /// app-state failures and the created group it echoes back. /// -/// The coordinator parses `name` / `cwd` / `child_workspace_ids` (resolving each -/// child through the handle registry) and surfaces the param-shape failures -/// (`invalid_params` for a malformed `child_workspace_ids` or unresolved -/// handles) itself. The remaining resolution depends on live app state — the +/// The coordinator parses `name` / legacy `cwd` / `child_workspace_ids` +/// (resolving each child through the handle registry) and surfaces the +/// param-shape failures (`invalid_params` for a malformed +/// `child_workspace_ids` or unresolved handles) itself. The remaining +/// resolution depends on live app state — the /// fallback selection when children are absent, the existence check against the /// target window, the all-children-are-anchors eligibility guard, and the create /// call — so it happens behind the seam and returns one of these cases. @@ -19,9 +20,9 @@ public enum ControlWorkspaceGroupCreateResolution: Sendable, Equatable { /// found in target window: …", `data: {"unknown_workspace_ids": …}`). /// Carries the missing workspace id strings, in request order. case childWorkspaceNotFound([String]) - /// Every explicitly-listed child is already a group anchor, so only an - /// anchor-only group could be created (legacy `invalid_state` / - /// `workspaceGroup.error.allChildrenAreAnchors`, `data: + /// Every explicitly-listed child is already a group anchor, so there is + /// no eligible child to use as the new group's ordering anchor (legacy + /// `invalid_state` / `workspaceGroup.error.allChildrenAreAnchors`, `data: /// {"ineligible_workspace_ids": …}`). Carries the ineligible workspace id /// strings. case allChildrenAreAnchors([String]) diff --git a/Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/WorkspaceGroup/ControlWorkspaceGroupFocusResolution.swift b/Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/WorkspaceGroup/ControlWorkspaceGroupFocusResolution.swift index 5c1cd61bfd10..19b156e5dbb4 100644 --- a/Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/WorkspaceGroup/ControlWorkspaceGroupFocusResolution.swift +++ b/Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/WorkspaceGroup/ControlWorkspaceGroupFocusResolution.swift @@ -6,7 +6,7 @@ public import Foundation /// The legacy body focused the owning window, made its TabManager active, then /// selected the group anchor through `selectWorkspace` (so the selection side /// effects fire). All of that is app state, so it runs behind the seam; the -/// coordinator mints the anchor workspace ref. +/// coordinator returns the existing anchor workspace ref. public enum ControlWorkspaceGroupFocusResolution: Sendable, Equatable { /// No TabManager resolved (legacy `unavailable` / "TabManager not /// available"). diff --git a/Packages/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileSyncWorkspaceListResponse.swift b/Packages/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileSyncWorkspaceListResponse.swift index 73a2da2ca417..ddd8da490896 100644 --- a/Packages/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileSyncWorkspaceListResponse.swift +++ b/Packages/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileSyncWorkspaceListResponse.swift @@ -71,8 +71,7 @@ public struct MobileSyncWorkspaceListResponse: Decodable, Sendable { public let isCollapsed: Bool /// Whether the group is pinned on the Mac. public let isPinned: Bool - /// The anchor workspace that owns this group. It is represented by the - /// group header and never rendered as a separate row. + /// The anchor workspace that positions this group in the workspace order. public let anchorWorkspaceID: String // The Mac also emits `member_workspace_ids`, but membership is derived on diff --git a/Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceGroupPreview.swift b/Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceGroupPreview.swift index de9add38c0d2..a99df9973f71 100644 --- a/Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceGroupPreview.swift +++ b/Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceGroupPreview.swift @@ -4,10 +4,11 @@ import Foundation /// mobile shell. /// /// Workspaces on the Mac can be organized into named, collapsible groups. An -/// anchor workspace owns each group; on the Mac sidebar the anchor renders as the -/// group header (no separate row), and collapsing the group hides its members but -/// keeps the header. The mobile shell mirrors those semantics. This is a pure -/// value model decoupled from any RPC or rendering concern. +/// anchor workspace positions each group; on the Mac sidebar the group header is +/// separate from the workspace rows while expanded, and collapsing the group +/// hides its members but keeps the header. The mobile shell mirrors those +/// semantics. This is a pure value model decoupled from any RPC or rendering +/// concern. public struct MobileWorkspaceGroupPreview: Identifiable, Equatable, Sendable { /// A stable, string-backed identifier for a ``MobileWorkspaceGroupPreview``. public struct ID: RawRepresentable, Hashable, Codable, Sendable, ExpressibleByStringLiteral { @@ -35,8 +36,7 @@ public struct MobileWorkspaceGroupPreview: Identifiable, Equatable, Sendable { public var isCollapsed: Bool /// Whether the group is pinned on the Mac. public var isPinned: Bool - /// The anchor workspace that owns this group. It is represented by the group - /// header and never rendered as a separate row. + /// The anchor workspace that positions this group. public var anchorWorkspaceID: MobileWorkspacePreview.ID /// Creates a workspace group preview. diff --git a/Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceListItem.swift b/Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceListItem.swift index 6fa3192c2ff7..f63cd09953b9 100644 --- a/Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceListItem.swift +++ b/Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceListItem.swift @@ -3,24 +3,21 @@ import Foundation /// One drawable item in the mobile workspace list. /// /// The mobile list mirrors the Mac sidebar's group semantics: a group is shown as -/// a header (representing its anchor workspace) followed by its non-anchor members; -/// collapsing a group hides its members but keeps the header; ungrouped workspaces +/// a header followed by its member workspace rows while expanded; collapsing a +/// group hides its members but keeps the header; ungrouped workspaces /// interleave inline by their position. This is a pure value type so the SwiftUI /// `List` can consume an immutable snapshot with no store reference below the list /// boundary. public enum MobileWorkspaceListItem: Identifiable, Equatable, Sendable { - /// A collapsible group header. The associated group's anchor workspace is - /// represented by this header and is never emitted as a separate - /// ``workspace`` item. + /// A collapsible group header. /// /// `hasUnread` is the header's aggregate unread state, mirroring the Mac - /// sidebar header badge: while the group is expanded it reflects only the - /// anchor workspace (visible member rows carry their own dots); while - /// collapsed it reflects the whole group, anchor included, so hidden - /// member activity is never silently swallowed. + /// sidebar header badge: expanded groups show unread state on visible + /// workspace rows; collapsed groups reflect the whole hidden group, anchor + /// included, so hidden member activity is never silently swallowed. case groupHeader(MobileWorkspaceGroupPreview, hasUnread: Bool) - /// A workspace row. `indented` is `true` for non-anchor members nested under - /// a group header, so the view can inset them. + /// A workspace row. `indented` is `true` for members nested under a group + /// header, so the view can inset them. case workspace(MobileWorkspacePreview, indented: Bool) /// A stable, list-unique identity for SwiftUI diffing. Namespaced by item @@ -40,7 +37,8 @@ public enum MobileWorkspaceListItem: Identifiable, Equatable, Sendable { /// Mirrors `SidebarWorkspaceRenderItem.renderItems` on the Mac: /// - Items follow `workspaces` order. A group header is emitted at the first /// member's position. - /// - The anchor workspace is never a separate row (the header represents it). + /// - Expanded groups keep every workspace, including the anchor, as a + /// separate row under the header. /// - When a group is collapsed, its members are skipped (header kept). /// - Ungrouped workspaces interleave inline by position. /// @@ -67,16 +65,12 @@ public enum MobileWorkspaceListItem: Identifiable, Equatable, Sendable { // Aggregate unread state per group up front (membership can be // non-contiguous, so this cannot be folded into the emit loop). - // Mirrors the Mac header badge: anchor-only while expanded, whole - // group (anchor included) while collapsed. - var anchorUnreadByGroupID: [MobileWorkspaceGroupPreview.ID: Bool] = [:] + // Mirrors the Mac header badge: no header badge while expanded because + // member rows are visible, whole group (anchor included) while collapsed. var anyMemberUnreadByGroupID: [MobileWorkspaceGroupPreview.ID: Bool] = [:] for workspace in workspaces { - guard let groupID = workspace.groupID, let group = groupsByID[groupID] else { continue } + guard let groupID = workspace.groupID, groupsByID[groupID] != nil else { continue } anyMemberUnreadByGroupID[groupID, default: false] = anyMemberUnreadByGroupID[groupID, default: false] || workspace.hasUnread - if group.anchorWorkspaceID == workspace.id { - anchorUnreadByGroupID[groupID] = workspace.hasUnread - } } var items: [MobileWorkspaceListItem] = [] @@ -94,20 +88,13 @@ public enum MobileWorkspaceListItem: Identifiable, Equatable, Sendable { if groupID != lastEmittedGroupID { lastEmittedGroupID = groupID if let groupID, let group = groupsByID[groupID], !emittedHeaders.contains(groupID) { - let hasUnread = group.isCollapsed - ? anyMemberUnreadByGroupID[groupID, default: false] - : anchorUnreadByGroupID[groupID, default: false] + let hasUnread = group.isCollapsed && anyMemberUnreadByGroupID[groupID, default: false] items.append(.groupHeader(group, hasUnread: hasUnread)) emittedHeaders.insert(groupID) collapsedByGroupID[groupID] = group.isCollapsed } } - if let groupID, let group = groupsByID[groupID], group.anchorWorkspaceID == workspace.id { - // Anchor is represented exclusively by the group header. - continue - } - let isCollapsed = groupID.map { collapsedByGroupID[$0] ?? false } ?? false if groupID == nil || !isCollapsed { items.append(.workspace(workspace, indented: groupID != nil)) diff --git a/Packages/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileWorkspaceListItemTests.swift b/Packages/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileWorkspaceListItemTests.swift index 45987322b6e2..930a8f5f14ab 100644 --- a/Packages/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileWorkspaceListItemTests.swift +++ b/Packages/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileWorkspaceListItemTests.swift @@ -44,15 +44,16 @@ import Testing ]) } - @Test func anchorRendersAsHeaderNotARow() { - // Anchor "a" owns group "g"; member "b" is nested. The anchor must not - // also appear as a workspace row. + @Test func expandedGroupRendersHeaderAndAnchorRow() { + // Anchor "a" positions group "g"; member "b" is nested. While + // expanded, both workspaces remain visible rows under the header. let items = MobileWorkspaceListItem.items( workspaces: [workspace("a", group: "g"), workspace("b", group: "g")], groups: [group("g", anchor: "a")] ) #expect(items == [ .groupHeader(group("g", anchor: "a"), hasUnread: false), + .workspace(workspace("a", group: "g"), indented: true), .workspace(workspace("b", group: "g"), indented: true), ]) } @@ -80,6 +81,7 @@ import Testing #expect(items == [ .workspace(workspace("top"), indented: false), .groupHeader(group("g", anchor: "anchor"), hasUnread: false), + .workspace(workspace("anchor", group: "g"), indented: true), .workspace(workspace("member", group: "g"), indented: true), .workspace(workspace("bottom"), indented: false), ]) @@ -95,12 +97,15 @@ import Testing #expect(items == [.workspace(workspace("a", group: "missing"), indented: false)]) } - @Test func anchorOnlyGroupRendersHeaderWithNoMembers() { + @Test func singleMemberGroupRendersHeaderAndWorkspace() { let items = MobileWorkspaceListItem.items( workspaces: [workspace("a", group: "g")], groups: [group("g", anchor: "a")] ) - #expect(items == [.groupHeader(group("g", anchor: "a"), hasUnread: false)]) + #expect(items == [ + .groupHeader(group("g", anchor: "a"), hasUnread: false), + .workspace(workspace("a", group: "g"), indented: true), + ]) } // MARK: Header unread aggregation (mirrors the Mac sidebar header badge) @@ -131,9 +136,9 @@ import Testing #expect(items == [.groupHeader(group("g", anchor: "a", collapsed: true), hasUnread: true)]) } - @Test func expandedGroupHeaderReflectsOnlyTheAnchor() { - // While expanded, member rows are visible and carry their own dots; - // the header only represents the anchor (matching the Mac header). + @Test func expandedGroupHeaderDoesNotDuplicateVisibleUnread() { + // While expanded, every workspace row is visible and carries its own + // dot, so the header does not duplicate unread state. let items = MobileWorkspaceListItem.items( workspaces: [ workspace("a", group: "g"), @@ -143,11 +148,12 @@ import Testing ) #expect(items == [ .groupHeader(group("g", anchor: "a"), hasUnread: false), + .workspace(workspace("a", group: "g"), indented: true), .workspace(workspace("b", group: "g", unread: true), indented: true), ]) } - @Test func expandedGroupHeaderCarriesUnreadAnchor() { + @Test func expandedGroupHeaderDoesNotCarryUnreadAnchor() { let items = MobileWorkspaceListItem.items( workspaces: [ workspace("a", group: "g", unread: true), @@ -156,7 +162,8 @@ import Testing groups: [group("g", anchor: "a")] ) #expect(items == [ - .groupHeader(group("g", anchor: "a"), hasUnread: true), + .groupHeader(group("g", anchor: "a"), hasUnread: false), + .workspace(workspace("a", group: "g", unread: true), indented: true), .workspace(workspace("b", group: "g"), indented: true), ]) } diff --git a/Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceGroupHeaderRow.swift b/Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceGroupHeaderRow.swift index 7cbf099fdb75..6377c7a06c7b 100644 --- a/Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceGroupHeaderRow.swift +++ b/Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceGroupHeaderRow.swift @@ -4,17 +4,16 @@ import SwiftUI /// A collapsible group-section header in the mobile workspace list. /// -/// Mirrors the Mac sidebar group header, which doubles as the group's anchor -/// workspace row: the leading disclosure chevron toggles collapse, while tapping -/// the name/body selects (and, in push navigation, opens) the anchor workspace. -/// The anchor is represented by this header and never rendered as a separate row, -/// so this split is what keeps the anchor's terminals reachable from the phone. +/// Mirrors the Mac sidebar group header: the leading disclosure chevron toggles +/// collapse, while tapping the name/body selects (and, in push navigation, +/// opens) the anchor workspace. +/// Expanded groups also render the anchor as a normal workspace row. struct WorkspaceGroupHeaderRow: View { let group: MobileWorkspaceGroupPreview /// Aggregate unread state for the header dot, computed by - /// `MobileWorkspaceListItem.items`: the anchor's unread while expanded, - /// the whole group's (anchor included) while collapsed, mirroring the Mac - /// sidebar header badge so collapsing a group never hides activity. + /// `MobileWorkspaceListItem.items`: no header badge while expanded because + /// workspace rows are visible, the whole group's unread while collapsed, + /// mirroring the Mac sidebar header badge so collapsing never hides activity. let hasUnread: Bool let navigationStyle: WorkspaceNavigationStyle /// Whether the anchor workspace is the current selection (sidebar style only). diff --git a/Packages/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift b/Packages/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift index 8442ea5765f0..9ca6e5f83044 100644 --- a/Packages/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift +++ b/Packages/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift @@ -5,7 +5,7 @@ internal import OSLog private let workspaceGroupLogger = Logger(subsystem: "com.cmuxterm.app", category: "WorkspaceGroupCoordinator") /// Sequences every workspace-group flow over the window's `WorkspacesModel`: -/// group creation (fresh anchor + member adoption), member add/remove, +/// group creation (member adoption), member add/remove, /// ungroup/delete, rename, collapse/pin/color/icon/anchor mutation, and /// group-slot moves — lifted one-for-one from the legacy TabManager method /// bodies. Workspace creation/teardown, selection moves, sidebar @@ -28,13 +28,10 @@ public final class WorkspaceGroupCoordinator { // MARK: - Creation - /// Create a new group, inserting a fresh anchor workspace above the given - /// child workspaces. Returns the new group id. + /// Create a new group around existing child workspaces. Returns the new group id. /// - /// The anchor is always brand new (never promoted from an existing - /// workspace). Its cwd defaults to `anchorWorkingDirectory`, or the first - /// eligible child's cwd, or whatever the host's workspace creation - /// resolves on its own. + /// The first eligible child becomes the group's anchor for ordering and + /// lifecycle purposes; no terminal workspace is created for the header. @discardableResult public func createWorkspaceGroup( name: String, @@ -59,43 +56,35 @@ public final class WorkspaceGroupCoordinator { ? nextAutoWorkspaceGroupName() : trimmedName - let firstChildTab = eligibleChildren.first.flatMap { firstId in - model.tabs.first(where: { $0.id == firstId }) - } - let inferredCwd: String? = anchorWorkingDirectory - ?? firstChildTab?.currentDirectory + guard let anchorId = eligibleChildren.first else { return nil } let originalTabOrder = model.tabs.map(\.id) - - let anchor = host.createGroupAnchorWorkspace( - title: resolvedName, - workingDirectory: inferredCwd, - inheritWorkingDirectory: inferredCwd == nil, - select: selectAnchor - ) + _ = anchorWorkingDirectory let group = WorkspaceGroup( id: UUID(), name: resolvedName, isCollapsed: false, isPinned: false, - anchorWorkspaceId: anchor.id, + anchorWorkspaceId: anchorId, customColor: nil, iconSymbol: nil ) model.workspaceGroups.append(group) - anchor.groupId = group.id for id in eligibleChildren { model.assignGroup(workspaceId: id, groupId: group.id) } placeNewWorkspaceGroupAtCreationPosition( groupId: group.id, - anchorId: anchor.id, childWorkspaceIds: eligibleChildren, originalTabOrder: originalTabOrder ) + if selectAnchor, + let anchor = model.tabs.first(where: { $0.id == anchorId }) { + host.selectWorkspace(anchor) + } // Collapse the sidebar multi-selection so a second ⌘⇧G press doesn't // immediately reuse the same child ids and create a duplicate group - // around them. The new anchor is the only sensible "current" + // around them. The promoted anchor is the only sensible "current" // selection at this point. Posts the hide notification so the // SwiftUI sidebar binding follows. // @@ -108,10 +97,10 @@ public final class WorkspaceGroupCoordinator { let hiddenIds = host.sidebarSelectedWorkspaceIds host.collapseSidebarSelectionForGroupCreation( hiddenWorkspaceIds: hiddenIds, - anchorId: anchor.id + anchorId: anchorId ) } - host.workspaceOrderDidChange(movedWorkspaceIds: [anchor.id] + eligibleChildren) + host.workspaceOrderDidChange(movedWorkspaceIds: eligibleChildren) return group.id } @@ -286,13 +275,10 @@ public final class WorkspaceGroupCoordinator { host?.workspaceOrderDidChange(movedWorkspaceIds: [workspaceId]) } - /// Dissolve a group while preserving every member workspace (including its - /// anchor) as a regular ungrouped workspace. Nothing is closed. The - /// former members KEEP their `tabs[]` positions so the anchor — which - /// was previously rendered exclusively as the group header — appears as - /// a workspace row at the same vertical spot the header occupied, with - /// the rest of the members staying right below it in their existing - /// relative order. We deliberately do not re-normalize here: that would + /// Dissolve a group while preserving every member workspace as a regular + /// ungrouped workspace. Nothing is closed. The former members KEEP their + /// `tabs[]` positions so ungrouping flattens the section in place. We + /// deliberately do not re-normalize here: that would /// push the now-ungrouped members down into the "ungrouped tier at the /// bottom" slot, which makes Ungroup feel like a destructive move /// instead of a flatten-in-place. @@ -575,7 +561,6 @@ public final class WorkspaceGroupCoordinator { /// position is the jump this creation path is avoiding. private func placeNewWorkspaceGroupAtCreationPosition( groupId: UUID, - anchorId: UUID, childWorkspaceIds: [UUID], originalTabOrder: [UUID] ) { @@ -591,7 +576,6 @@ public final class WorkspaceGroupCoordinator { desiredIds.reserveCapacity(model.tabs.count) for (index, id) in originalTabOrder.enumerated() { if index == insertionIndex { - desiredIds.append(anchorId) desiredIds.append(contentsOf: orderedChildIds) } if !childIdSet.contains(id) { diff --git a/Packages/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupHosting.swift b/Packages/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupHosting.swift index eeeb54ad803d..cb3dba5edb7d 100644 --- a/Packages/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupHosting.swift +++ b/Packages/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupHosting.swift @@ -9,25 +9,13 @@ public import CmuxSettings /// /// Synchronous two-way protocol for the same reason as `WorkspacesHosting`: /// every legacy group operation is one MainActor turn interleaving reads -/// and writes (creating the anchor re-enters the model through the tabs -/// willSet, selecting a workspace re-enters through the selection didSet). +/// and writes (selecting a workspace re-enters through the selection didSet). @MainActor public protocol WorkspaceGroupHosting: WorkspaceOrderHosting { /// The window's workspace ("tab") type; the app target's `Workspace`. associatedtype Tab: WorkspaceTabRepresenting // MARK: Workspace lifecycle (stays with the Workspace god object) - - /// Creates the fresh anchor workspace for a new group (legacy - /// `addWorkspace(title:workingDirectory:inheritWorkingDirectory:select: - /// placementOverride: .top, autoWelcomeIfNeeded: false, - /// normalizeWorkspaceGroupsAfterInsert: false)`). - func createGroupAnchorWorkspace( - title: String, - workingDirectory: String?, - inheritWorkingDirectory: Bool, - select: Bool - ) -> Tab /// Creates a member workspace for `createWorkspaceInGroup` (legacy /// `addWorkspace(workingDirectory:initialSurface:inheritWorkingDirectory: /// select:autoWelcomeIfNeeded: false)`). @@ -48,7 +36,7 @@ public protocol WorkspaceGroupHosting: WorkspaceOrderHosting { /// The current sidebar multi-selection. var sidebarSelectedWorkspaceIds: Set { get } - /// Collapses the sidebar multi-selection onto the fresh group anchor + /// Collapses the sidebar multi-selection onto the group anchor /// (legacy `replaceSelection(with: [anchorId])` + /// `postDidHide(hiddenWorkspaceIds:focusedWorkspaceId: anchorId)`). func collapseSidebarSelectionForGroupCreation( diff --git a/Packages/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspaceTabRepresenting.swift b/Packages/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspaceTabRepresenting.swift index f262c6f25347..a29e755467c5 100644 --- a/Packages/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspaceTabRepresenting.swift +++ b/Packages/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspaceTabRepresenting.swift @@ -18,7 +18,7 @@ public protocol WorkspaceTabRepresenting: AnyObject, Identifiable where ID == UU var groupId: UUID? { get set } /// Whether the workspace is pinned (pinned rows float above unpinned). var isPinned: Bool { get set } - /// The workspace's current working directory (group creation inherits - /// the anchor's / first child's cwd from this). + /// The workspace's current working directory (group actions use the + /// anchor's cwd as their reference). var currentDirectory: String { get } } diff --git a/Packages/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspacesModel+GroupInvariants.swift b/Packages/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspacesModel+GroupInvariants.swift index 07a68c275588..7b510c0f649f 100644 --- a/Packages/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspacesModel+GroupInvariants.swift +++ b/Packages/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspacesModel+GroupInvariants.swift @@ -96,10 +96,10 @@ extension WorkspacesModel { workspaceGroups[index].isCollapsed else { return } - // The anchor is the group header's visible representation, so - // focusing it doesn't hide it. Skip auto-expand when the focused - // workspace IS the group's anchor — that lets users work in the - // anchor while keeping the rest of the group folded away. + // When a group is collapsed, the anchor workspace's row is hidden + // behind the group header. Skip auto-expand when the focused workspace + // IS the group's anchor so users can keep a group folded while the + // anchor remains the selected workspace. guard workspaceGroups[index].anchorWorkspaceId != selectedTabId else { return } workspaceGroups[index].isCollapsed = false } diff --git a/Packages/CmuxWorkspaces/Sources/CmuxWorkspaces/Values/WorkspaceGroup.swift b/Packages/CmuxWorkspaces/Sources/CmuxWorkspaces/Values/WorkspaceGroup.swift index c46ae8deb218..4d7551c4bad7 100644 --- a/Packages/CmuxWorkspaces/Sources/CmuxWorkspaces/Values/WorkspaceGroup.swift +++ b/Packages/CmuxWorkspaces/Sources/CmuxWorkspaces/Values/WorkspaceGroup.swift @@ -3,12 +3,11 @@ public import Foundation /// Named collapsible sidebar group containing one or more workspaces. /// The membership relation lives on `Workspace.groupId`; this struct stores /// the group's identity, display name, collapse/pin state, and the explicit -/// anchor workspace whose lifecycle gates the group itself. +/// anchor workspace used to position the group in workspace order. /// -/// The anchor workspace is always a real member workspace. It is created -/// fresh when the group is created (never promoted from an existing member), -/// rendered IMPLICITLY as the group header (no separate sidebar row), and -/// when closed dissolves the group while keeping other members alive. +/// The anchor workspace is always a real member workspace. Creating a group +/// promotes the first selected child to be the anchor; it does not create a +/// separate terminal workspace for the folder header. public struct WorkspaceGroup: Identifiable, Equatable, Sendable { /// The group's stable identity. public let id: UUID @@ -18,9 +17,9 @@ public struct WorkspaceGroup: Identifiable, Equatable, Sendable { public var isCollapsed: Bool /// Whether the group is pinned. public var isPinned: Bool - /// Identifier of the member workspace that owns this group's lifecycle. + /// Identifier of the member workspace that positions this group. /// Always present and always points to a workspace in the window's tabs - /// whose `groupId == self.id`. Closing this workspace dissolves the group. + /// whose `groupId == self.id`. public var anchorWorkspaceId: UUID /// Group-level color override (hex string). When nil, falls back to the /// cwd-config color resolved from `cmux.json` for the anchor's cwd, then diff --git a/Packages/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceCoordinatorTests.swift b/Packages/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceCoordinatorTests.swift index a16923d8d4f6..24cb7a3c4f05 100644 --- a/Packages/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceCoordinatorTests.swift +++ b/Packages/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceCoordinatorTests.swift @@ -49,20 +49,6 @@ private final class StubGroupHost: WorkspaceGroupHosting { orderChanges.append(movedWorkspaceIds) } - func createGroupAnchorWorkspace( - title: String, - workingDirectory: String?, - inheritWorkingDirectory: Bool, - select: Bool - ) -> CoordinatorStubTab { - let tab = CoordinatorStubTab(currentDirectory: workingDirectory ?? "/tmp") - // Legacy addWorkspace(placementOverride: .top) inserts after pinned. - let pinnedCount = model.tabs.prefix(while: \.isPinned).count - model.tabs.insert(tab, at: pinnedCount) - if select { model.selectedTabId = tab.id } - return tab - } - func createWorkspaceForGroup( workingDirectory: String?, initialSurface: NewWorkspaceInitialSurface, @@ -213,11 +199,12 @@ struct WorkspaceCoordinatorTests { let group = try! #require(model.workspaceGroups.first(where: { $0.id == groupId })) #expect(group.name == "Group 1") let anchorId = group.anchorWorkspaceId + #expect(anchorId == child1.id) #expect(model.tabs.first(where: { $0.id == child1.id })?.groupId == groupId) #expect(model.tabs.first(where: { $0.id == child2.id })?.groupId == groupId) // Section is contiguous and anchor-first at the first child's slot. - #expect(model.tabs.map(\.id) == [anchorId, child1.id, child2.id, other.id]) - #expect(host.orderChanges.last == [anchorId, child1.id, child2.id]) + #expect(model.tabs.map(\.id) == [child1.id, child2.id, other.id]) + #expect(host.orderChanges.last == [child1.id, child2.id]) } @Test @@ -245,10 +232,11 @@ struct WorkspaceCoordinatorTests { let closed = groups.deleteWorkspaceGroup(groupId: groupId) - // Anchor + one member close for real; the final holdout is kept - // alive as an ungrouped workspace (closeWorkspace's last-tab guard). - #expect(closed == 2) - #expect(host.closedWorkspaceIds.count >= 2) + // The first existing member is the anchor. Closing it dissolves the + // group; the final holdout is kept alive as an ungrouped workspace + // (closeWorkspace's last-tab guard). + #expect(closed == 1) + #expect(host.closedWorkspaceIds.count == 1) #expect(model.workspaceGroups.isEmpty) #expect(model.tabs.count == 1) #expect(model.tabs[0].groupId == nil) @@ -274,17 +262,18 @@ struct WorkspaceCoordinatorTests { func collapseToggleMovesFocusToAnchorAndStripsHiddenSelection() { let (model, host, groups, _) = makeWorld() let a = CoordinatorStubTab() - model.tabs = [a] - let groupId = try! #require(groups.createWorkspaceGroup(name: "G", childWorkspaceIds: [a.id])) + let b = CoordinatorStubTab() + model.tabs = [a, b] + let groupId = try! #require(groups.createWorkspaceGroup(name: "G", childWorkspaceIds: [a.id, b.id])) let anchorId = model.workspaceGroups[0].anchorWorkspaceId - model.selectedTabId = a.id - host.sidebarSelectedWorkspaceIds = [a.id] + model.selectedTabId = b.id + host.sidebarSelectedWorkspaceIds = [b.id] groups.toggleWorkspaceGroupCollapsed(groupId: groupId) #expect(host.selectedWorkspaceIds == [anchorId]) #expect(host.subtractedSidebarSelections.count == 1) - #expect(host.subtractedSidebarSelections[0].hidden == [a.id]) + #expect(host.subtractedSidebarSelections[0].hidden == [b.id]) #expect(model.workspaceGroups[0].isCollapsed) } diff --git a/Sources/App/TerminalDirectoryOpenSupport.swift b/Sources/App/TerminalDirectoryOpenSupport.swift index 6c757ca3dff6..bf4115bc2669 100644 --- a/Sources/App/TerminalDirectoryOpenSupport.swift +++ b/Sources/App/TerminalDirectoryOpenSupport.swift @@ -939,6 +939,42 @@ enum WorkspaceShortcutMapper { } return nil } + + static func workspaceId(forDigit digit: Int, workspaceIds: [UUID]) -> UUID? { + guard let index = workspaceIndex(forDigit: digit, workspaceCount: workspaceIds.count) else { + return nil + } + return workspaceIds[index] + } + + static func digitForWorkspace(id workspaceId: UUID, workspaceIds: [UUID]) -> Int? { + guard let index = workspaceIds.firstIndex(of: workspaceId) else { return nil } + return digitForWorkspace(at: index, workspaceCount: workspaceIds.count) + } +} + +@MainActor +extension TabManager { + var numberedWorkspaceShortcutWorkspaceIds: [UUID] { + let groupsById = Dictionary(uniqueKeysWithValues: workspaceGroups.map { ($0.id, $0) }) + return SidebarWorkspaceRenderItem.numberedShortcutWorkspaceIds( + tabs: tabs, + groupsById: groupsById + ) + } + + @discardableResult + func selectWorkspaceByShortcutDigit(_ digit: Int) -> Bool { + guard let workspaceId = WorkspaceShortcutMapper.workspaceId( + forDigit: digit, + workspaceIds: numberedWorkspaceShortcutWorkspaceIds + ), + let workspace = tabs.first(where: { $0.id == workspaceId }) else { + return false + } + selectWorkspace(workspace) + return true + } } extension CommandPaletteContextKeys { diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index d9678801bec0..e367700382fd 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -13162,14 +13162,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent // goto_tab fallback from creating a new window when the index is out of bounds. if shortcutWhenClauseAllows(action: .selectWorkspaceByNumber, event: event), let digit = numberedConfiguredShortcutDigit(event: event, action: .selectWorkspaceByNumber) { - if let manager = tabManager, - let targetIndex = WorkspaceShortcutMapper.workspaceIndex(forDigit: digit, workspaceCount: manager.tabs.count) { + if let manager = tabManager { #if DEBUG cmuxDebugLog( - "shortcut.action name=workspaceDigit digit=\(digit) targetIndex=\(targetIndex) manager=\(debugManagerToken(manager)) \(debugShortcutRouteSnapshot(event: event))" + "shortcut.action name=workspaceDigit digit=\(digit) manager=\(debugManagerToken(manager)) \(debugShortcutRouteSnapshot(event: event))" ) #endif - manager.selectTab(at: targetIndex) + manager.selectWorkspaceByShortcutDigit(digit) } return true } @@ -14391,9 +14390,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent let resolvedTabManager: TabManager? = contextForMainWindow(targetWindow)?.tabManager ?? self.tabManager guard let tabManager = resolvedTabManager else { return false } let selectedSet = tabManager.sidebarSelectedWorkspaceIds - // sidebarSelectedWorkspaceIds is a Set; sort by tabs[] order so the - // anchor is placed before the first sidebar-visible selected workspace - // (createWorkspaceGroup uses the first child to position the anchor). + // sidebarSelectedWorkspaceIds is a Set; sort by tabs[] order so + // createWorkspaceGroup promotes the first selected workspace as the + // group's ordering anchor. let orderedSelectedIds: [UUID] = selectedSet.isEmpty ? [] : tabManager.tabs.compactMap { selectedSet.contains($0.id) ? $0.id : nil } @@ -14408,8 +14407,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent guard orderedSelectedIds.count >= 2 else { return false } let candidateIds: [UUID] = orderedSelectedIds // Match the workspace context-menu eligibility filter so the shortcut - // doesn't silently create an anchor-only group when every selected - // target is already an existing group's anchor. + // doesn't silently no-op when every selected target is already an + // existing group's anchor. let existingAnchorIds = Set(tabManager.workspaceGroups.map(\.anchorWorkspaceId)) let eligibleIds: [UUID] = candidateIds.filter { id in tabManager.tabs.contains(where: { $0.id == id }) && !existingAnchorIds.contains(id) diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 1162f8fbae73..7821eedc2b24 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -10610,6 +10610,7 @@ struct VerticalTabsSidebar: View { let workspaceGroupMenuSnapshot: WorkspaceGroupMenuSnapshot let workspaceRenderItems: [SidebarWorkspaceRenderItem] let visibleWorkspaceRowIds: [UUID] + let numberedWorkspaceShortcutIds: [UUID] var workspaceIds: [UUID] { tabIds } } @@ -10645,6 +10646,7 @@ struct VerticalTabsSidebar: View { groupsById: workspaceGroupById ) let visibleWorkspaceRowIds = workspaceRenderItems.map(\.rowWorkspaceId) + let numberedWorkspaceShortcutIds = workspaceRenderItems.compactMap(\.numberedShortcutWorkspaceId) let draggedSidebarTabId = dragState.draggedTabId let sidebarReorderIds = draggedSidebarTabId.map { tabManager.sidebarReorderWorkspaceIds( @@ -10671,7 +10673,8 @@ struct VerticalTabsSidebar: View { workspaceGroupById: workspaceGroupById, workspaceGroupMenuSnapshot: workspaceGroupMenuSnapshot, workspaceRenderItems: workspaceRenderItems, - visibleWorkspaceRowIds: visibleWorkspaceRowIds + visibleWorkspaceRowIds: visibleWorkspaceRowIds, + numberedWorkspaceShortcutIds: numberedWorkspaceShortcutIds ) ZStack(alignment: .bottomLeading) { @@ -12318,8 +12321,8 @@ struct VerticalTabsSidebar: View { index: index, isActive: tabManager.selectedTabId == tab.id, workspaceShortcutDigit: WorkspaceShortcutMapper.digitForWorkspace( - at: index, - workspaceCount: renderContext.workspaceCount + id: tab.id, + workspaceIds: renderContext.numberedWorkspaceShortcutIds ), workspaceShortcutModifierSymbol: renderContext.workspaceNumberShortcut.numberedDigitHintPrefix, canCloseWorkspace: renderContext.canCloseWorkspace, diff --git a/Sources/SidebarWorkspaceGroupHeaderView.swift b/Sources/SidebarWorkspaceGroupHeaderView.swift index 6bd30c1b7187..5e01fa75256a 100644 --- a/Sources/SidebarWorkspaceGroupHeaderView.swift +++ b/Sources/SidebarWorkspaceGroupHeaderView.swift @@ -4,7 +4,7 @@ import CmuxFoundation import SwiftUI import CmuxSettings -/// Collapsible group header that doubles as the anchor workspace row. +/// Collapsible group header for a workspace group. struct SidebarWorkspaceGroupHeaderView: View, Equatable { // Closures and delegate factories are excluded because they are recreated // by the parent on each evaluation. The scalar snapshots below are the diff --git a/Sources/SidebarWorkspaceRenderItem.swift b/Sources/SidebarWorkspaceRenderItem.swift index 64163b27121f..1e7b1e322f8d 100644 --- a/Sources/SidebarWorkspaceRenderItem.swift +++ b/Sources/SidebarWorkspaceRenderItem.swift @@ -39,6 +39,15 @@ enum SidebarWorkspaceRenderItem { } } + var numberedShortcutWorkspaceId: UUID? { + switch self { + case .groupHeader: + return nil + case .workspace(let workspace): + return workspace.id + } + } + static func renderItems( tabs: [Workspace], groupsById: [UUID: WorkspaceGroup] @@ -73,14 +82,17 @@ enum SidebarWorkspaceRenderItem { skipChildrenUntilNextGroup = collapsedByGroupId[groupId] ?? false } } - // Anchor workspaces are represented exclusively by the group header. - if let groupId, let group = groupsById[groupId], group.anchorWorkspaceId == tab.id { - continue - } if groupId == nil || !skipChildrenUntilNextGroup { items.append(.workspace(tab)) } } return items } + + static func numberedShortcutWorkspaceIds( + tabs: [Workspace], + groupsById: [UUID: WorkspaceGroup] + ) -> [UUID] { + renderItems(tabs: tabs, groupsById: groupsById).compactMap(\.numberedShortcutWorkspaceId) + } } diff --git a/Sources/TabManager+WindowTitle.swift b/Sources/TabManager+WindowTitle.swift index 90ae5665de07..167aa185b025 100644 --- a/Sources/TabManager+WindowTitle.swift +++ b/Sources/TabManager+WindowTitle.swift @@ -28,17 +28,7 @@ extension TabManager { /// The name to display for `tab` across window chrome — the custom title /// bar, `NSWindow.title`, and the toolbar command label. - /// - /// A workspace group's anchor is represented everywhere by the group itself - /// (the sidebar draws only the group header, never a separate anchor row, - /// per `SidebarWorkspaceRenderItem`), so for an anchor the single source of - /// truth for the displayed name is the group's `name`. The anchor's own - /// `title` is merely seeded equal to the group name at creation and would - /// otherwise drift when the group is renamed. func resolvedWorkspaceDisplayTitle(for tab: Workspace) -> String { - if let group = workspaceGroups.first(where: { $0.anchorWorkspaceId == tab.id }) { - return group.name - } return tab.title } diff --git a/Sources/TabManager.swift b/Sources/TabManager.swift index 0221887edcb5..2c7f8acb0d9d 100644 --- a/Sources/TabManager.swift +++ b/Sources/TabManager.swift @@ -1860,23 +1860,6 @@ class TabManager: ObservableObject { // MARK: - WorkspaceGroupHosting (effects the group coordinator inverts) - func createGroupAnchorWorkspace( - title: String, - workingDirectory: String?, - inheritWorkingDirectory: Bool, - select: Bool - ) -> Workspace { - addWorkspace( - title: title, - workingDirectory: workingDirectory, - inheritWorkingDirectory: inheritWorkingDirectory, - select: select, - placementOverride: .top, - autoWelcomeIfNeeded: false, - normalizeWorkspaceGroupsAfterInsert: false - ) - } - func createWorkspaceForGroup( workingDirectory: String?, initialSurface: NewWorkspaceInitialSurface, @@ -6102,10 +6085,8 @@ extension Notification.Name { static let terminalPortalVisibilityDidChange = Notification.Name("cmux.terminalPortalVisibilityDidChange") static let browserPortalRegistryDidChange = Notification.Name("cmux.browserPortalRegistryDidChange") static let workspaceOrderDidChange = Notification.Name("cmux.workspaceOrderDidChange") - /// Posted when an existing workspace group's `name` changes (rename). The - /// imperatively-cached window-chrome surfaces (custom title bar in - /// `ContentView`, toolbar command label in `WindowToolbarController`) read - /// a grouped anchor's displayed name from `group.name` and refresh on this. + /// Posted when an existing workspace group's `name` changes (rename). + /// Sidebar group headers and any cached chrome surfaces refresh on this. static let workspaceGroupNameDidChange = Notification.Name("cmux.workspaceGroupNameDidChange") static let workspaceCurrentDirectoryDidChange = Notification.Name("cmux.workspaceCurrentDirectoryDidChange") static let tabManagerFocusHistoryRevisionDidChange = Notification.Name("cmux.tabManagerFocusHistoryRevisionDidChange") diff --git a/Sources/TerminalController+ControlWorkspaceGroupContext.swift b/Sources/TerminalController+ControlWorkspaceGroupContext.swift index a058e77e57f5..d24123a2b2a5 100644 --- a/Sources/TerminalController+ControlWorkspaceGroupContext.swift +++ b/Sources/TerminalController+ControlWorkspaceGroupContext.swift @@ -71,7 +71,8 @@ extension TerminalController: ControlWorkspaceGroupContext { // Default behavior when children were absent: group the active sidebar // selection, or fall back to the caller workspace_id, or the focused - // workspace. (An explicit empty array still creates an anchor-only group.) + // workspace. An explicit empty array now fails with not_created rather + // than creating a terminal just to serve as a group header. let parsedChildIds: [UUID] if childrenExplicit { parsedChildIds = childWorkspaceIDs @@ -91,7 +92,7 @@ extension TerminalController: ControlWorkspaceGroupContext { // A syntactically valid UUID can still reference a workspace that doesn't // exist in this TabManager. Surface those instead of silently dropping - // them into an anchor-only group. + // them and creating a group around only the remaining ids. let knownTabIds = Set(tabManager.tabs.map(\.id)) let missing: [String] = parsedChildIds.compactMap { id in knownTabIds.contains(id) ? nil : id.uuidString @@ -101,9 +102,8 @@ extension TerminalController: ControlWorkspaceGroupContext { } let childIds = parsedChildIds - // When the caller explicitly listed children, refuse to create an - // anchor-only group if every one of them was already an anchor of - // another group. + // When the caller explicitly listed children, refuse the request if + // every one of them was already an anchor of another group. if childrenExplicit, !parsedChildIds.isEmpty { let existingAnchorIds = Set(tabManager.workspaceGroups.map(\.anchorWorkspaceId)) let ineligible: [String] = parsedChildIds.compactMap { id -> String? in @@ -123,6 +123,9 @@ extension TerminalController: ControlWorkspaceGroupContext { let createdGroupId = tabManager.createWorkspaceGroup( name: name, childWorkspaceIds: childIds, + // Kept for socket compatibility. Group creation promotes an + // existing workspace as the anchor now, so there is no new header + // terminal whose cwd should be initialized from this value. anchorWorkingDirectory: cwd, selectAnchor: false, collapseSidebarSelection: false diff --git a/Sources/VerticalTabsSidebar+WorkspaceGroups.swift b/Sources/VerticalTabsSidebar+WorkspaceGroups.swift index 8ab727715b08..cd7ac64caeed 100644 --- a/Sources/VerticalTabsSidebar+WorkspaceGroups.swift +++ b/Sources/VerticalTabsSidebar+WorkspaceGroups.swift @@ -14,7 +14,7 @@ extension VerticalTabsSidebar { showModifierHoldHints: Bool ) -> some View { let settings = renderContext.tabItemSettings - let isAnchorActive = tabManager.selectedTabId == group.anchorWorkspaceId + let isAnchorActive = group.isCollapsed && tabManager.selectedTabId == group.anchorWorkspaceId let anchorCwd = renderContext.workspaceById[group.anchorWorkspaceId]?.currentDirectory let resolvedConfig = cmuxConfigStore.resolveWorkspaceGroupConfig(forCwd: anchorCwd) let effectiveColor = group.customColor ?? resolvedConfig?.color @@ -25,20 +25,13 @@ extension VerticalTabsSidebar { let cwdContextMenuItems = resolvedConfig?.contextMenuItems ?? [] let newWorkspacePlacement = resolvedConfig?.newWorkspacePlacement let anchorUnreadCount: Int = { - if group.isCollapsed { - return memberWorkspaceIds.reduce(0) { partial, workspaceId in - partial + notificationStore.unreadCount(forTabId: workspaceId) - } + guard group.isCollapsed else { return 0 } + return memberWorkspaceIds.reduce(0) { partial, workspaceId in + partial + notificationStore.unreadCount(forTabId: workspaceId) } - return notificationStore.unreadCount(forTabId: group.anchorWorkspaceId) }() - let anchorIndex = renderContext.tabIndexById[group.anchorWorkspaceId] ?? 0 - let shortcutDigit = WorkspaceShortcutMapper.digitForWorkspace( - at: anchorIndex, - workspaceCount: renderContext.workspaceCount - ) let modifierSymbol = renderContext.workspaceNumberShortcut.numberedDigitHintPrefix - let showsHintForAnchor = showModifierHoldHints && modifierKeyMonitor.isModifierPressed + let showsHintForAnchor = false let topDropIndicatorVisible = SidebarTabDropIndicatorPredicate().topVisible( forTabId: group.anchorWorkspaceId, draggedTabId: dragState.draggedTabId, @@ -91,7 +84,7 @@ extension VerticalTabsSidebar { isAnchorActive: isAnchorActive, memberCount: memberWorkspaceIds.count, anchorUnreadCount: anchorUnreadCount, - shortcutDigit: shortcutDigit, + shortcutDigit: nil, shortcutModifierSymbol: modifierSymbol, showsShortcutHint: showsHintForAnchor, shortcutHintXOffset: settings.sidebarShortcutHintXOffset, @@ -161,12 +154,12 @@ extension VerticalTabsSidebar { } ) .equatable() - .id(group.anchorWorkspaceId) + .id(group.isCollapsed ? group.anchorWorkspaceId : group.id) .accessibilityIdentifier("sidebarWorkspaceGroup.\(group.id.uuidString)") header .sidebarWorkspaceFrameAnchor( - id: group.anchorWorkspaceId, + id: group.isCollapsed ? group.anchorWorkspaceId : group.id, isEnabled: shouldCollectWorkspaceDropTargets ) } diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index 7d2f315e2493..72dd828cdc2f 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -1050,17 +1050,11 @@ struct cmuxApp: App { let selectWorkspaceByNumberShortcut = menuShortcut(for: .selectWorkspaceByNumber) if selectWorkspaceByNumberShortcut.isUnbound || selectWorkspaceByNumberShortcut.hasChord { Button(String(localized: "menu.view.workspace", defaultValue: "Workspace \(number)")) { - let manager = activeTabManager - if let targetIndex = WorkspaceShortcutMapper.workspaceIndex(forDigit: number, workspaceCount: manager.tabs.count) { - manager.selectTab(at: targetIndex) - } + activeTabManager.selectWorkspaceByShortcutDigit(number) } } else { Button(String(localized: "menu.view.workspace", defaultValue: "Workspace \(number)")) { - let manager = activeTabManager - if let targetIndex = WorkspaceShortcutMapper.workspaceIndex(forDigit: number, workspaceCount: manager.tabs.count) { - manager.selectTab(at: targetIndex) - } + activeTabManager.selectWorkspaceByShortcutDigit(number) } .keyboardShortcut( KeyEquivalent(Character("\(number)")), diff --git a/cmuxTests/MobileWorkspaceListFidelityTests.swift b/cmuxTests/MobileWorkspaceListFidelityTests.swift index ad4a5b033e93..d7e5dcb82ba4 100644 --- a/cmuxTests/MobileWorkspaceListFidelityTests.swift +++ b/cmuxTests/MobileWorkspaceListFidelityTests.swift @@ -149,8 +149,13 @@ struct MobileWorkspaceListFidelityTests { @Test func movingWorkspaceBetweenGroupsChangesObserverHash() throws { let manager = TabManager() let member = try #require(manager.selectedWorkspace) - // A real group with its own anchor; the member starts ungrouped. - let groupId = try #require(manager.createWorkspaceGroup(name: "Group A")) + let groupAnchor = manager.addWorkspace(autoWelcomeIfNeeded: false) + // A real group with an existing workspace anchor; the member starts ungrouped. + let groupId = try #require(manager.createWorkspaceGroup( + name: "Group A", + childWorkspaceIds: [groupAnchor.id], + selectAnchor: false + )) #expect(member.groupId == nil) let before = MobileWorkspaceListObserver.summaryHashForTesting( diff --git a/cmuxTests/WorkspaceGroupTests.swift b/cmuxTests/WorkspaceGroupTests.swift index c6b94599ee12..ab2de7c35009 100644 --- a/cmuxTests/WorkspaceGroupTests.swift +++ b/cmuxTests/WorkspaceGroupTests.swift @@ -21,23 +21,23 @@ struct WorkspaceGroupTests { return manager } - @Test func createGroupInsertsFreshAnchorAndGroupsChildren() throws { + @Test func createGroupPromotesFirstChildAsAnchorWithoutCreatingWorkspace() throws { let manager = makeTabManager() let children = manager.tabs.map(\.id) let initialCount = manager.tabs.count let gid = manager.createWorkspaceGroup(name: "Test Group", childWorkspaceIds: children) #expect(gid != nil) - #expect(manager.tabs.count == initialCount + 1) + #expect(manager.tabs.count == initialCount) let groupId = try #require(gid) let group = try #require(manager.workspaceGroups.first(where: { $0.id == groupId })) #expect(group.name == "Test Group") #expect(!group.isCollapsed) #expect(!group.isPinned) - #expect(manager.tabs.contains(where: { $0.id == group.anchorWorkspaceId })) + #expect(group.anchorWorkspaceId == children[0]) let membersIds = manager.tabs.filter { $0.groupId == groupId }.map(\.id) - #expect(membersIds.count == children.count + 1) + #expect(membersIds.count == children.count) #expect(membersIds.contains(group.anchorWorkspaceId)) for childId in children { #expect(membersIds.contains(childId)) @@ -57,9 +57,9 @@ struct WorkspaceGroupTests { #expect(reorderedIds[0] == originalIds[0]) #expect(reorderedIds[1] == originalIds[1]) - #expect(reorderedIds[2] == group.anchorWorkspaceId) - #expect(reorderedIds[3] == originalIds[2]) - #expect(reorderedIds[4] == originalIds[3]) + #expect(group.anchorWorkspaceId == originalIds[2]) + #expect(reorderedIds[2] == originalIds[2]) + #expect(reorderedIds[3] == originalIds[3]) } @Test func draggingGroupHeaderReordersAmongTopLevelWorkspaces() throws { @@ -88,7 +88,6 @@ struct WorkspaceGroupTests { #expect(manager.tabs.map(\.id) == [ originalIds[0], originalIds[2], - group.anchorWorkspaceId, originalIds[1], originalIds[3], ]) @@ -135,7 +134,6 @@ struct WorkspaceGroupTests { #expect(manager.tabs.first { $0.id == originalIds[0] }?.groupId == nil) #expect(manager.tabs.map(\.id) == [ group.anchorWorkspaceId, - originalIds[1], originalIds[2], originalIds[0], originalIds[3], @@ -194,7 +192,6 @@ struct WorkspaceGroupTests { } #expect(groupMemberIds == [ - group.anchorWorkspaceId, originalIds[1], originalIds[2], ]) @@ -207,6 +204,50 @@ struct WorkspaceGroupTests { ]) } + @Test func numberedWorkspaceShortcutsSkipGroupHeadersAndCollapsedMembers() throws { + let manager = makeTabManager() + manager.addWorkspace(autoWelcomeIfNeeded: false) + manager.addWorkspace(autoWelcomeIfNeeded: false) + let originalIds = manager.tabs.map(\.id) + + let collapsedGroupId = try #require(manager.createWorkspaceGroup( + name: "sun", + childWorkspaceIds: [originalIds[0], originalIds[1]], + selectAnchor: false + )) + manager.toggleWorkspaceGroupCollapsed(groupId: collapsedGroupId) + let collapsedGroup = try #require(manager.workspaceGroups.first { $0.id == collapsedGroupId }) + + let expandedGroupId = try #require(manager.createWorkspaceGroup( + name: "research", + childWorkspaceIds: [originalIds[2]], + selectAnchor: false + )) + let expandedGroup = try #require(manager.workspaceGroups.first { $0.id == expandedGroupId }) + + let shortcutIds = manager.numberedWorkspaceShortcutWorkspaceIds + #expect(shortcutIds == [ + originalIds[2], + originalIds[3], + originalIds[4], + ]) + #expect(!shortcutIds.contains(collapsedGroup.anchorWorkspaceId)) + #expect(!shortcutIds.contains(originalIds[0])) + #expect(!shortcutIds.contains(originalIds[1])) + #expect(shortcutIds.contains(expandedGroup.anchorWorkspaceId)) + #expect(WorkspaceShortcutMapper.digitForWorkspace( + id: originalIds[2], + workspaceIds: shortcutIds + ) == 1) + + #expect(manager.selectWorkspaceByShortcutDigit(1)) + #expect(manager.selectedTabId == originalIds[2]) + #expect(manager.selectWorkspaceByShortcutDigit(2)) + #expect(manager.selectedTabId == originalIds[3]) + #expect(manager.selectWorkspaceByShortcutDigit(9)) + #expect(manager.selectedTabId == originalIds[4]) + } + @Test func groupHeaderEdgeDropUsesTopLevelIndicatorScope() throws { let manager = makeTabManager() manager.addWorkspace(autoWelcomeIfNeeded: false) @@ -261,7 +302,7 @@ struct WorkspaceGroupTests { originalIds[2], ])) let group = try #require(manager.workspaceGroups.first { $0.id == groupId }) - let draggedId = originalIds[1] + let draggedId = originalIds[2] let targetId = originalIds[0] let usesTopLevelRows = manager.sidebarReorderUsesTopLevelRows( forDraggedWorkspaceId: draggedId, @@ -298,7 +339,6 @@ struct WorkspaceGroupTests { originalIds[0], draggedId, group.anchorWorkspaceId, - originalIds[2], originalIds[3], ]) } @@ -309,12 +349,15 @@ struct WorkspaceGroupTests { manager.addWorkspace(autoWelcomeIfNeeded: false) let originalIds = manager.tabs.map(\.id) - let firstPinnedId = try #require(manager.createWorkspaceGroup(name: "Pinned A", childWorkspaceIds: [originalIds[1]])) + let firstPinnedId = try #require(manager.createWorkspaceGroup(name: "Pinned A", childWorkspaceIds: [ + originalIds[1], + originalIds[3], + ])) manager.toggleWorkspaceGroupPinned(groupId: firstPinnedId) let secondPinnedId = try #require(manager.createWorkspaceGroup(name: "Pinned B", childWorkspaceIds: [originalIds[2]])) manager.toggleWorkspaceGroupPinned(groupId: secondPinnedId) - let newGroupId = try #require(manager.createWorkspaceGroup(name: "Unpinned", childWorkspaceIds: [originalIds[1]])) + let newGroupId = try #require(manager.createWorkspaceGroup(name: "Unpinned", childWorkspaceIds: [originalIds[3]])) let newGroup = try #require(manager.workspaceGroups.first { $0.id == newGroupId }) let pinnedGroupIds = Set(manager.workspaceGroups.filter(\.isPinned).map(\.id)) let lastPinnedIndex = try #require(manager.tabs.lastIndex { tab in @@ -341,7 +384,6 @@ struct WorkspaceGroupTests { #expect(manager.tabs.map(\.id) == [ group.anchorWorkspaceId, originalIds[1], - originalIds[0], originalIds[2], ]) } @@ -365,8 +407,8 @@ struct WorkspaceGroupTests { #expect(Array(manager.tabs.map(\.id).prefix(3)) == [ pinnedGroup.anchorWorkspaceId, - originalIds[2], unpinnedGroup.anchorWorkspaceId, + originalIds[1], ]) } @@ -386,9 +428,8 @@ struct WorkspaceGroupTests { manager.moveTabToTop(originalIds[2]) - #expect(Array(manager.tabs.map(\.id).prefix(3)) == [ + #expect(Array(manager.tabs.map(\.id).prefix(2)) == [ group.anchorWorkspaceId, - originalIds[2], originalIds[0], ]) } @@ -409,9 +450,8 @@ struct WorkspaceGroupTests { manager.moveTabsToTop([originalIds[2]]) - #expect(Array(manager.tabs.map(\.id).prefix(3)) == [ + #expect(Array(manager.tabs.map(\.id).prefix(2)) == [ group.anchorWorkspaceId, - originalIds[2], originalIds[0], ]) } @@ -436,7 +476,6 @@ struct WorkspaceGroupTests { #expect(manager.tabs.filter { $0.groupId == groupId }.map(\.id) == [ group.anchorWorkspaceId, originalIds[3], - originalIds[1], originalIds[2], ]) } @@ -463,7 +502,6 @@ struct WorkspaceGroupTests { originalIds[1], group.anchorWorkspaceId, originalIds[3], - originalIds[2], ]) #expect(!group.isPinned) #expect(pinnedChild.groupId == groupId) @@ -531,11 +569,9 @@ struct WorkspaceGroupTests { manager.moveTabToTopForNotification(originalIds[2]) - #expect(Array(manager.tabs.map(\.id).prefix(4)) == [ + #expect(Array(manager.tabs.map(\.id).prefix(2)) == [ secondGroup.anchorWorkspaceId, - originalIds[2], firstGroup.anchorWorkspaceId, - originalIds[0], ]) #expect(Array(manager.workspaceGroups.map(\.id).prefix(2)) == [ secondGroupId, @@ -557,7 +593,6 @@ struct WorkspaceGroupTests { #expect(manager.tabs.map(\.id) == [ originalIds[0], group.anchorWorkspaceId, - originalIds[1], originalIds[3], originalIds[2], ]) @@ -578,7 +613,6 @@ struct WorkspaceGroupTests { originalIds[1], group.anchorWorkspaceId, originalIds[0], - originalIds[2], originalIds[3], ]) } @@ -606,7 +640,6 @@ struct WorkspaceGroupTests { #expect(inserted.groupId == groupId) #expect(manager.tabs.filter { $0.groupId == groupId }.map(\.id) == [ group.anchorWorkspaceId, - originalIds[1], originalIds[2], inserted.id, originalIds[3], @@ -634,7 +667,6 @@ struct WorkspaceGroupTests { #expect(manager.tabs.filter { $0.groupId == groupId }.map(\.id) == [ group.anchorWorkspaceId, inserted.id, - originalIds[1], originalIds[2], ]) } @@ -659,7 +691,6 @@ struct WorkspaceGroupTests { #expect(manager.tabs.filter { $0.groupId == groupId }.map(\.id) == [ group.anchorWorkspaceId, - originalIds[1], originalIds[0], originalIds[2], ]) @@ -684,7 +715,7 @@ struct WorkspaceGroupTests { let manager = makeTabManager() let children = manager.tabs.map(\.id) let groupId = manager.createWorkspaceGroup(name: "G", childWorkspaceIds: children)! - let firstChild = children[0] + let firstChild = children[1] manager.removeWorkspaceFromGroup(workspaceId: firstChild) @@ -876,37 +907,31 @@ struct WorkspaceGroupTests { #expect(RenderableSystemSymbol.resolvedSurfaceTabIcon(" ") == "doc.text") } - // Regression for #5404: renaming a group must update the name shown in - // window chrome (the custom title bar / NSWindow title / toolbar label), - // not just the sidebar header. The chrome derives a grouped anchor's - // displayed name from `resolvedWorkspaceDisplayTitle(for:)`, which must - // track the group's `name` — the single source of truth — rather than the - // anchor's own (stale) title that was merely seeded at creation. - @Test func renamingGroupUpdatesAnchorDisplayTitle() throws { + @Test func renamingGroupLeavesAnchorWorkspaceTitleAlone() throws { let manager = makeTabManager() + let anchorId = manager.tabs[0].id + let originalAnchor = try #require(manager.tabs.first { $0.id == anchorId }) + let anchorTitle = originalAnchor.title let groupId = try #require( - manager.createWorkspaceGroup(name: "Group 1", childWorkspaceIds: [manager.tabs[0].id]) + manager.createWorkspaceGroup(name: "Group 1", childWorkspaceIds: [anchorId]) ) let group = try #require(manager.workspaceGroups.first { $0.id == groupId }) let anchor = try #require(manager.tabs.first { $0.id == group.anchorWorkspaceId }) - // Sanity: the anchor's displayed title starts at the group name. - #expect(manager.resolvedWorkspaceDisplayTitle(for: anchor) == "Group 1") + #expect(manager.resolvedWorkspaceDisplayTitle(for: anchor) == anchorTitle) manager.renameWorkspaceGroup(groupId: groupId, name: "AUSTIN GENERAL INTELLIGENCE") - // The chrome's source of truth must reflect the rename. #expect(manager.workspaceGroups.first { $0.id == groupId }?.name == "AUSTIN GENERAL INTELLIGENCE") - #expect(manager.resolvedWorkspaceDisplayTitle(for: anchor) == "AUSTIN GENERAL INTELLIGENCE") + #expect(manager.resolvedWorkspaceDisplayTitle(for: anchor) == anchorTitle) } - // A non-anchor workspace keeps its own title; only the anchor mirrors the - // group name. Guards against the derivation over-reaching to every member. @Test func renamingGroupLeavesNonAnchorMemberTitleAlone() throws { let manager = makeTabManager() + let anchorId = manager.tabs[0].id let memberId = manager.tabs[1].id let groupId = try #require( - manager.createWorkspaceGroup(name: "Group 1", childWorkspaceIds: [memberId]) + manager.createWorkspaceGroup(name: "Group 1", childWorkspaceIds: [anchorId, memberId]) ) let member = try #require(manager.tabs.first { $0.id == memberId }) let memberTitle = member.title diff --git a/docs/workspace-groups.md b/docs/workspace-groups.md index 56dae9ebeb13..8453ba1bc939 100644 --- a/docs/workspace-groups.md +++ b/docs/workspace-groups.md @@ -1,20 +1,20 @@ # Workspace Groups -Workspace groups let you nest workspaces into collapsible named sections in the sidebar. Each group has an implicit "anchor workspace," a customizable `+` button for spawning new workspaces inside it, and right-click actions for renaming, pinning, ungrouping, and editing its configuration. +Workspace groups let you nest workspaces into collapsible named sections in the sidebar. Each group has a folder-style header, an implicit "anchor workspace" used for ordering, a customizable `+` button for spawning new workspaces inside it, and right-click actions for renaming, pinning, ungrouping, and editing its configuration. ## Concepts ### Anchor workspace -Every group is owned by exactly one workspace called the **anchor**. The group header in the sidebar IS the anchor's representation — there is no separate row for it. Clicking the header name area focuses the anchor's panels. Clicking the chevron toggles collapse. +Every group has exactly one workspace called the **anchor**. The anchor is an existing member workspace used to position the group and provide a reference working directory. The group header is its own folder-style row; when the group is expanded, the anchor remains visible as a normal workspace row under the header. Clicking the chevron toggles collapse. -Anchors are always brand new when a group is created. They are never promoted from an existing workspace. The anchor's working directory is inherited from the first selected workspace (when grouping a selection) or from the active workspace (when creating via the CLI without `--cwd`). +When a group is created, cmux promotes the first eligible selected workspace to be the anchor. It does not create a new terminal workspace just to act as the group header. Closing the anchor workspace **dissolves the group**: every other member loses its `groupId` and stays in the tabs list as an ungrouped workspace. Nothing is closed besides the anchor itself. The app shows a confirm dialog with a "Don't ask again" toggle before this happens. ### Group identity -A group has a `name`, `iconSymbol` (an SF Symbol, default `folder.fill`), and an optional `customColor` (hex string). Both are independent of the anchor workspace's own customizations. The anchor's color and icon are seeded from the group on creation, but they can diverge afterwards. +A group has a `name`, `iconSymbol` (an SF Symbol, default `folder.fill`), and an optional `customColor` (hex string). These are independent of the anchor workspace's own customizations. ### Pinning @@ -28,7 +28,7 @@ The sidebar layout, top to bottom: ### From the keyboard (`⌘⇧G`) -Select two or more workspaces in the sidebar, press `⌘⇧G`. A fresh anchor workspace is inserted above the selection; all selected workspaces become children. The group is auto-named `Group 1`, `Group 2`, … (rename anytime via the header context menu). `⌘⇧G` collides with React Grab's default; the group handler only consumes the chord when there is an explicit sidebar multi-selection of at least two workspaces, so React Grab still fires in single-selection and browser/terminal contexts. Rebind in Settings → Keyboard if you'd rather the two not share a key. +Select two or more workspaces in the sidebar, press `⌘⇧G`. The selected workspaces move under a new folder-style group header, and the first selected workspace becomes the group's anchor. No extra terminal workspace is created. The group is auto-named `Group 1`, `Group 2`, … (rename anytime via the header context menu). `⌘⇧G` collides with React Grab's default; the group handler only consumes the chord when there is an explicit sidebar multi-selection of at least two workspaces, so React Grab still fires in single-selection and browser/terminal contexts. Rebind in Settings → Keyboard if you'd rather the two not share a key. Single-tab groups are not created from the shortcut. Use the workspace context menu's **New Group from Workspace** entry for that. @@ -54,7 +54,7 @@ All group operations are scriptable via `cmux workspace-group `. The ```bash cmux workspace-group list [--json] -cmux workspace-group create --name "manaflow" [--cwd ~/projects/manaflow] [--from ,] +cmux workspace-group create --name "manaflow" [--from ,] cmux workspace-group ungroup cmux workspace-group delete # destructive: closes every member workspace cmux workspace-group rename --name "new name" diff --git a/web/messages/en.json b/web/messages/en.json index 417fc8cc1610..732b5a9abb97 100644 --- a/web/messages/en.json +++ b/web/messages/en.json @@ -772,30 +772,30 @@ "paneIdSocket": "Pane ID (socket API)", "panelIdInternal": "Panel ID (internal)", "workspaceGroups": "Workspace Groups", - "workspaceGroupsDesc": "Group related workspaces into collapsible named sections in the sidebar. Each group is owned by an anchor workspace and can be pinned, renamed, and given its own icon and color. New workspaces can be spawned directly inside a group from its header.", + "workspaceGroupsDesc": "Group related workspaces into collapsible named sections in the sidebar. Each group has a folder-style header and an anchor workspace for ordering, and can be pinned, renamed, and given its own icon and color. New workspaces can be spawned directly inside a group from its header.", "workspaceGroupsLink": "Read the full Workspace Groups guide" }, "workspaceGroups": { "metaTitle": "Workspace Groups", "metaDescription": "Nest workspaces into collapsible named sections in the cmux sidebar. The anchor model, how to create and manage a group, and the workspace-group CLI.", "title": "Workspace Groups", - "intro": "Workspace groups let you nest workspaces into collapsible named sections in the sidebar. Each group has an implicit anchor workspace, a customizable + button for spawning new workspaces inside it, and right-click actions for renaming, pinning, ungrouping, and editing its configuration.", + "intro": "Workspace groups let you nest workspaces into collapsible named sections in the sidebar. Each group has a folder-style header, an implicit anchor workspace used for ordering, a customizable + button for spawning new workspaces inside it, and right-click actions for renaming, pinning, ungrouping, and editing its configuration.", "concepts": "Concepts", "anchorTitle": "Anchor workspace", - "anchorDesc": "Every group is owned by exactly one workspace called the anchor. The group header in the sidebar is the anchor's representation — there is no separate row for it. Clicking the header name area focuses the anchor's panels; clicking the chevron toggles collapse.", - "anchorNew": "Anchors are always brand new when a group is created. They are never promoted from an existing workspace. The anchor's working directory is inherited from the first selected workspace when grouping a selection, or from the active workspace when creating via the CLI without --cwd.", + "anchorDesc": "Every group has exactly one workspace called the anchor. The anchor is an existing member workspace used to position the group and provide a reference working directory. The group header is its own folder-style row; when the group is expanded, the anchor remains visible as a normal workspace row under the header. Clicking the chevron toggles collapse.", + "anchorNew": "When a group is created, cmux promotes the first eligible selected workspace to be the anchor. It does not create a new terminal workspace just to act as the group header.", "anchorClose": "Closing the anchor workspace dissolves the group: every other member becomes an ungrouped workspace and stays in the tabs list. Nothing is closed besides the anchor itself. The app shows a confirm dialog with a \"Don't ask again\" toggle before this happens.", "identityTitle": "Group identity", - "identityDesc": "A group has a name, an icon (an SF Symbol, default folder.fill), and an optional custom color. These are independent of the anchor workspace's own customizations. The anchor's color and icon are seeded from the group on creation, but they can diverge afterwards.", + "identityDesc": "A group has a name, an icon (an SF Symbol, default folder.fill), and an optional custom color. These are independent of the anchor workspace's own customizations.", "pinningTitle": "Pinning", "pinningDesc": "Groups can be pinned independently of individual workspace pins. Pinned top-level rows, whether individual workspaces or groups, stay above unpinned rows. Within each tier, groups and workspaces keep the order you drag them into.", "layoutIntro": "The sidebar lays out top-level rows top to bottom:", "layoutItem1": "Pinned top-level rows (workspaces and groups).", "layoutItem2": "Unpinned top-level rows (workspaces and groups).", "creatingTitle": "Creating a group", - "creatingIntro": "A group is created in one of two ways: a keyboard shortcut or a workspace context menu. Both insert a fresh anchor workspace above the selection and move the selected workspaces into the group. Once a group exists, you manage it and add workspaces to it from the group header (see Managing a group below).", + "creatingIntro": "A group is created in one of two ways: a keyboard shortcut or a workspace context menu. Both move existing workspaces under a folder-style group header and promote the first eligible selected workspace as the ordering anchor. Neither creates a new terminal workspace for the header. Once a group exists, you manage it and add workspaces to it from the group header (see Managing a group below).", "keyboardTitle": "From the keyboard ({shortcut})", - "keyboardDesc": "Select two or more workspaces in the sidebar, then press {shortcut}. A fresh anchor workspace is inserted above the selection and all selected workspaces become children. The group is auto-named Group 1, Group 2, and so on — rename it anytime via the header context menu.", + "keyboardDesc": "Select two or more workspaces in the sidebar, then press {shortcut}. The selected workspaces move under a new folder-style group header, and the first selected workspace becomes the group's anchor. No extra terminal workspace is created. The group is auto-named Group 1, Group 2, and so on — rename it anytime via the header context menu.", "keyboardNote": "{shortcut} collides with React Grab's default. The group handler only consumes the chord when there is an explicit sidebar multi-selection of at least two workspaces, so React Grab still fires in single-selection and in browser or terminal contexts. Rebind it in Settings → Keyboard if you would rather the two not share a key.", "keyboardSingle": "Single-tab groups are not created from the shortcut. Use the workspace context menu New Group from Workspace entry for that.", "contextMenuTitle": "From a workspace context menu", diff --git a/web/messages/ja.json b/web/messages/ja.json index 1df9f9efa72a..664b6c886bba 100644 --- a/web/messages/ja.json +++ b/web/messages/ja.json @@ -731,30 +731,30 @@ "panelIdInternal": "パネルID(内部)", "metaTitle": "コンセプト", "workspaceGroups": "ワークスペースグループ", - "workspaceGroupsDesc": "関連するワークスペースを、サイドバー内の折りたたみ可能な名前付きセクションにまとめます。各グループはアンカーワークスペースが所有し、ピン留めや名前変更ができ、独自のアイコンと色を設定できます。グループのヘッダーから、グループ内に新しいワークスペースを直接生成できます。", + "workspaceGroupsDesc": "関連するワークスペースを、サイドバー内の折りたたみ可能な名前付きセクションにまとめます。各グループにはフォルダー形式のヘッダーと並び順用のアンカーワークスペースがあり、ピン留めや名前変更ができ、独自のアイコンと色を設定できます。グループのヘッダーから、グループ内に新しいワークスペースを直接生成できます。", "workspaceGroupsLink": "ワークスペースグループの完全ガイドを読む" }, "workspaceGroups": { "metaTitle": "ワークスペースグループ", "metaDescription": "cmux サイドバーで、ワークスペースを折りたためる名前付きセクションにまとめて入れ子にします。アンカーモデル、グループの作成・管理方法、そして workspace-group CLI について。", "title": "ワークスペースグループ", - "intro": "ワークスペースグループを使うと、ワークスペースをサイドバー内の折りたたみ可能な名前付きセクションにまとめられます。各グループには暗黙の「アンカーワークスペース」、グループ内に新しいワークスペースを生成するためのカスタマイズ可能な + ボタン、名前変更・ピン留め・グループ解除・設定編集を行う右クリックアクションがあります。", + "intro": "ワークスペースグループを使うと、ワークスペースをサイドバー内の折りたたみ可能な名前付きセクションにまとめられます。各グループにはフォルダー形式のヘッダー、並び順に使う暗黙の「アンカーワークスペース」、グループ内に新しいワークスペースを生成するためのカスタマイズ可能な + ボタン、名前変更・ピン留め・グループ解除・設定編集を行う右クリックアクションがあります。", "concepts": "概念", "anchorTitle": "アンカーワークスペース", - "anchorDesc": "各グループは「アンカー」と呼ばれる 1 つのワークスペースが所有します。サイドバーのグループヘッダーがアンカーそのものを表し、専用の行は別に存在しません。ヘッダーの名前部分をクリックするとアンカーのパネルにフォーカスし、シェブロンをクリックすると折りたたみを切り替えます。", - "anchorNew": "アンカーはグループ作成時に常に新規作成されます。既存のワークスペースが昇格してアンカーになることはありません。アンカーの作業ディレクトリは、選択範囲をグループ化する場合は最初に選択したワークスペースから、--cwd なしで CLI から作成する場合はアクティブなワークスペースから引き継がれます。", + "anchorDesc": "各グループには「アンカー」と呼ばれる 1 つのワークスペースがあります。アンカーは既存のメンバーワークスペースで、グループの位置決めと参照用の作業ディレクトリに使われます。グループヘッダーは独立したフォルダー形式の行で、グループを展開している間はアンカーも通常のワークスペース行としてヘッダーの下に表示されます。シェブロンをクリックすると折りたたみを切り替えます。", + "anchorNew": "グループ作成時、cmux は最初の有効な選択ワークスペースをアンカーに昇格させます。グループヘッダーのためだけに新しいターミナルワークスペースを作成することはありません。", "anchorClose": "アンカーワークスペースを閉じるとグループは解散します。他のすべてのメンバーはグループ化されていないワークスペースとなり、タブ一覧に残ります。アンカー自体以外は何も閉じられません。この処理の前に、アプリは「今後表示しない」トグル付きの確認ダイアログを表示します。", "identityTitle": "グループの識別情報", - "identityDesc": "グループには名前、アイコン(SF Symbol、デフォルトは folder.fill)、および任意のカスタムカラーがあります。これらはアンカーワークスペース自身のカスタマイズとは独立しています。アンカーの色とアイコンは作成時にグループから初期設定されますが、その後は別々に変更できます。", + "identityDesc": "グループには名前、アイコン(SF Symbol、デフォルトは folder.fill)、および任意のカスタムカラーがあります。これらはアンカーワークスペース自身のカスタマイズとは独立しています。", "pinningTitle": "ピン留め", "pinningDesc": "グループは個々のワークスペースのピン留めとは独立してピン留めできます。ピン留めされたトップレベルの行は、個々のワークスペースであれグループであれ、ピン留めされていない行より上に表示されます。各層の中では、グループとワークスペースはドラッグした順序を保ちます。", "layoutIntro": "サイドバーはトップレベルの行を上から下に次のように配置します。", "layoutItem1": "ピン留めされたトップレベルの行(ワークスペースとグループ)。", "layoutItem2": "ピン留めされていないトップレベルの行(ワークスペースとグループ)。", "creatingTitle": "グループの作成", - "creatingIntro": "グループは 2 つの方法で作成できます。キーボードショートカット、またはワークスペースのコンテキストメニューです。どちらも選択範囲の上に新しいアンカーワークスペースを挿入し、選択したワークスペースをグループに移動します。グループが作成された後は、グループヘッダーからグループを管理したりワークスペースを追加したりできます(後述の「グループの管理」を参照)。", + "creatingIntro": "グループは 2 つの方法で作成できます。キーボードショートカット、またはワークスペースのコンテキストメニューです。どちらも既存のワークスペースをフォルダー形式のグループヘッダーの下に移動し、最初の有効な選択ワークスペースを並び順用のアンカーにします。ヘッダー用の新しいターミナルワークスペースは作成しません。グループが作成された後は、グループヘッダーからグループを管理したりワークスペースを追加したりできます(後述の「グループの管理」を参照)。", "keyboardTitle": "キーボードから({shortcut})", - "keyboardDesc": "サイドバーで 2 つ以上のワークスペースを選択し、{shortcut} を押します。選択範囲の上に新しいアンカーワークスペースが挿入され、選択したすべてのワークスペースが子になります。グループは Group 1、Group 2 のように自動的に名前が付けられ、ヘッダーのコンテキストメニューからいつでも名前を変更できます。", + "keyboardDesc": "サイドバーで 2 つ以上のワークスペースを選択し、{shortcut} を押します。選択したワークスペースは新しいフォルダー形式のグループヘッダーの下に移動し、最初の選択ワークスペースがグループのアンカーになります。余分なターミナルワークスペースは作成されません。グループは Group 1、Group 2 のように自動的に名前が付けられ、ヘッダーのコンテキストメニューからいつでも名前を変更できます。", "keyboardNote": "{shortcut} は React Grab のデフォルトと競合します。グループハンドラーは、サイドバーで 2 つ以上のワークスペースが明示的に複数選択されている場合にのみこのショートカットを消費するため、単一選択時やブラウザ・ターミナルのコンテキストでは React Grab が引き続き動作します。2 つでキーを共有させたくない場合は、設定 → キーボードで再割り当てしてください。", "keyboardSingle": "単一タブのグループはショートカットからは作成されません。それには、ワークスペースのコンテキストメニューの「ワークスペースからグループを作成」項目を使用してください。", "contextMenuTitle": "ワークスペースのコンテキストメニューから", From 020e8c7e22cd9cf3c53bfdb878194903d5c7629d Mon Sep 17 00:00:00 2001 From: leegisang Date: Tue, 16 Jun 2026 03:27:36 +0900 Subject: [PATCH 2/7] Address workspace group review feedback --- .../CmuxWorkspaces/Values/WorkspaceGroup.swift | 2 +- Sources/VerticalTabsSidebar+WorkspaceGroups.swift | 3 +-- web/messages/ar.json | 14 +++++++------- web/messages/bs.json | 14 +++++++------- web/messages/da.json | 14 +++++++------- web/messages/de.json | 14 +++++++------- web/messages/es.json | 14 +++++++------- web/messages/fr.json | 14 +++++++------- web/messages/it.json | 14 +++++++------- web/messages/km.json | 14 +++++++------- web/messages/ko.json | 14 +++++++------- web/messages/no.json | 14 +++++++------- web/messages/pl.json | 14 +++++++------- web/messages/pt-BR.json | 14 +++++++------- web/messages/ru.json | 14 +++++++------- web/messages/th.json | 14 +++++++------- web/messages/tr.json | 14 +++++++------- web/messages/uk.json | 14 +++++++------- web/messages/zh-CN.json | 14 +++++++------- web/messages/zh-TW.json | 14 +++++++------- 20 files changed, 128 insertions(+), 129 deletions(-) diff --git a/Packages/CmuxWorkspaces/Sources/CmuxWorkspaces/Values/WorkspaceGroup.swift b/Packages/CmuxWorkspaces/Sources/CmuxWorkspaces/Values/WorkspaceGroup.swift index 4d7551c4bad7..4fa1e3801a02 100644 --- a/Packages/CmuxWorkspaces/Sources/CmuxWorkspaces/Values/WorkspaceGroup.swift +++ b/Packages/CmuxWorkspaces/Sources/CmuxWorkspaces/Values/WorkspaceGroup.swift @@ -19,7 +19,7 @@ public struct WorkspaceGroup: Identifiable, Equatable, Sendable { public var isPinned: Bool /// Identifier of the member workspace that positions this group. /// Always present and always points to a workspace in the window's tabs - /// whose `groupId == self.id`. + /// whose `groupId == self.id`. Closing this workspace dissolves the group. public var anchorWorkspaceId: UUID /// Group-level color override (hex string). When nil, falls back to the /// cwd-config color resolved from `cmux.json` for the anchor's cwd, then diff --git a/Sources/VerticalTabsSidebar+WorkspaceGroups.swift b/Sources/VerticalTabsSidebar+WorkspaceGroups.swift index cd7ac64caeed..4f8c57fc3991 100644 --- a/Sources/VerticalTabsSidebar+WorkspaceGroups.swift +++ b/Sources/VerticalTabsSidebar+WorkspaceGroups.swift @@ -30,7 +30,6 @@ extension VerticalTabsSidebar { partial + notificationStore.unreadCount(forTabId: workspaceId) } }() - let modifierSymbol = renderContext.workspaceNumberShortcut.numberedDigitHintPrefix let showsHintForAnchor = false let topDropIndicatorVisible = SidebarTabDropIndicatorPredicate().topVisible( forTabId: group.anchorWorkspaceId, @@ -85,7 +84,7 @@ extension VerticalTabsSidebar { memberCount: memberWorkspaceIds.count, anchorUnreadCount: anchorUnreadCount, shortcutDigit: nil, - shortcutModifierSymbol: modifierSymbol, + shortcutModifierSymbol: nil, showsShortcutHint: showsHintForAnchor, shortcutHintXOffset: settings.sidebarShortcutHintXOffset, shortcutHintYOffset: settings.sidebarShortcutHintYOffset, diff --git a/web/messages/ar.json b/web/messages/ar.json index 5695d1a6f76c..f97cfe6d4455 100644 --- a/web/messages/ar.json +++ b/web/messages/ar.json @@ -675,30 +675,30 @@ "panelIdInternal": "معرف اللوحة (داخلي)", "metaTitle": "المفاهيم", "workspaceGroups": "مجموعات مساحات العمل", - "workspaceGroupsDesc": "اجمع مساحات العمل المرتبطة في أقسام مسماة قابلة للطي في الشريط الجانبي. كل مجموعة مملوكة لمساحة عمل مرساة، ويمكن تثبيتها وإعادة تسميتها وإعطاؤها أيقونتها ولونها الخاصين. يمكن إنشاء مساحات عمل جديدة مباشرة داخل مجموعة من ترويستها.", + "workspaceGroupsDesc": "اجمع مساحات العمل المرتبطة في أقسام مسماة قابلة للطي في الشريط الجانبي. لكل مجموعة ترويسة بأسلوب مجلد ومساحة عمل مرساة للترتيب، ويمكن تثبيتها وإعادة تسميتها وإعطاؤها أيقونتها ولونها الخاصين. يمكن إنشاء مساحات عمل جديدة مباشرة داخل مجموعة من ترويستها.", "workspaceGroupsLink": "اقرأ دليل مجموعات مساحات العمل الكامل" }, "workspaceGroups": { "metaTitle": "مجموعات مساحات العمل", "metaDescription": "قم بتداخل مساحات العمل داخل أقسام مُسمّاة قابلة للطي في الشريط الجانبي لـ cmux. نموذج المرساة، وكيفية إنشاء مجموعة وإدارتها، وواجهة سطر الأوامر workspace-group.", "title": "مجموعات مساحات العمل", - "intro": "تتيح لك مجموعات مساحات العمل وضع مساحات العمل ضمن أقسام مسماة قابلة للطي في الشريط الجانبي. لكل مجموعة مساحة عمل مرساة ضمنية، وزر + قابل للتخصيص لإنشاء مساحات عمل جديدة داخلها، وإجراءات بالنقر بالزر الأيمن لإعادة التسمية والتثبيت وإلغاء التجميع وتحرير تكوينها.", + "intro": "تتيح لك مجموعات مساحات العمل تداخل مساحات العمل في أقسام مسماة قابلة للطي في الشريط الجانبي. لكل مجموعة ترويسة بأسلوب مجلد، ومساحة عمل مرساة ضمنية تُستخدم للترتيب، وزر + قابل للتخصيص لإنشاء مساحات عمل جديدة داخلها، وإجراءات النقر بزر الماوس الأيمن لإعادة التسمية والتثبيت وإلغاء التجميع وتحرير إعداداتها.", "concepts": "المفاهيم", "anchorTitle": "مساحة العمل المرساة", - "anchorDesc": "كل مجموعة مملوكة لمساحة عمل واحدة بالضبط تُسمى المرساة. ترويسة المجموعة في الشريط الجانبي هي تمثيل المرساة — لا يوجد صف منفصل لها. يؤدي النقر على منطقة اسم الترويسة إلى تركيز لوحات المرساة؛ بينما يؤدي النقر على السهم إلى تبديل الطي.", - "anchorNew": "تكون المراسي دائمًا جديدة تمامًا عند إنشاء مجموعة. ولا تتم ترقيتها أبدًا من مساحة عمل موجودة. يُورّث دليل العمل الخاص بالمرساة من أول مساحة عمل محددة عند تجميع تحديد، أو من مساحة العمل النشطة عند الإنشاء عبر CLI بدون --cwd.", + "anchorDesc": "كل مجموعة لها مساحة عمل واحدة بالضبط تُسمى المرساة. المرساة هي مساحة عمل عضو موجودة تُستخدم لتحديد موضع المجموعة وتوفير دليل عمل مرجعي. ترويسة المجموعة هي صف مستقل بأسلوب مجلد؛ وعند توسيع المجموعة تبقى المرساة مرئية كصف مساحة عمل عادي تحت الترويسة. النقر على السهم يبدّل الطي.", + "anchorNew": "عند إنشاء مجموعة، يرقّي cmux أول مساحة عمل محددة مؤهلة لتصبح المرساة. ولا ينشئ مساحة عمل طرفية جديدة لمجرد استخدامها كترويسة للمجموعة.", "anchorClose": "يؤدي إغلاق مساحة عمل المرساة إلى حل المجموعة: يصبح كل عضو آخر مساحة عمل غير مجمعة ويبقى في قائمة علامات التبويب. لا يُغلق أي شيء سوى المرساة نفسها. يعرض التطبيق مربع حوار تأكيد مع مفتاح تبديل \"عدم السؤال مرة أخرى\" قبل حدوث ذلك.", "identityTitle": "هوية المجموعة", - "identityDesc": "للمجموعة اسم وأيقونة (رمز SF Symbol، الافتراضي folder.fill) ولون مخصص اختياري. وهذه مستقلة عن تخصيصات مساحة العمل المرساة الخاصة بها. يُؤخذ لون المرساة وأيقونتها مبدئيًا من المجموعة عند الإنشاء، لكن يمكن أن يتباعدا بعد ذلك.", + "identityDesc": "للمجموعة اسم وأيقونة (SF Symbol، الافتراضي folder.fill) ولون مخصص اختياري. هذه مستقلة عن تخصيصات مساحة عمل المرساة نفسها.", "pinningTitle": "التثبيت", "pinningDesc": "يمكن تثبيت المجموعات بشكل مستقل عن تثبيتات مساحات العمل الفردية. تبقى الصفوف المثبتة من المستوى الأعلى، سواء كانت مساحات عمل فردية أو مجموعات، فوق الصفوف غير المثبتة. ضمن كل طبقة، تحتفظ المجموعات ومساحات العمل بالترتيب الذي تسحبها إليه.", "layoutIntro": "يرتب الشريط الجانبي صفوف المستوى الأعلى من الأعلى إلى الأسفل:", "layoutItem1": "صفوف المستوى الأعلى المثبتة (مساحات العمل والمجموعات).", "layoutItem2": "صفوف المستوى الأعلى غير المثبتة (مساحات العمل والمجموعات).", "creatingTitle": "إنشاء مجموعة", - "creatingIntro": "تُنشأ المجموعة بإحدى طريقتين: اختصار لوحة المفاتيح أو قائمة سياق مساحة العمل. كلتاهما تُدرجان مساحة عمل مرساة جديدة أعلى التحديد وتنقلان مساحات العمل المحددة إلى المجموعة. بمجرد وجود المجموعة، يمكنك إدارتها وإضافة مساحات العمل إليها من رأس المجموعة (راجع إدارة مجموعة أدناه).", + "creatingIntro": "تُنشأ المجموعة بإحدى طريقتين: اختصار لوحة مفاتيح أو قائمة سياق مساحة عمل. كلاهما ينقل مساحات العمل الموجودة تحت ترويسة مجموعة بأسلوب مجلد ويرقّي أول مساحة عمل محددة مؤهلة كمرساة للترتيب. لا ينشئ أي منهما مساحة عمل طرفية جديدة للترويسة. بعد وجود المجموعة، تديرها وتضيف مساحات عمل إليها من ترويسة المجموعة (انظر إدارة مجموعة أدناه).", "keyboardTitle": "من لوحة المفاتيح ({shortcut})", - "keyboardDesc": "حدد مساحتي عمل أو أكثر في الشريط الجانبي، ثم اضغط على {shortcut}. تُدرج مساحة عمل مرساة جديدة فوق التحديد وتصبح جميع مساحات العمل المحددة عناصر فرعية. تُسمى المجموعة تلقائيًا Group 1، وGroup 2، وهكذا — أعد تسميتها في أي وقت عبر قائمة سياق الترويسة.", + "keyboardDesc": "حدد مساحتي عمل أو أكثر في الشريط الجانبي، ثم اضغط على {shortcut}. تنتقل مساحات العمل المحددة تحت ترويسة مجموعة جديدة بأسلوب مجلد، وتصبح أول مساحة عمل محددة مرساة المجموعة. لا يتم إنشاء مساحة عمل طرفية إضافية. تُسمى المجموعة تلقائيًا Group 1، وGroup 2، وهكذا — أعد تسميتها في أي وقت عبر قائمة سياق الترويسة.", "keyboardNote": "يتعارض {shortcut} مع الافتراضي لـ React Grab. لا يستهلك معالج المجموعة الاختصار إلا عند وجود تحديد متعدد صريح في الشريط الجانبي لمساحتي عمل على الأقل، لذا لا يزال React Grab يعمل في التحديد الأحادي وفي سياقات المتصفح أو الطرفية. أعد ربطه في الإعدادات ← لوحة المفاتيح إذا كنت تفضل ألا يتشاركا مفتاحًا واحدًا.", "keyboardSingle": "لا يتم إنشاء المجموعات أحادية علامة التبويب من الاختصار. استخدم عنصر مجموعة جديدة من مساحة العمل في قائمة سياق مساحة العمل لذلك.", "contextMenuTitle": "من قائمة سياق مساحة العمل", diff --git a/web/messages/bs.json b/web/messages/bs.json index 62dd666a3327..e09d27868116 100644 --- a/web/messages/bs.json +++ b/web/messages/bs.json @@ -675,30 +675,30 @@ "panelIdInternal": "ID panela (interno)", "metaTitle": "Koncepti", "workspaceGroups": "Grupe radnih prostora", - "workspaceGroupsDesc": "Grupišite povezane radne prostore u sklopive imenovane odjeljke u bočnoj traci. Svaku grupu posjeduje sidreni radni prostor, a može se prikvačiti, preimenovati i dobiti vlastitu ikonu i boju. Novi radni prostori mogu se pokrenuti direktno unutar grupe iz njenog zaglavlja.", + "workspaceGroupsDesc": "Grupišite povezane radne prostore u sklopive imenovane odjeljke u bočnoj traci. Svaka grupa ima zaglavlje u stilu foldera i sidreni radni prostor za redoslijed, a može se prikvačiti, preimenovati i dobiti vlastitu ikonu i boju. Novi radni prostori mogu se pokrenuti direktno unutar grupe iz njenog zaglavlja.", "workspaceGroupsLink": "Pročitajte cjelovit vodič o grupama radnih prostora" }, "workspaceGroups": { "metaTitle": "Grupe radnih prostora", "metaDescription": "Ugnijezdite radne prostore u sklopive imenovane sekcije u cmux bočnoj traci. Model sidra, kako kreirati i upravljati grupom, te workspace-group CLI.", "title": "Grupe radnih prostora", - "intro": "Grupe radnih prostora omogućavaju vam da ugnijezdite radne prostore u sklopive imenovane odjeljke u bočnoj traci. Svaka grupa ima implicitni sidreni radni prostor, prilagodljivo dugme + za pokretanje novih radnih prostora unutar nje, te akcije desnim klikom za preimenovanje, prikvačivanje, razgrupisavanje i uređivanje njene konfiguracije.", + "intro": "Grupe radnih prostora omogućavaju ugnježđivanje radnih prostora u sklopive imenovane odjeljke u bočnoj traci. Svaka grupa ima zaglavlje u stilu foldera, implicitni sidreni radni prostor koji se koristi za redoslijed, prilagodljivo dugme + za pokretanje novih radnih prostora unutar nje i radnje desnog klika za preimenovanje, prikvačivanje, razgrupisavanje i uređivanje konfiguracije.", "concepts": "Koncepti", "anchorTitle": "Sidreni radni prostor", - "anchorDesc": "Svaku grupu posjeduje tačno jedan radni prostor koji se naziva sidro. Zaglavlje grupe u bočnoj traci predstavlja sidro — za njega ne postoji poseban red. Klikom na područje naziva u zaglavlju fokusiraju se paneli sidra; klikom na strelicu prebacuje se sklapanje.", - "anchorNew": "Sidra su uvijek potpuno nova kada se grupa kreira. Nikada se ne promovišu iz postojećeg radnog prostora. Radni direktorij sidra nasljeđuje se od prvog odabranog radnog prostora prilikom grupisanja odabira, ili od aktivnog radnog prostora prilikom kreiranja putem CLI bez --cwd.", + "anchorDesc": "Svaka grupa ima tačno jedan radni prostor koji se zove sidro. Sidro je postojeći članski radni prostor koji se koristi za pozicioniranje grupe i kao referentni radni direktorij. Zaglavlje grupe je vlastiti red u stilu foldera; kada je grupa proširena, sidro ostaje vidljivo kao običan red radnog prostora ispod zaglavlja. Klik na strelicu prebacuje sklapanje.", + "anchorNew": "Kada se grupa kreira, cmux promoviše prvi podobni odabrani radni prostor u sidro. Ne kreira novi terminalski radni prostor samo da bi služio kao zaglavlje grupe.", "anchorClose": "Zatvaranje sidrenog radnog prostora raspušta grupu: svaki drugi član postaje razgrupisani radni prostor i ostaje na listi kartica. Ništa se ne zatvara osim samog sidra. Aplikacija prije toga prikazuje dijalog za potvrdu s prekidačem \"Ne pitaj ponovo\".", "identityTitle": "Identitet grupe", - "identityDesc": "Grupa ima naziv, ikonu (SF Symbol, podrazumijevano folder.fill) i opcionalnu prilagođenu boju. Oni su nezavisni od vlastitih prilagođavanja sidrenog radnog prostora. Boja i ikona sidra se prilikom kreiranja preuzimaju iz grupe, ali se nakon toga mogu razlikovati.", + "identityDesc": "Grupa ima ime, ikonu (SF Symbol, zadano folder.fill) i opcionalnu prilagođenu boju. Oni su nezavisni od vlastitih prilagodbi sidrenog radnog prostora.", "pinningTitle": "Prikvačivanje", "pinningDesc": "Grupe se mogu prikvačiti nezavisno od prikvačavanja pojedinačnih radnih prostora. Prikvačeni redovi najvišeg nivoa, bilo da su pojedinačni radni prostori ili grupe, ostaju iznad neprikvačenih redova. Unutar svakog nivoa, grupe i radni prostori zadržavaju redoslijed kojim ih prevučete.", "layoutIntro": "Bočna traka raspoređuje redove najvišeg nivoa odozgo prema dolje:", "layoutItem1": "Prikvačeni redovi najvišeg nivoa (radni prostori i grupe).", "layoutItem2": "Neprikvačeni redovi najvišeg nivoa (radni prostori i grupe).", "creatingTitle": "Kreiranje grupe", - "creatingIntro": "Grupa se kreira na jedan od dva načina: putem prečice na tastaturi ili kontekstualnog menija workspace-a. Oba načina umeću novi sidreni workspace iznad odabira i premještaju odabrane workspace-ove u grupu. Kada grupa postoji, upravljate njome i dodajete joj workspace-ove iz zaglavlja grupe (pogledajte Upravljanje grupom u nastavku).", + "creatingIntro": "Grupa se kreira na jedan od dva načina: prečicom na tastaturi ili kontekstnim menijem radnog prostora. Oba premještaju postojeće radne prostore pod zaglavlje grupe u stilu foldera i promovišu prvi podobni odabrani radni prostor kao sidro za redoslijed. Nijedan ne kreira novi terminalski radni prostor za zaglavlje. Kada grupa postoji, upravljate njom i dodajete radne prostore iz zaglavlja grupe (pogledajte Upravljanje grupom ispod).", "keyboardTitle": "S tastature ({shortcut})", - "keyboardDesc": "Odaberite dva ili više radnih prostora u bočnoj traci, zatim pritisnite {shortcut}. Novi sidreni radni prostor umeće se iznad odabira i svi odabrani radni prostori postaju potomci. Grupa se automatski imenuje Group 1, Group 2 i tako dalje — preimenujte je bilo kada putem kontekstnog menija zaglavlja.", + "keyboardDesc": "Odaberite dva ili više radnih prostora u bočnoj traci, zatim pritisnite {shortcut}. Odabrani radni prostori premještaju se pod novo zaglavlje grupe u stilu foldera, a prvi odabrani radni prostor postaje sidro grupe. Ne kreira se dodatni terminalski radni prostor. Grupa se automatski imenuje Group 1, Group 2 i tako dalje — preimenujte je bilo kada putem kontekstnog menija zaglavlja.", "keyboardNote": "{shortcut} se podudara s podrazumijevanom prečicom za React Grab. Rukovalac grupe troši kombinaciju tipki samo kada postoji eksplicitan višestruki odabir od najmanje dva radna prostora u bočnoj traci, pa se React Grab i dalje aktivira pri pojedinačnom odabiru te u kontekstu preglednika ili terminala. Promijenite je u Postavke → Tastatura ako ne želite da to dvoje dijele tipku.", "keyboardSingle": "Grupe s jednom karticom ne kreiraju se putem prečice. Za to koristite stavku Nova grupa iz radnog prostora u kontekstualnom meniju radnog prostora.", "contextMenuTitle": "Iz kontekstnog menija radnog prostora", diff --git a/web/messages/da.json b/web/messages/da.json index f8720475e48d..5232322d635a 100644 --- a/web/messages/da.json +++ b/web/messages/da.json @@ -675,30 +675,30 @@ "panelIdInternal": "Panel-ID (internt)", "metaTitle": "Koncepter", "workspaceGroups": "Workspace-grupper", - "workspaceGroupsDesc": "Saml relaterede workspaces i sammenklappelige navngivne sektioner i sidebaren. Hver gruppe ejes af et anker-workspace og kan fastgøres, omdøbes og få sit eget ikon og farve. Nye workspaces kan oprettes direkte inde i en gruppe fra dens header.", + "workspaceGroupsDesc": "Saml relaterede workspaces i sammenklappelige navngivne sektioner i sidebaren. Hver gruppe har en header i mappestil og et anker-workspace til rækkefølge, og den kan fastgøres, omdøbes og få sit eget ikon og farve. Nye workspaces kan oprettes direkte inde i en gruppe fra dens header.", "workspaceGroupsLink": "Læs den fulde Workspace Groups-guide" }, "workspaceGroups": { "metaTitle": "Workspace-grupper", "metaDescription": "Indlejr workspaces i sammenklappelige, navngivne sektioner i cmux-sidebjælken. Ankermodellen, hvordan du opretter og administrerer en gruppe, og workspace-group-CLI'en.", "title": "Workspace-grupper", - "intro": "Workspace groups lader dig indlejre workspaces i sammenklappelige navngivne sektioner i sidebaren. Hver gruppe har et implicit anker-workspace, en tilpasselig +-knap til at oprette nye workspaces inde i den og højrekliks-handlinger til at omdøbe, fastgøre, opdele og redigere dens konfiguration.", + "intro": "Workspace-grupper lader dig indlejre workspaces i sammenklappelige navngivne sektioner i sidebaren. Hver gruppe har en header i mappestil, et implicit anker-workspace der bruges til rækkefølge, en tilpasselig +-knap til at oprette nye workspaces inde i den og højreklikshandlinger til omdøbning, fastgørelse, ophævelse af gruppering og redigering af konfigurationen.", "concepts": "Koncepter", "anchorTitle": "Anker-workspace", - "anchorDesc": "Hver gruppe ejes af præcis ét workspace kaldet ankeret. Gruppens header i sidebaren er ankerets repræsentation — der er ingen separat række til det. Klik på headerens navneområde fokuserer ankerets paneler; klik på chevronen skifter sammenklapning til/fra.", - "anchorNew": "Ankere er altid helt nye, når en gruppe oprettes. De forfremmes aldrig fra et eksisterende workspace. Ankerets arbejdsmappe nedarves fra det første valgte workspace, når en markering grupperes, eller fra det aktive workspace, når der oprettes via CLI'en uden --cwd.", + "anchorDesc": "Hver gruppe har præcis ét workspace kaldet ankeret. Ankeret er et eksisterende medlems-workspace, der bruges til at placere gruppen og give en referencearbejdsmappe. Gruppeheaderen er sin egen række i mappestil; når gruppen er udvidet, forbliver ankeret synligt som en normal workspace-række under headeren. Klik på chevronen for at slå sammenklapning til eller fra.", + "anchorNew": "Når en gruppe oprettes, promoverer cmux det første egnede valgte workspace til at være ankeret. Den opretter ikke et nyt terminal-workspace kun for at fungere som gruppeheader.", "anchorClose": "Lukning af anker-workspacet opløser gruppen: alle andre medlemmer bliver ugrupperede workspaces og forbliver i fanelisten. Intet andet end selve ankeret lukkes. Appen viser en bekræftelsesdialog med en \"Spørg ikke igen\"-kontakt, før dette sker.", "identityTitle": "Gruppeidentitet", - "identityDesc": "En gruppe har et navn, et ikon (et SF Symbol, standard folder.fill) og en valgfri brugerdefineret farve. Disse er uafhængige af anker-workspacets egne tilpasninger. Ankerets farve og ikon udledes fra gruppen ved oprettelse, men de kan afvige bagefter.", + "identityDesc": "En gruppe har et navn, et ikon (et SF Symbol, standard folder.fill) og en valgfri brugerdefineret farve. Disse er uafhængige af anker-workspacets egne tilpasninger.", "pinningTitle": "Fastgørelse", "pinningDesc": "Grupper kan fastgøres uafhængigt af individuelle workspace-fastgørelser. Fastgjorte rækker på øverste niveau, hvad enten det er individuelle workspaces eller grupper, forbliver over ufastgjorte rækker. Inden for hvert niveau bevarer grupper og workspaces den rækkefølge, du trækker dem ind i.", "layoutIntro": "Sidebaren placerer rækker på øverste niveau fra top til bund:", "layoutItem1": "Fastgjorte rækker på øverste niveau (workspaces og grupper).", "layoutItem2": "Ufastgjorte rækker på øverste niveau (workspaces og grupper).", "creatingTitle": "Oprettelse af en gruppe", - "creatingIntro": "En gruppe oprettes på en af to måder: via en tastaturgenvej eller en workspace-kontekstmenu. Begge indsætter et nyt anker-workspace over markeringen og flytter de markerede workspaces ind i gruppen. Når en gruppe findes, administrerer du den og tilføjer workspaces til den fra gruppens overskrift (se Administration af en gruppe nedenfor).", + "creatingIntro": "En gruppe oprettes på én af to måder: en tastaturgenvej eller en workspace-kontekstmenu. Begge flytter eksisterende workspaces ind under en gruppeheader i mappestil og promoverer det første egnede valgte workspace som rækkefølgeanker. Ingen af dem opretter et nyt terminal-workspace til headeren. Når en gruppe findes, administrerer du den og tilføjer workspaces til den fra gruppeheaderen (se Administrering af en gruppe nedenfor).", "keyboardTitle": "Fra tastaturet ({shortcut})", - "keyboardDesc": "Vælg to eller flere workspaces i sidebaren, og tryk derefter på {shortcut}. Et nyt anker-workspace indsættes over markeringen, og alle valgte workspaces bliver børn. Gruppen navngives automatisk Group 1, Group 2 og så videre — omdøb den når som helst via headerens kontekstmenu.", + "keyboardDesc": "Vælg to eller flere workspaces i sidebaren, og tryk derefter på {shortcut}. De valgte workspaces flyttes under en ny gruppeheader i mappestil, og det første valgte workspace bliver gruppens anker. Der oprettes ikke et ekstra terminal-workspace. Gruppen navngives automatisk Group 1, Group 2 og så videre — omdøb den når som helst via headerens kontekstmenu.", "keyboardNote": "{shortcut} kolliderer med React Grabs standard. Gruppehåndteringen forbruger kun akkorden, når der er en eksplicit multimarkering af mindst to workspaces i sidebaren, så React Grab udløses stadig ved enkeltmarkering og i browser- eller terminalkontekster. Genbind den i Indstillinger → Tastatur, hvis du hellere vil have, at de to ikke deler en tast.", "keyboardSingle": "Grupper med en enkelt fane oprettes ikke via genvejen. Brug i stedet menupunktet Ny gruppe fra arbejdsområde i workspace-kontekstmenuen.", "contextMenuTitle": "Fra en workspace-kontekstmenu", diff --git a/web/messages/de.json b/web/messages/de.json index 9c4fcb89f260..09bfa9c8c052 100644 --- a/web/messages/de.json +++ b/web/messages/de.json @@ -675,30 +675,30 @@ "panelIdInternal": "Panel-ID (intern)", "metaTitle": "Konzepte", "workspaceGroups": "Workspace-Gruppen", - "workspaceGroupsDesc": "Fassen Sie zusammengehörige Workspaces zu zusammenklappbaren benannten Abschnitten in der Seitenleiste zusammen. Jede Gruppe gehört einem Anker-Workspace und kann angeheftet, umbenannt sowie mit einem eigenen Symbol und einer eigenen Farbe versehen werden. Neue Workspaces können direkt aus der Kopfzeile einer Gruppe heraus innerhalb dieser erstellt werden.", + "workspaceGroupsDesc": "Fassen Sie zusammengehörige Workspaces zu zusammenklappbaren benannten Abschnitten in der Seitenleiste zusammen. Jede Gruppe hat eine Kopfzeile im Ordnerstil und einen Anker-Workspace für die Reihenfolge und kann angeheftet, umbenannt sowie mit einem eigenen Symbol und einer eigenen Farbe versehen werden. Neue Workspaces können direkt aus der Kopfzeile einer Gruppe heraus innerhalb dieser erstellt werden.", "workspaceGroupsLink": "Den vollständigen Leitfaden zu Workspace-Gruppen lesen" }, "workspaceGroups": { "metaTitle": "Workspace-Gruppen", "metaDescription": "Verschachteln Sie Arbeitsbereiche in einklappbare benannte Abschnitte in der cmux-Seitenleiste. Das Anker-Modell, das Erstellen und Verwalten einer Gruppe sowie die workspace-group-CLI.", "title": "Workspace-Gruppen", - "intro": "Mit Workspace-Gruppen können Sie Workspaces zu zusammenklappbaren benannten Abschnitten in der Seitenleiste verschachteln. Jede Gruppe hat einen impliziten Anker-Workspace, eine anpassbare +-Schaltfläche zum Erstellen neuer Workspaces darin sowie Rechtsklick-Aktionen zum Umbenennen, Anheften, Auflösen und Bearbeiten ihrer Konfiguration.", + "intro": "Workspace-Gruppen ermöglichen es, Workspaces in zusammenklappbaren benannten Abschnitten in der Seitenleiste zu verschachteln. Jede Gruppe hat eine Kopfzeile im Ordnerstil, einen impliziten Anker-Workspace für die Reihenfolge, eine anpassbare +-Schaltfläche zum Erstellen neuer Workspaces darin sowie Rechtsklick-Aktionen zum Umbenennen, Anheften, Auflösen und Bearbeiten ihrer Konfiguration.", "concepts": "Konzepte", "anchorTitle": "Anker-Workspace", - "anchorDesc": "Jede Gruppe gehört genau einem Workspace, dem sogenannten Anker. Die Gruppenkopfzeile in der Seitenleiste ist die Repräsentation des Ankers – es gibt keine separate Zeile dafür. Ein Klick auf den Namensbereich der Kopfzeile fokussiert die Bereiche des Ankers; ein Klick auf das Chevron schaltet das Zusammenklappen um.", - "anchorNew": "Anker sind beim Erstellen einer Gruppe immer brandneu. Sie werden nie aus einem bestehenden Workspace heraufgestuft. Das Arbeitsverzeichnis des Ankers wird beim Gruppieren einer Auswahl vom ersten ausgewählten Workspace übernommen oder beim Erstellen über das CLI ohne --cwd vom aktiven Workspace.", + "anchorDesc": "Jede Gruppe hat genau einen Workspace, den sogenannten Anker. Der Anker ist ein vorhandener Mitglieds-Workspace, der die Gruppe positioniert und ein Referenz-Arbeitsverzeichnis bereitstellt. Die Gruppenkopfzeile ist eine eigene Zeile im Ordnerstil; wenn die Gruppe aufgeklappt ist, bleibt der Anker als normale Workspace-Zeile unter der Kopfzeile sichtbar. Ein Klick auf das Chevron schaltet das Zusammenklappen um.", + "anchorNew": "Wenn eine Gruppe erstellt wird, stuft cmux den ersten geeigneten ausgewählten Workspace zum Anker hoch. Es wird kein neuer Terminal-Workspace erstellt, nur um als Gruppenkopfzeile zu dienen.", "anchorClose": "Das Schließen des Anker-Workspace löst die Gruppe auf: Jedes andere Mitglied wird zu einem nicht gruppierten Workspace und bleibt in der Tab-Liste. Außer dem Anker selbst wird nichts geschlossen. Die App zeigt zuvor einen Bestätigungsdialog mit einem Umschalter \"Nicht erneut fragen\" an.", "identityTitle": "Gruppenidentität", - "identityDesc": "Eine Gruppe hat einen Namen, ein Symbol (ein SF Symbol, standardmäßig folder.fill) und eine optionale benutzerdefinierte Farbe. Diese sind unabhängig von den eigenen Anpassungen des Anker-Workspace. Die Farbe und das Symbol des Ankers werden bei der Erstellung von der Gruppe übernommen, können danach aber abweichen.", + "identityDesc": "Eine Gruppe hat einen Namen, ein Symbol (ein SF Symbol, standardmäßig folder.fill) und eine optionale benutzerdefinierte Farbe. Diese sind unabhängig von den eigenen Anpassungen des Anker-Workspace.", "pinningTitle": "Anheften", "pinningDesc": "Gruppen können unabhängig von einzelnen Workspace-Anheftungen angeheftet werden. Angeheftete Zeilen der obersten Ebene, ob einzelne Workspaces oder Gruppen, bleiben über nicht angehefteten Zeilen. Innerhalb jeder Ebene behalten Gruppen und Workspaces die Reihenfolge bei, in die Sie sie ziehen.", "layoutIntro": "Die Seitenleiste ordnet die Zeilen der obersten Ebene von oben nach unten an:", "layoutItem1": "Angeheftete Zeilen der obersten Ebene (Workspaces und Gruppen).", "layoutItem2": "Nicht angeheftete Zeilen der obersten Ebene (Workspaces und Gruppen).", "creatingTitle": "Eine Gruppe erstellen", - "creatingIntro": "Eine Gruppe wird auf eine von zwei Arten erstellt: über ein Tastenkürzel oder das Kontextmenü eines Workspace. Beide fügen einen neuen Anker-Workspace oberhalb der Auswahl ein und verschieben die ausgewählten Workspaces in die Gruppe. Sobald eine Gruppe existiert, verwalten Sie sie und fügen ihr Workspaces über die Gruppenkopfzeile hinzu (siehe Verwalten einer Gruppe weiter unten).", + "creatingIntro": "Eine Gruppe wird auf eine von zwei Arten erstellt: über ein Tastenkürzel oder das Kontextmenü eines Workspace. Beide verschieben vorhandene Workspaces unter eine Gruppenkopfzeile im Ordnerstil und stufen den ersten geeigneten ausgewählten Workspace als Anker für die Reihenfolge hoch. Keine der beiden Methoden erstellt einen neuen Terminal-Workspace für die Kopfzeile. Sobald eine Gruppe existiert, verwalten Sie sie und fügen ihr Workspaces über die Gruppenkopfzeile hinzu (siehe Verwalten einer Gruppe weiter unten).", "keyboardTitle": "Über die Tastatur ({shortcut})", - "keyboardDesc": "Wählen Sie zwei oder mehr Workspaces in der Seitenleiste aus und drücken Sie dann {shortcut}. Ein neuer Anker-Workspace wird oberhalb der Auswahl eingefügt, und alle ausgewählten Workspaces werden zu Kindern. Die Gruppe wird automatisch Group 1, Group 2 und so weiter benannt – benennen Sie sie jederzeit über das Kontextmenü der Kopfzeile um.", + "keyboardDesc": "Wählen Sie zwei oder mehr Workspaces in der Seitenleiste aus und drücken Sie dann {shortcut}. Die ausgewählten Workspaces werden unter eine neue Gruppenkopfzeile im Ordnerstil verschoben, und der erste ausgewählte Workspace wird zum Anker der Gruppe. Es wird kein zusätzlicher Terminal-Workspace erstellt. Die Gruppe wird automatisch Group 1, Group 2 und so weiter benannt – benennen Sie sie jederzeit über das Kontextmenü der Kopfzeile um.", "keyboardNote": "{shortcut} kollidiert mit der Standardbelegung von React Grab. Der Gruppen-Handler verwendet den Tastenakkord nur, wenn es eine explizite Mehrfachauswahl von mindestens zwei Workspaces in der Seitenleiste gibt, sodass React Grab bei Einzelauswahl sowie in Browser- oder Terminalkontexten weiterhin auslöst. Belegen Sie es in Einstellungen → Tastatur neu, falls Sie nicht möchten, dass sich die beiden eine Taste teilen.", "keyboardSingle": "Gruppen mit nur einem Tab werden nicht über das Tastenkürzel erstellt. Verwenden Sie dafür den Eintrag „Neue Gruppe aus Arbeitsbereich“ im Kontextmenü des Arbeitsbereichs.", "contextMenuTitle": "Über ein Workspace-Kontextmenü", diff --git a/web/messages/es.json b/web/messages/es.json index 141cc369f8df..7315eea45cd0 100644 --- a/web/messages/es.json +++ b/web/messages/es.json @@ -675,30 +675,30 @@ "panelIdInternal": "ID de panel (interno)", "metaTitle": "Conceptos", "workspaceGroups": "Grupos de workspaces", - "workspaceGroupsDesc": "Agrupa workspaces relacionados en secciones nombradas y plegables en la barra lateral. Cada grupo pertenece a un workspace de anclaje y se puede fijar, renombrar y darle su propio icono y color. Se pueden crear nuevos workspaces directamente dentro de un grupo desde su encabezado.", + "workspaceGroupsDesc": "Agrupa workspaces relacionados en secciones nombradas y plegables en la barra lateral. Cada grupo tiene un encabezado con estilo de carpeta y un workspace de anclaje para el orden, y se puede fijar, renombrar y darle su propio icono y color. Se pueden crear nuevos workspaces directamente dentro de un grupo desde su encabezado.", "workspaceGroupsLink": "Lee la guía completa de Grupos de workspaces" }, "workspaceGroups": { "metaTitle": "Grupos de workspaces", "metaDescription": "Anida espacios de trabajo en secciones con nombre y plegables en la barra lateral de cmux. El modelo de ancla, cómo crear y administrar un grupo, y la CLI workspace-group.", "title": "Grupos de workspaces", - "intro": "Los grupos de workspaces te permiten anidar workspaces en secciones nombradas y plegables en la barra lateral. Cada grupo tiene un workspace de anclaje implícito, un botón + personalizable para crear nuevos workspaces dentro de él, y acciones de clic derecho para renombrar, fijar, desagrupar y editar su configuración.", + "intro": "Los grupos de workspaces te permiten anidar workspaces en secciones nombradas y plegables de la barra lateral. Cada grupo tiene un encabezado con estilo de carpeta, un workspace de anclaje implícito usado para el orden, un botón + personalizable para crear nuevos workspaces dentro y acciones de clic derecho para renombrar, fijar, desagrupar y editar su configuración.", "concepts": "Conceptos", "anchorTitle": "Workspace de anclaje", - "anchorDesc": "Cada grupo pertenece a exactamente un workspace llamado el anclaje. El encabezado del grupo en la barra lateral es la representación del anclaje: no hay una fila separada para él. Al hacer clic en el área del nombre del encabezado se enfocan los paneles del anclaje; al hacer clic en el chevrón se alterna el plegado.", - "anchorNew": "Los anclajes siempre son completamente nuevos cuando se crea un grupo. Nunca se promueven a partir de un workspace existente. El directorio de trabajo del anclaje se hereda del primer workspace seleccionado al agrupar una selección, o del workspace activo al crearlo mediante la CLI sin --cwd.", + "anchorDesc": "Cada grupo tiene exactamente un workspace llamado anclaje. El anclaje es un workspace miembro existente que se usa para posicionar el grupo y proporcionar un directorio de trabajo de referencia. El encabezado del grupo es su propia fila con estilo de carpeta; cuando el grupo está expandido, el anclaje permanece visible como una fila normal de workspace bajo el encabezado. Hacer clic en el chevrón alterna el plegado.", + "anchorNew": "Cuando se crea un grupo, cmux promueve el primer workspace seleccionado que sea apto para ser el anclaje. No crea un nuevo workspace de terminal solo para actuar como encabezado del grupo.", "anchorClose": "Cerrar el workspace de anclaje disuelve el grupo: cada uno de los demás miembros se convierte en un workspace sin agrupar y permanece en la lista de pestañas. No se cierra nada más que el propio anclaje. La aplicación muestra un cuadro de diálogo de confirmación con una opción \"No volver a preguntar\" antes de que esto ocurra.", "identityTitle": "Identidad del grupo", - "identityDesc": "Un grupo tiene un nombre, un icono (un SF Symbol, por defecto folder.fill) y un color personalizado opcional. Estos son independientes de las personalizaciones propias del workspace de anclaje. El color y el icono del anclaje se inicializan a partir del grupo al crearlo, pero pueden divergir posteriormente.", + "identityDesc": "Un grupo tiene un nombre, un icono (un SF Symbol, por defecto folder.fill) y un color personalizado opcional. Estos son independientes de las propias personalizaciones del workspace de anclaje.", "pinningTitle": "Fijado", "pinningDesc": "Los grupos se pueden fijar de forma independiente de los fijados de workspaces individuales. Las filas de nivel superior fijadas, ya sean workspaces individuales o grupos, permanecen por encima de las filas no fijadas. Dentro de cada nivel, los grupos y workspaces conservan el orden en que los arrastras.", "layoutIntro": "La barra lateral organiza las filas de nivel superior de arriba a abajo:", "layoutItem1": "Filas de nivel superior fijadas (workspaces y grupos).", "layoutItem2": "Filas de nivel superior no fijadas (workspaces y grupos).", "creatingTitle": "Crear un grupo", - "creatingIntro": "Un grupo se crea de una de dos maneras: mediante un atajo de teclado o el menú contextual de un workspace. Ambas opciones insertan un nuevo workspace ancla por encima de la selección y mueven los workspaces seleccionados al grupo. Una vez que existe un grupo, lo gestionas y le agregas workspaces desde el encabezado del grupo (consulta Gestionar un grupo más abajo).", + "creatingIntro": "Un grupo se crea de una de dos maneras: con un atajo de teclado o desde el menú contextual de un workspace. Ambas mueven workspaces existentes bajo un encabezado de grupo con estilo de carpeta y promueven el primer workspace seleccionado apto como anclaje de orden. Ninguna crea un nuevo workspace de terminal para el encabezado. Una vez que existe un grupo, lo administras y le agregas workspaces desde el encabezado del grupo (consulta Administrar un grupo más abajo).", "keyboardTitle": "Desde el teclado ({shortcut})", - "keyboardDesc": "Selecciona dos o más workspaces en la barra lateral y luego presiona {shortcut}. Se inserta un nuevo workspace de anclaje encima de la selección y todos los workspaces seleccionados se convierten en hijos. El grupo se nombra automáticamente como Group 1, Group 2, y así sucesivamente; renómbralo en cualquier momento mediante el menú contextual del encabezado.", + "keyboardDesc": "Selecciona dos o más workspaces en la barra lateral y luego presiona {shortcut}. Los workspaces seleccionados se mueven bajo un nuevo encabezado de grupo con estilo de carpeta, y el primer workspace seleccionado se convierte en el anclaje del grupo. No se crea ningún workspace de terminal adicional. El grupo se nombra automáticamente como Group 1, Group 2 y así sucesivamente; renómbralo en cualquier momento mediante el menú contextual del encabezado.", "keyboardNote": "{shortcut} entra en conflicto con el predeterminado de React Grab. El gestor de grupos solo consume la combinación cuando hay una multiselección explícita en la barra lateral de al menos dos workspaces, por lo que React Grab sigue activándose en selecciones individuales y en contextos de navegador o terminal. Reasígnalo en Configuración → Teclado si prefieres que ambos no compartan una tecla.", "keyboardSingle": "Los grupos de una sola pestaña no se crean desde el atajo. Para ello, usa la entrada Nuevo grupo desde el espacio de trabajo del menú contextual del espacio de trabajo.", "contextMenuTitle": "Desde el menú contextual de un workspace", diff --git a/web/messages/fr.json b/web/messages/fr.json index f1255144a0f2..3489b2b46349 100644 --- a/web/messages/fr.json +++ b/web/messages/fr.json @@ -675,30 +675,30 @@ "panelIdInternal": "ID du panel (interne)", "metaTitle": "Concepts", "workspaceGroups": "Groupes d'espaces de travail", - "workspaceGroupsDesc": "Regroupez des espaces de travail liés dans des sections nommées et repliables de la barre latérale. Chaque groupe est détenu par un espace de travail d'ancrage et peut être épinglé, renommé et doté de sa propre icône et couleur. De nouveaux espaces de travail peuvent être créés directement à l'intérieur d'un groupe depuis son en-tête.", + "workspaceGroupsDesc": "Regroupez des espaces de travail liés dans des sections nommées et repliables de la barre latérale. Chaque groupe possède un en-tête de style dossier et un espace de travail d’ancrage pour l’ordre, et peut être épinglé, renommé et doté de sa propre icône et couleur. De nouveaux espaces de travail peuvent être créés directement à l’intérieur d’un groupe depuis son en-tête.", "workspaceGroupsLink": "Lire le guide complet des groupes d'espaces de travail" }, "workspaceGroups": { "metaTitle": "Groupes d'espaces de travail", "metaDescription": "Imbriquez des espaces de travail dans des sections nommées et repliables dans la barre latérale de cmux. Le modèle d'ancrage, la création et la gestion d'un groupe, et la CLI workspace-group.", "title": "Groupes d'espaces de travail", - "intro": "Les groupes d'espaces de travail vous permettent d'imbriquer des espaces de travail dans des sections nommées et repliables de la barre latérale. Chaque groupe possède un espace de travail d'ancrage implicite, un bouton + personnalisable pour créer de nouveaux espaces de travail à l'intérieur, et des actions par clic droit pour renommer, épingler, dégrouper et modifier sa configuration.", + "intro": "Les groupes d’espaces de travail vous permettent d’imbriquer des espaces de travail dans des sections nommées et repliables de la barre latérale. Chaque groupe possède un en-tête de style dossier, un espace de travail d’ancrage implicite utilisé pour l’ordre, un bouton + personnalisable pour créer de nouveaux espaces de travail à l’intérieur, et des actions par clic droit pour renommer, épingler, dégrouper et modifier sa configuration.", "concepts": "Concepts", "anchorTitle": "Espace de travail d'ancrage", - "anchorDesc": "Chaque groupe est détenu par exactement un espace de travail appelé l'ancrage. L'en-tête du groupe dans la barre latérale est la représentation de l'ancrage — il n'y a pas de ligne distincte pour celui-ci. Cliquer sur la zone du nom de l'en-tête met le focus sur les panneaux de l'ancrage ; cliquer sur le chevron bascule le repliement.", - "anchorNew": "Les ancrages sont toujours flambant neufs lorsqu'un groupe est créé. Ils ne sont jamais promus à partir d'un espace de travail existant. Le répertoire de travail de l'ancrage est hérité du premier espace de travail sélectionné lors du regroupement d'une sélection, ou de l'espace de travail actif lors de la création via le CLI sans --cwd.", + "anchorDesc": "Chaque groupe possède exactement un espace de travail appelé l’ancrage. L’ancrage est un espace de travail membre existant utilisé pour positionner le groupe et fournir un répertoire de travail de référence. L’en-tête du groupe est sa propre ligne de style dossier ; lorsque le groupe est développé, l’ancrage reste visible comme une ligne d’espace de travail normale sous l’en-tête. Cliquer sur le chevron bascule le repliement.", + "anchorNew": "Lorsqu’un groupe est créé, cmux promeut le premier espace de travail sélectionné admissible comme ancrage. Il ne crée pas de nouvel espace de travail de terminal uniquement pour servir d’en-tête de groupe.", "anchorClose": "Fermer l'espace de travail d'ancrage dissout le groupe : tous les autres membres deviennent des espaces de travail dégroupés et restent dans la liste des onglets. Rien n'est fermé hormis l'ancrage lui-même. L'application affiche une boîte de dialogue de confirmation avec une bascule « Ne plus demander » avant que cela ne se produise.", "identityTitle": "Identité du groupe", - "identityDesc": "Un groupe possède un nom, une icône (un SF Symbol, par défaut folder.fill) et une couleur personnalisée facultative. Ceux-ci sont indépendants des personnalisations propres à l'espace de travail d'ancrage. La couleur et l'icône de l'ancrage sont initialisées à partir du groupe lors de la création, mais elles peuvent ensuite diverger.", + "identityDesc": "Un groupe possède un nom, une icône (un SF Symbol, par défaut folder.fill) et une couleur personnalisée facultative. Ceux-ci sont indépendants des personnalisations propres à l’espace de travail d’ancrage.", "pinningTitle": "Épinglage", "pinningDesc": "Les groupes peuvent être épinglés indépendamment des épinglages d'espaces de travail individuels. Les lignes de premier niveau épinglées, qu'il s'agisse d'espaces de travail individuels ou de groupes, restent au-dessus des lignes non épinglées. Au sein de chaque niveau, les groupes et les espaces de travail conservent l'ordre dans lequel vous les faites glisser.", "layoutIntro": "La barre latérale dispose les lignes de premier niveau de haut en bas :", "layoutItem1": "Lignes de premier niveau épinglées (espaces de travail et groupes).", "layoutItem2": "Lignes de premier niveau non épinglées (espaces de travail et groupes).", "creatingTitle": "Créer un groupe", - "creatingIntro": "Un groupe se crée de deux manières : par un raccourci clavier ou par le menu contextuel d’un workspace. Les deux insèrent un nouveau workspace d’ancrage au-dessus de la sélection et déplacent les workspaces sélectionnés dans le groupe. Une fois le groupe créé, vous le gérez et y ajoutez des workspaces depuis l’en-tête du groupe (voir Gérer un groupe ci-dessous).", + "creatingIntro": "Un groupe se crée de deux manières : par un raccourci clavier ou par le menu contextuel d’un espace de travail. Les deux déplacent des espaces de travail existants sous un en-tête de groupe de style dossier et promeuvent le premier espace de travail sélectionné admissible comme ancrage d’ordre. Aucun des deux ne crée de nouvel espace de travail de terminal pour l’en-tête. Une fois le groupe créé, vous le gérez et y ajoutez des espaces de travail depuis l’en-tête du groupe (voir Gérer un groupe ci-dessous).", "keyboardTitle": "Depuis le clavier ({shortcut})", - "keyboardDesc": "Sélectionnez deux espaces de travail ou plus dans la barre latérale, puis appuyez sur {shortcut}. Un nouvel espace de travail d'ancrage est inséré au-dessus de la sélection et tous les espaces de travail sélectionnés deviennent des enfants. Le groupe est automatiquement nommé Group 1, Group 2, et ainsi de suite — renommez-le à tout moment via le menu contextuel de l'en-tête.", + "keyboardDesc": "Sélectionnez deux espaces de travail ou plus dans la barre latérale, puis appuyez sur {shortcut}. Les espaces de travail sélectionnés sont déplacés sous un nouvel en-tête de groupe de style dossier, et le premier espace de travail sélectionné devient l’ancrage du groupe. Aucun espace de travail de terminal supplémentaire n’est créé. Le groupe est automatiquement nommé Group 1, Group 2, et ainsi de suite — renommez-le à tout moment via le menu contextuel de l’en-tête.", "keyboardNote": "{shortcut} entre en conflit avec le raccourci par défaut de React Grab. Le gestionnaire de groupe ne consomme l'accord que lorsqu'il existe une multi-sélection explicite d'au moins deux espaces de travail dans la barre latérale, de sorte que React Grab se déclenche toujours en sélection unique et dans les contextes navigateur ou terminal. Réaffectez-le dans Réglages → Clavier si vous préférez que les deux ne partagent pas une touche.", "keyboardSingle": "Les groupes à onglet unique ne sont pas créés depuis le raccourci. Utilisez pour cela l'entrée Nouveau groupe depuis cet espace de travail du menu contextuel de l'espace de travail.", "contextMenuTitle": "Depuis un menu contextuel d'espace de travail", diff --git a/web/messages/it.json b/web/messages/it.json index 6cc157c6b5de..5e76cacb1858 100644 --- a/web/messages/it.json +++ b/web/messages/it.json @@ -675,30 +675,30 @@ "panelIdInternal": "ID panel (interno)", "metaTitle": "Concetti", "workspaceGroups": "Gruppi di workspace", - "workspaceGroupsDesc": "Raggruppa i workspace correlati in sezioni nominate e comprimibili nella barra laterale. Ogni gruppo è di proprietà di un workspace di ancoraggio e può essere fissato, rinominato e dotato di un'icona e di un colore propri. Nuovi workspace possono essere generati direttamente all'interno di un gruppo dalla sua intestazione.", + "workspaceGroupsDesc": "Raggruppa i workspace correlati in sezioni nominate e comprimibili nella barra laterale. Ogni gruppo ha un’intestazione in stile cartella e un workspace di ancoraggio per l’ordinamento, e può essere fissato, rinominato e dotato di un’icona e di un colore propri. Nuovi workspace possono essere generati direttamente all’interno di un gruppo dalla sua intestazione.", "workspaceGroupsLink": "Leggi la guida completa ai Gruppi di workspace" }, "workspaceGroups": { "metaTitle": "Gruppi di workspace", "metaDescription": "Annida le aree di lavoro in sezioni con nome comprimibili nella barra laterale di cmux. Il modello di ancoraggio, come creare e gestire un gruppo e la CLI workspace-group.", "title": "Gruppi di workspace", - "intro": "I gruppi di workspace ti permettono di annidare i workspace in sezioni nominate e comprimibili nella barra laterale. Ogni gruppo ha un workspace di ancoraggio implicito, un pulsante + personalizzabile per generare nuovi workspace al suo interno, e azioni del clic destro per rinominare, fissare, separare e modificare la sua configurazione.", + "intro": "I gruppi di workspace ti permettono di annidare workspace in sezioni nominate e comprimibili nella barra laterale. Ogni gruppo ha un’intestazione in stile cartella, un workspace di ancoraggio implicito usato per l’ordinamento, un pulsante + personalizzabile per generare nuovi workspace al suo interno e azioni con clic destro per rinominare, fissare, separare e modificare la configurazione.", "concepts": "Concetti", "anchorTitle": "Workspace di ancoraggio", - "anchorDesc": "Ogni gruppo è di proprietà di esattamente un workspace chiamato ancoraggio. L'intestazione del gruppo nella barra laterale è la rappresentazione dell'ancoraggio — non c'è una riga separata per esso. Cliccando sull'area del nome dell'intestazione si mettono a fuoco i pannelli dell'ancoraggio; cliccando sul chevron si alterna la compressione.", - "anchorNew": "Gli ancoraggi sono sempre completamente nuovi quando viene creato un gruppo. Non vengono mai promossi da un workspace esistente. La directory di lavoro dell'ancoraggio viene ereditata dal primo workspace selezionato quando si raggruppa una selezione, oppure dal workspace attivo quando si crea tramite il CLI senza --cwd.", + "anchorDesc": "Ogni gruppo ha esattamente un workspace chiamato ancoraggio. L’ancoraggio è un workspace membro esistente usato per posizionare il gruppo e fornire una directory di lavoro di riferimento. L’intestazione del gruppo è una riga autonoma in stile cartella; quando il gruppo è espanso, l’ancoraggio rimane visibile come una normale riga di workspace sotto l’intestazione. Fare clic sul chevron alterna la compressione.", + "anchorNew": "Quando viene creato un gruppo, cmux promuove il primo workspace selezionato idoneo a diventare l’ancoraggio. Non crea un nuovo workspace di terminale solo per fungere da intestazione del gruppo.", "anchorClose": "Chiudere il workspace di ancoraggio dissolve il gruppo: ogni altro membro diventa un workspace non raggruppato e rimane nell'elenco dei tab. Non viene chiuso nulla oltre all'ancoraggio stesso. L'app mostra una finestra di conferma con un interruttore \"Non chiedere più\" prima che ciò avvenga.", "identityTitle": "Identità del gruppo", - "identityDesc": "Un gruppo ha un nome, un'icona (un SF Symbol, predefinito folder.fill) e un colore personalizzato opzionale. Questi sono indipendenti dalle personalizzazioni proprie del workspace di ancoraggio. Il colore e l'icona dell'ancoraggio vengono inizializzati dal gruppo alla creazione, ma possono divergere in seguito.", + "identityDesc": "Un gruppo ha un nome, un’icona (un SF Symbol, predefinito folder.fill) e un colore personalizzato opzionale. Questi sono indipendenti dalle personalizzazioni del workspace di ancoraggio stesso.", "pinningTitle": "Fissaggio", "pinningDesc": "I gruppi possono essere fissati indipendentemente dai fissaggi dei singoli workspace. Le righe di primo livello fissate, siano esse singoli workspace o gruppi, restano sopra le righe non fissate. All'interno di ciascun livello, i gruppi e i workspace mantengono l'ordine in cui li trascini.", "layoutIntro": "La barra laterale dispone le righe di primo livello dall'alto verso il basso:", "layoutItem1": "Righe di primo livello fissate (workspace e gruppi).", "layoutItem2": "Righe di primo livello non fissate (workspace e gruppi).", "creatingTitle": "Creare un gruppo", - "creatingIntro": "Un gruppo si crea in uno di due modi: con una scorciatoia da tastiera o dal menu contestuale di un workspace. Entrambi inseriscono un nuovo workspace di ancoraggio sopra la selezione e spostano i workspace selezionati nel gruppo. Una volta creato il gruppo, lo gestisci e vi aggiungi workspace dall’intestazione del gruppo (vedi Gestire un gruppo più avanti).", + "creatingIntro": "Un gruppo viene creato in uno di due modi: con una scorciatoia da tastiera o dal menu contestuale di un workspace. Entrambi spostano workspace esistenti sotto un’intestazione di gruppo in stile cartella e promuovono il primo workspace selezionato idoneo come ancoraggio per l’ordinamento. Nessuno dei due crea un nuovo workspace di terminale per l’intestazione. Una volta che il gruppo esiste, lo gestisci e aggiungi workspace dal suo header (vedi Gestire un gruppo sotto).", "keyboardTitle": "Dalla tastiera ({shortcut})", - "keyboardDesc": "Seleziona due o più workspace nella barra laterale, poi premi {shortcut}. Un nuovo workspace di ancoraggio viene inserito sopra la selezione e tutti i workspace selezionati diventano figli. Il gruppo viene nominato automaticamente Group 1, Group 2, e così via — rinominalo in qualsiasi momento tramite il menu contestuale dell'intestazione.", + "keyboardDesc": "Seleziona due o più workspace nella barra laterale, poi premi {shortcut}. I workspace selezionati vengono spostati sotto una nuova intestazione di gruppo in stile cartella, e il primo workspace selezionato diventa l’ancoraggio del gruppo. Non viene creato alcun workspace di terminale aggiuntivo. Il gruppo viene nominato automaticamente Group 1, Group 2 e così via — rinominalo in qualsiasi momento tramite il menu contestuale dell’intestazione.", "keyboardNote": "{shortcut} entra in conflitto con la combinazione predefinita di React Grab. Il gestore del gruppo consuma la combinazione solo quando c'è una multi-selezione esplicita di almeno due workspace nella barra laterale, quindi React Grab si attiva comunque in selezione singola e nei contesti del browser o del terminale. Riassegnalo in Impostazioni → Tastiera se preferisci che i due non condividano un tasto.", "keyboardSingle": "I gruppi a scheda singola non vengono creati dalla scorciatoia. A tale scopo, usa la voce Nuovo gruppo da quest’area di lavoro nel menu contestuale dell'area di lavoro.", "contextMenuTitle": "Da un menu contestuale del workspace", diff --git a/web/messages/km.json b/web/messages/km.json index 0845c973b9ad..67a0cdbbec4d 100644 --- a/web/messages/km.json +++ b/web/messages/km.json @@ -675,30 +675,30 @@ "panelIdInternal": "Panel ID (ខាងក្នុង)", "metaTitle": "គំនិត", "workspaceGroups": "ក្រុមកន្លែងធ្វើការ", - "workspaceGroupsDesc": "ដាក់ជាក្រុមនូវកន្លែងធ្វើការដែលពាក់ព័ន្ធគ្នាទៅជាផ្នែកមានឈ្មោះដែលអាចបង្រួមបានក្នុងរបារចំហៀង។ ក្រុមនីមួយៗត្រូវបានកាន់កាប់ដោយកន្លែងធ្វើការយុថ្កា ហើយអាចត្រូវបានខ្ទាស់ ប្ដូរឈ្មោះ និងផ្ដល់រូបតំណាង និងពណ៌ផ្ទាល់ខ្លួន។ កន្លែងធ្វើការថ្មីអាចត្រូវបានបង្កើតដោយផ្ទាល់នៅក្នុងក្រុមមួយ ចេញពីក្បាលក្រុមរបស់វា។", + "workspaceGroupsDesc": "ដាក់ជាក្រុមនូវកន្លែងធ្វើការដែលពាក់ព័ន្ធគ្នាទៅជាផ្នែកមានឈ្មោះដែលអាចបង្រួមបានក្នុងរបារចំហៀង។ ក្រុមនីមួយៗមានក្បាលបែបថត និងកន្លែងធ្វើការយុថ្កាសម្រាប់រៀបលំដាប់ ហើយអាចខ្ទាស់ ប្តូរឈ្មោះ និងកំណត់រូបតំណាងនិងពណ៌ផ្ទាល់ខ្លួនបាន។ កន្លែងធ្វើការថ្មីអាចបង្កើតដោយផ្ទាល់នៅក្នុងក្រុមពីក្បាលក្រុម។", "workspaceGroupsLink": "អានមគ្គុទ្ទេសក៍ក្រុមកន្លែងធ្វើការពេញលេញ" }, "workspaceGroups": { "metaTitle": "ក្រុមកន្លែងធ្វើការ", "metaDescription": "ដាក់ workspace ចូលក្នុងផ្នែកមានឈ្មោះដែលអាចបត់បានក្នុងរបារចំហៀង cmux។ គំរូ anchor របៀបបង្កើត និងគ្រប់គ្រងក្រុម និង workspace-group CLI។", "title": "ក្រុមកន្លែងធ្វើការ", - "intro": "ក្រុមកន្លែងធ្វើការអនុញ្ញាតឱ្យអ្នកដាក់កន្លែងធ្វើការទៅជាផ្នែកមានឈ្មោះដែលអាចបង្រួមបាននៅក្នុងរបារចំហៀង។ ក្រុមនីមួយៗមានកន្លែងធ្វើការយុថ្កាជាក់ស្ដែង ប៊ូតុង + ដែលអាចប្ដូរតាមបំណងសម្រាប់បង្កើតកន្លែងធ្វើការថ្មីនៅខាងក្នុង និងសកម្មភាពចុចស្ដាំសម្រាប់ការប្ដូរឈ្មោះ ខ្ទាស់ ដោះក្រុម និងកែសម្រួលការកំណត់រចនាសម្ព័ន្ធរបស់វា។", + "intro": "ក្រុមកន្លែងធ្វើការអនុញ្ញាតឱ្យអ្នកដាក់កន្លែងធ្វើការជាស្រទាប់ក្នុងផ្នែកមានឈ្មោះដែលអាចបង្រួមបានក្នុងរបារចំហៀង។ ក្រុមនីមួយៗមានក្បាលបែបថត កន្លែងធ្វើការយុថ្កាដែលប្រើសម្រាប់រៀបលំដាប់ ប៊ូតុង + ដែលអាចកែតម្រូវបានសម្រាប់បង្កើតកន្លែងធ្វើការថ្មីនៅខាងក្នុង និងសកម្មភាពចុចស្តាំសម្រាប់ប្តូរឈ្មោះ ខ្ទាស់ ដកការដាក់ជាក្រុម និងកែការកំណត់របស់វា។", "concepts": "គំនិត", "anchorTitle": "កន្លែងធ្វើការយុថ្កា", - "anchorDesc": "ក្រុមនីមួយៗត្រូវបានកាន់កាប់ដោយកន្លែងធ្វើការតែមួយប៉ុណ្ណោះដែលហៅថាយុថ្កា។ ក្បាលក្រុមនៅក្នុងរបារចំហៀងគឺជាការតំណាងរបស់យុថ្កា — គ្មានជួរដាច់ដោយឡែកសម្រាប់វាទេ។ ការចុចលើតំបន់ឈ្មោះក្បាលធ្វើឱ្យផ្ទាំងរបស់យុថ្កាបានផ្ដោត ការចុចលើ chevron បិទ/បើកការបង្រួម។", - "anchorNew": "យុថ្កាតែងតែថ្មីស្រឡាងនៅពេលក្រុមមួយត្រូវបានបង្កើត។ ពួកវាមិនដែលត្រូវបានតម្លើងពីកន្លែងធ្វើការដែលមានស្រាប់ឡើយ។ ថតធ្វើការរបស់យុថ្កាត្រូវបានទទួលមរតកពីកន្លែងធ្វើការដែលបានជ្រើសរើសដំបូងនៅពេលដាក់ការជ្រើសរើសជាក្រុម ឬពីកន្លែងធ្វើការសកម្មនៅពេលបង្កើតតាមរយៈ CLI ដោយគ្មាន --cwd។", + "anchorDesc": "ក្រុមនីមួយៗមានកន្លែងធ្វើការតែមួយគត់ដែលហៅថាយុថ្កា។ យុថ្កាគឺជាកន្លែងធ្វើការសមាជិកដែលមានស្រាប់ ប្រើសម្រាប់កំណត់ទីតាំងក្រុម និងផ្តល់ថតធ្វើការយោង។ ក្បាលក្រុមគឺជាជួរផ្ទាល់ខ្លួនបែបថត; នៅពេលក្រុមត្រូវបានពង្រីក យុថ្កានៅតែមើលឃើញជាជួរកន្លែងធ្វើការធម្មតាក្រោមក្បាល។ ចុចលើ chevron ដើម្បីបិទបើកការបង្រួម។", + "anchorNew": "នៅពេលបង្កើតក្រុម cmux លើកកន្លែងធ្វើការដែលបានជ្រើសរើសនិងមានសិទ្ធិដំបូងឱ្យក្លាយជាយុថ្កា។ វាមិនបង្កើតកន្លែងធ្វើការស្ថានីយថ្មីតែដើម្បីធ្វើជាក្បាលក្រុមទេ។", "anchorClose": "ការបិទកន្លែងធ្វើការយុថ្កាធ្វើឱ្យក្រុមរលាយ៖ សមាជិកផ្សេងទៀតទាំងអស់ក្លាយជាកន្លែងធ្វើការដែលគ្មានក្រុម ហើយនៅតែស្ថិតក្នុងបញ្ជីផ្ទាំង។ គ្មានអ្វីត្រូវបានបិទក្រៅពីយុថ្កាខ្លួនឯងទេ។ កម្មវិធីបង្ហាញប្រអប់បញ្ជាក់ជាមួយ \"Don't ask again\" មុនពេលរឿងនេះកើតឡើង។", "identityTitle": "អត្តសញ្ញាណក្រុម", - "identityDesc": "ក្រុមមួយមានឈ្មោះ រូបតំណាង (SF Symbol មួយ លំនាំដើម folder.fill) និងពណ៌ផ្ទាល់ខ្លួនជាជម្រើស។ ទាំងនេះឯករាជ្យពីការប្ដូរតាមបំណងផ្ទាល់ខ្លួនរបស់កន្លែងធ្វើការយុថ្កា។ ពណ៌ និងរូបតំណាងរបស់យុថ្កាត្រូវបានបង្កើតពីក្រុមនៅពេលបង្កើត ប៉ុន្តែពួកវាអាចខុសគ្នាបន្ទាប់ពីនោះ។", + "identityDesc": "ក្រុមមានឈ្មោះ រូបតំណាង (SF Symbol, លំនាំដើម folder.fill) និងពណ៌ផ្ទាល់ខ្លួនជាជម្រើស។ វាទាំងនេះឯករាជ្យពីការកែតម្រូវផ្ទាល់ខ្លួនរបស់កន្លែងធ្វើការយុថ្កា។", "pinningTitle": "ការខ្ទាស់", "pinningDesc": "ក្រុមអាចត្រូវបានខ្ទាស់ដោយឯករាជ្យពីការខ្ទាស់កន្លែងធ្វើការនីមួយៗ។ ជួរកម្រិតកំពូលដែលបានខ្ទាស់ មិនថាជាកន្លែងធ្វើការនីមួយៗ ឬក្រុមឡើយ នៅខាងលើជួរដែលមិនបានខ្ទាស់។ នៅក្នុងថ្នាក់នីមួយៗ ក្រុម និងកន្លែងធ្វើការរក្សាលំដាប់ដែលអ្នកអូសពួកវាចូល។", "layoutIntro": "របារចំហៀងរៀបចំជួរកម្រិតកំពូលពីលើចុះក្រោម៖", "layoutItem1": "ជួរកម្រិតកំពូលដែលបានខ្ទាស់ (កន្លែងធ្វើការ និងក្រុម)។", "layoutItem2": "ជួរកម្រិតកំពូលដែលមិនបានខ្ទាស់ (កន្លែងធ្វើការ និងក្រុម)។", "creatingTitle": "ការបង្កើតក្រុមមួយ", - "creatingIntro": "ក្រុមមួយត្រូវបានបង្កើតតាមវិធីពីរយ៉ាង៖ ផ្លូវកាត់ក្តារចុច ឬម៉ឺនុយបរិបទ workspace។ ទាំងពីរបញ្ចូល workspace យុថ្កាថ្មីនៅខាងលើជម្រើស ហើយផ្លាស់ទី workspace ដែលបានជ្រើសរើសចូលក្នុងក្រុម។ នៅពេលក្រុមមានរួចហើយ អ្នកគ្រប់គ្រងវា និងបន្ថែម workspace ទៅក្នុងវាពីក្បាលក្រុម (សូមមើល ការគ្រប់គ្រងក្រុម ខាងក្រោម)។", + "creatingIntro": "ក្រុមត្រូវបានបង្កើតតាមវិធីមួយក្នុងចំណោមពីរ៖ ផ្លូវកាត់ក្ដារចុច ឬម៉ឺនុយបរិបទរបស់កន្លែងធ្វើការ។ ទាំងពីរផ្លាស់ទីកន្លែងធ្វើការដែលមានស្រាប់ក្រោមក្បាលក្រុមបែបថត ហើយលើកកន្លែងធ្វើការដែលបានជ្រើសរើសនិងមានសិទ្ធិដំបូងជាយុថ្កាសម្រាប់រៀបលំដាប់។ ទាំងពីរមិនបង្កើតកន្លែងធ្វើការស្ថានីយថ្មីសម្រាប់ក្បាលទេ។ បន្ទាប់ពីក្រុមមានរួច អ្នកគ្រប់គ្រងវា និងបន្ថែមកន្លែងធ្វើការទៅវាពីក្បាលក្រុម (មើល ការគ្រប់គ្រងក្រុម ខាងក្រោម)។", "keyboardTitle": "ពីក្ដារចុច ({shortcut})", - "keyboardDesc": "ជ្រើសរើសកន្លែងធ្វើការពីរ ឬច្រើននៅក្នុងរបារចំហៀង បន្ទាប់មកចុច {shortcut}។ កន្លែងធ្វើការយុថ្កាថ្មីត្រូវបានបញ្ចូលនៅខាងលើការជ្រើសរើស ហើយកន្លែងធ្វើការដែលបានជ្រើសរើសទាំងអស់ក្លាយជាកូន។ ក្រុមត្រូវបានដាក់ឈ្មោះដោយស្វ័យប្រវត្តិ Group 1, Group 2 ជាដើម — ប្ដូរឈ្មោះវាគ្រប់ពេលតាមរយៈម៉ឺនុយបរិបទក្បាល។", + "keyboardDesc": "ជ្រើសរើសកន្លែងធ្វើការពីរ ឬច្រើនក្នុងរបារចំហៀង បន្ទាប់មកចុច {shortcut}។ កន្លែងធ្វើការដែលបានជ្រើសរើសផ្លាស់ទីក្រោមក្បាលក្រុមថ្មីបែបថត ហើយកន្លែងធ្វើការដែលបានជ្រើសរើសដំបូងក្លាយជាយុថ្ការបស់ក្រុម។ មិនមានកន្លែងធ្វើការស្ថានីយបន្ថែមត្រូវបានបង្កើតទេ។ ក្រុមត្រូវបានដាក់ឈ្មោះដោយស្វ័យប្រវត្តិ Group 1, Group 2 ជាដើម — ប្តូរឈ្មោះវាបានគ្រប់ពេលតាមម៉ឺនុយបរិបទក្បាល។", "keyboardNote": "{shortcut} ប៉ះទង្គិចជាមួយលំនាំដើមរបស់ React Grab។ កម្មវិធីដោះស្រាយក្រុមប្រើតែ chord នៅពេលមានការជ្រើសរើសច្រើនរបារចំហៀងច្បាស់លាស់នៃកន្លែងធ្វើការយ៉ាងហោចណាស់ពីរ ដូច្នេះ React Grab នៅតែបាញ់នៅក្នុងការជ្រើសរើសតែមួយ និងនៅក្នុងបរិបទកម្មវិធីរុករក ឬស្ថានីយ។ ចងវាឡើងវិញនៅក្នុង Settings → Keyboard ប្រសិនបើអ្នកមិនចង់ឱ្យទាំងពីរចែករំលែកគ្រាប់ចុចមួយ។", "keyboardSingle": "ក្រុមផ្ទាំងតែមួយមិនត្រូវបានបង្កើតពីផ្លូវកាត់ទេ។ សូមប្រើធាតុ New Group from Workspace នៅក្នុងម៉ឺនុយបរិបទ workspace សម្រាប់ការនេះ។", "contextMenuTitle": "ពីម៉ឺនុយបរិបទកន្លែងធ្វើការ", diff --git a/web/messages/ko.json b/web/messages/ko.json index fa30d60fa1ac..4eb88a8d178b 100644 --- a/web/messages/ko.json +++ b/web/messages/ko.json @@ -675,30 +675,30 @@ "panelIdInternal": "패널 ID (내부)", "metaTitle": "개념", "workspaceGroups": "워크스페이스 그룹", - "workspaceGroupsDesc": "관련된 워크스페이스를 사이드바에서 접을 수 있는 이름 있는 섹션으로 그룹화합니다. 각 그룹은 앵커 워크스페이스가 소유하며, 고정하고 이름을 바꾸고 자체 아이콘과 색상을 지정할 수 있습니다. 그룹 헤더에서 새 워크스페이스를 그룹 내부에 직접 생성할 수 있습니다.", + "workspaceGroupsDesc": "관련된 워크스페이스를 사이드바에서 접을 수 있는 이름 있는 섹션으로 그룹화합니다. 각 그룹에는 폴더 형태의 헤더와 순서 지정용 앵커 워크스페이스가 있으며, 고정하고 이름을 바꾸고 자체 아이콘과 색상을 지정할 수 있습니다. 그룹 헤더에서 새 워크스페이스를 그룹 내부에 직접 생성할 수 있습니다.", "workspaceGroupsLink": "워크스페이스 그룹 전체 가이드 읽기" }, "workspaceGroups": { "metaTitle": "워크스페이스 그룹", "metaDescription": "cmux 사이드바에서 작업 공간을 접을 수 있는 이름 있는 섹션으로 묶습니다. 앵커 모델, 그룹을 만들고 관리하는 방법, 그리고 workspace-group CLI를 다룹니다.", "title": "워크스페이스 그룹", - "intro": "워크스페이스 그룹을 사용하면 워크스페이스를 사이드바에서 접을 수 있는 이름 있는 섹션으로 중첩할 수 있습니다. 각 그룹에는 암시적인 앵커 워크스페이스, 그룹 내부에 새 워크스페이스를 생성하는 사용자 정의 가능한 + 버튼, 그리고 이름 변경·고정·그룹 해제·설정 편집을 위한 마우스 오른쪽 버튼 작업이 있습니다.", + "intro": "워크스페이스 그룹을 사용하면 워크스페이스를 사이드바에서 접을 수 있는 이름 있는 섹션으로 중첩할 수 있습니다. 각 그룹에는 폴더 형태의 헤더, 순서 지정에 사용되는 암시적 앵커 워크스페이스, 그룹 내부에 새 워크스페이스를 생성하는 사용자 정의 가능한 + 버튼, 그리고 이름 변경·고정·그룹 해제·설정 편집을 위한 마우스 오른쪽 버튼 작업이 있습니다.", "concepts": "개념", "anchorTitle": "앵커 워크스페이스", - "anchorDesc": "모든 그룹은 앵커라고 하는 정확히 하나의 워크스페이스가 소유합니다. 사이드바의 그룹 헤더가 앵커를 나타내며, 앵커를 위한 별도의 행은 없습니다. 헤더의 이름 영역을 클릭하면 앵커의 패널에 포커스가 가고, 셰브론을 클릭하면 접기가 전환됩니다.", - "anchorNew": "앵커는 그룹이 생성될 때 항상 완전히 새로 만들어집니다. 기존 워크스페이스가 앵커로 승격되는 일은 없습니다. 앵커의 작업 디렉터리는 선택 항목을 그룹화할 때는 첫 번째로 선택된 워크스페이스에서, --cwd 없이 CLI로 생성할 때는 활성 워크스페이스에서 상속됩니다.", + "anchorDesc": "모든 그룹에는 앵커라고 하는 워크스페이스가 정확히 하나 있습니다. 앵커는 그룹의 위치를 정하고 기준 작업 디렉터리를 제공하는 기존 멤버 워크스페이스입니다. 그룹 헤더는 별도의 폴더 형태 행이며, 그룹이 펼쳐져 있을 때 앵커는 헤더 아래에 일반 워크스페이스 행으로 계속 표시됩니다. 셰브론을 클릭하면 접기 상태가 전환됩니다.", + "anchorNew": "그룹이 생성될 때 cmux는 첫 번째 적격 선택 워크스페이스를 앵커로 승격합니다. 그룹 헤더 역할만을 위해 새 터미널 워크스페이스를 만들지는 않습니다.", "anchorClose": "앵커 워크스페이스를 닫으면 그룹이 해체됩니다. 다른 모든 멤버는 그룹화되지 않은 워크스페이스가 되어 탭 목록에 남습니다. 앵커 자체 외에는 아무것도 닫히지 않습니다. 이 동작이 발생하기 전에 앱은 \"다시 묻지 않음\" 토글이 있는 확인 대화 상자를 표시합니다.", "identityTitle": "그룹 정체성", - "identityDesc": "그룹에는 이름, 아이콘(SF Symbol, 기본값 folder.fill), 선택적 사용자 정의 색상이 있습니다. 이들은 앵커 워크스페이스 자체의 사용자 정의와 독립적입니다. 앵커의 색상과 아이콘은 생성 시 그룹에서 시드되지만, 이후에는 서로 달라질 수 있습니다.", + "identityDesc": "그룹에는 이름, 아이콘(SF Symbol, 기본값 folder.fill), 선택적 사용자 정의 색상이 있습니다. 이들은 앵커 워크스페이스 자체의 사용자 정의와 독립적입니다.", "pinningTitle": "고정", "pinningDesc": "그룹은 개별 워크스페이스 고정과 독립적으로 고정할 수 있습니다. 고정된 최상위 행은 개별 워크스페이스든 그룹이든 고정되지 않은 행보다 위에 유지됩니다. 각 계층 내에서 그룹과 워크스페이스는 드래그한 순서를 유지합니다.", "layoutIntro": "사이드바는 최상위 행을 위에서 아래로 배치합니다:", "layoutItem1": "고정된 최상위 행(워크스페이스 및 그룹).", "layoutItem2": "고정되지 않은 최상위 행(워크스페이스 및 그룹).", "creatingTitle": "그룹 만들기", - "creatingIntro": "그룹은 두 가지 방법으로 만들 수 있습니다. 키보드 단축키 또는 워크스페이스 컨텍스트 메뉴입니다. 두 방법 모두 선택 항목 위에 새 앵커 워크스페이스를 삽입하고 선택한 워크스페이스를 그룹으로 이동합니다. 그룹이 생성되면 그룹 헤더에서 그룹을 관리하고 워크스페이스를 추가할 수 있습니다(아래의 그룹 관리 참조).", + "creatingIntro": "그룹은 두 가지 방법 중 하나로 생성됩니다. 키보드 단축키 또는 워크스페이스 컨텍스트 메뉴입니다. 두 방법 모두 기존 워크스페이스를 폴더 형태의 그룹 헤더 아래로 이동하고 첫 번째 적격 선택 워크스페이스를 순서 지정 앵커로 승격합니다. 헤더를 위해 새 터미널 워크스페이스를 만들지는 않습니다. 그룹이 생성되면 그룹 헤더에서 그룹을 관리하고 워크스페이스를 추가할 수 있습니다(아래의 그룹 관리 참조).", "keyboardTitle": "키보드로 ({shortcut})", - "keyboardDesc": "사이드바에서 두 개 이상의 워크스페이스를 선택한 다음 {shortcut}을 누릅니다. 선택 항목 위에 새 앵커 워크스페이스가 삽입되고 선택된 모든 워크스페이스가 하위 항목이 됩니다. 그룹은 자동으로 Group 1, Group 2 등으로 이름이 지정되며, 헤더 컨텍스트 메뉴를 통해 언제든지 이름을 바꿀 수 있습니다.", + "keyboardDesc": "사이드바에서 두 개 이상의 워크스페이스를 선택한 다음 {shortcut}을 누릅니다. 선택된 워크스페이스가 새 폴더 형태의 그룹 헤더 아래로 이동하고, 첫 번째 선택 워크스페이스가 그룹의 앵커가 됩니다. 추가 터미널 워크스페이스는 생성되지 않습니다. 그룹은 자동으로 Group 1, Group 2 등으로 이름이 지정되며, 헤더 컨텍스트 메뉴를 통해 언제든지 이름을 바꿀 수 있습니다.", "keyboardNote": "{shortcut}은 React Grab의 기본값과 충돌합니다. 그룹 핸들러는 사이드바에서 두 개 이상의 워크스페이스가 명시적으로 다중 선택된 경우에만 이 코드를 사용하므로, 단일 선택과 브라우저 또는 터미널 컨텍스트에서는 React Grab이 여전히 동작합니다. 두 기능이 키를 공유하지 않게 하려면 설정 → 키보드에서 다시 바인딩하세요.", "keyboardSingle": "단일 탭 그룹은 단축키로 만들어지지 않습니다. 그런 경우에는 작업 공간 컨텍스트 메뉴의 이 작업 공간으로 새 그룹 항목을 사용하세요.", "contextMenuTitle": "워크스페이스 컨텍스트 메뉴에서", diff --git a/web/messages/no.json b/web/messages/no.json index 479bd2783d66..c082bffcc100 100644 --- a/web/messages/no.json +++ b/web/messages/no.json @@ -675,30 +675,30 @@ "panelIdInternal": "Panel-ID (intern)", "metaTitle": "Konsepter", "workspaceGroups": "Arbeidsområdegrupper", - "workspaceGroupsDesc": "Grupper relaterte arbeidsområder i sammenleggbare navngitte seksjoner i sidefeltet. Hver gruppe eies av et ankerarbeidsområde og kan festes, gis nytt navn og få sitt eget ikon og farge. Nye arbeidsområder kan opprettes direkte inne i en gruppe fra gruppens overskrift.", + "workspaceGroupsDesc": "Grupper relaterte arbeidsområder i sammenleggbare navngitte seksjoner i sidefeltet. Hver gruppe har en mappeaktig overskrift og et ankerarbeidsområde for rekkefølge, og kan festes, gis nytt navn og få sitt eget ikon og farge. Nye arbeidsområder kan opprettes direkte inne i en gruppe fra gruppens overskrift.", "workspaceGroupsLink": "Les hele veiledningen om arbeidsområdegrupper" }, "workspaceGroups": { "metaTitle": "Arbeidsområdegrupper", "metaDescription": "Nest arbeidsområder i sammenleggbare, navngitte seksjoner i cmux-sidefeltet. Ankermodellen, hvordan du oppretter og administrerer en gruppe, og workspace-group-CLI-en.", "title": "Arbeidsområdegrupper", - "intro": "Arbeidsområdegrupper lar deg nøste arbeidsområder inn i sammenleggbare navngitte seksjoner i sidefeltet. Hver gruppe har et implisitt ankerarbeidsområde, en tilpassbar +-knapp for å opprette nye arbeidsområder inne i den, og handlinger ved høyreklikk for å gi nytt navn, feste, oppheve gruppering og redigere konfigurasjonen.", + "intro": "Arbeidsområdegrupper lar deg nøste arbeidsområder i sammenleggbare navngitte seksjoner i sidefeltet. Hver gruppe har en mappeaktig overskrift, et implisitt ankerarbeidsområde som brukes til rekkefølge, en tilpassbar +-knapp for å opprette nye arbeidsområder inne i den, og høyreklikkhandlinger for å gi nytt navn, feste, oppheve gruppering og redigere konfigurasjonen.", "concepts": "Konsepter", "anchorTitle": "Ankerarbeidsområde", - "anchorDesc": "Hver gruppe eies av nøyaktig ett arbeidsområde kalt ankeret. Gruppeoverskriften i sidefeltet er ankerets representasjon — det finnes ingen egen rad for det. Å klikke på navneområdet i overskriften gir fokus til ankerets paneler; å klikke på vinkelpilen veksler sammenlegging.", - "anchorNew": "Ankere er alltid helt nye når en gruppe opprettes. De forfremmes aldri fra et eksisterende arbeidsområde. Ankerets arbeidskatalog arves fra det første valgte arbeidsområdet når du grupperer et utvalg, eller fra det aktive arbeidsområdet når du oppretter via CLI-en uten --cwd.", + "anchorDesc": "Hver gruppe har nøyaktig ett arbeidsområde kalt ankeret. Ankeret er et eksisterende medlemsarbeidsområde som brukes til å plassere gruppen og gi en referansearbeidsmappe. Gruppeoverskriften er sin egen mappeaktige rad; når gruppen er utvidet, forblir ankeret synlig som en vanlig arbeidsområderad under overskriften. Klikk på chevronen for å slå sammenlegging av eller på.", + "anchorNew": "Når en gruppe opprettes, forfremmer cmux det første kvalifiserte valgte arbeidsområdet til å være ankeret. Det oppretter ikke et nytt terminalarbeidsområde bare for å fungere som gruppeoverskrift.", "anchorClose": "Å lukke ankerarbeidsområdet oppløser gruppen: alle andre medlemmer blir til ugrupperte arbeidsområder og forblir i fanelisten. Ingenting lukkes bortsett fra selve ankeret. Appen viser en bekreftelsesdialog med en «Ikke spør igjen»-bryter før dette skjer.", "identityTitle": "Gruppeidentitet", - "identityDesc": "En gruppe har et navn, et ikon (et SF Symbol, standard folder.fill) og en valgfri tilpasset farge. Disse er uavhengige av ankerarbeidsområdets egne tilpasninger. Ankerets farge og ikon hentes fra gruppen ved opprettelse, men de kan avvike etterpå.", + "identityDesc": "En gruppe har et navn, et ikon (et SF Symbol, standard folder.fill) og en valgfri egendefinert farge. Disse er uavhengige av ankerarbeidsområdets egne tilpasninger.", "pinningTitle": "Festing", "pinningDesc": "Grupper kan festes uavhengig av festing av enkeltarbeidsområder. Festede rader på øverste nivå, enten det er enkeltarbeidsområder eller grupper, holder seg over ikke-festede rader. Innenfor hvert nivå beholder grupper og arbeidsområder rekkefølgen du drar dem inn i.", "layoutIntro": "Sidefeltet plasserer rader på øverste nivå ovenfra og ned:", "layoutItem1": "Festede rader på øverste nivå (arbeidsområder og grupper).", "layoutItem2": "Ikke-festede rader på øverste nivå (arbeidsområder og grupper).", "creatingTitle": "Opprette en gruppe", - "creatingIntro": "En gruppe opprettes på en av to måter: med en hurtigtast eller en hurtigmeny for arbeidsområde. Begge setter inn et nytt anker-arbeidsområde over utvalget og flytter de valgte arbeidsområdene inn i gruppen. Når en gruppe finnes, administrerer du den og legger til arbeidsområder i den fra gruppeoverskriften (se Administrere en gruppe nedenfor).", + "creatingIntro": "En gruppe opprettes på én av to måter: en hurtigtast eller en kontekstmeny for arbeidsområde. Begge flytter eksisterende arbeidsområder under en mappeaktig gruppeoverskrift og forfremmer det første kvalifiserte valgte arbeidsområdet som rekkefølgeanker. Ingen av dem oppretter et nytt terminalarbeidsområde for overskriften. Når en gruppe finnes, administrerer du den og legger arbeidsområder til den fra gruppeoverskriften (se Administrere en gruppe nedenfor).", "keyboardTitle": "Fra tastaturet ({shortcut})", - "keyboardDesc": "Velg to eller flere arbeidsområder i sidefeltet, og trykk deretter {shortcut}. Et nytt ankerarbeidsområde settes inn over utvalget, og alle valgte arbeidsområder blir til underordnede. Gruppen navngis automatisk Group 1, Group 2 og så videre — gi den nytt navn når som helst via kontekstmenyen i overskriften.", + "keyboardDesc": "Velg to eller flere arbeidsområder i sidefeltet, og trykk deretter {shortcut}. De valgte arbeidsområdene flyttes under en ny mappeaktig gruppeoverskrift, og det første valgte arbeidsområdet blir gruppens anker. Det opprettes ikke noe ekstra terminalarbeidsområde. Gruppen navngis automatisk Group 1, Group 2 og så videre — gi den nytt navn når som helst via kontekstmenyen i overskriften.", "keyboardNote": "{shortcut} kolliderer med React Grabs standard. Gruppebehandleren forbruker bare tastekombinasjonen når det finnes et eksplisitt flervalg på minst to arbeidsområder i sidefeltet, så React Grab utløses fortsatt ved enkeltvalg og i nettleser- eller terminalkontekster. Bind den om i Innstillinger → Tastatur dersom du heller ikke vil at de to skal dele en tast.", "keyboardSingle": "Grupper med én fane opprettes ikke fra snarveien. Bruk oppføringen New Group from Workspace i arbeidsområdets kontekstmeny for det.", "contextMenuTitle": "Fra en kontekstmeny for arbeidsområde", diff --git a/web/messages/pl.json b/web/messages/pl.json index a594b25b6e48..8ed249e06675 100644 --- a/web/messages/pl.json +++ b/web/messages/pl.json @@ -675,30 +675,30 @@ "panelIdInternal": "ID panelu (wewnętrzny)", "metaTitle": "Koncepcje", "workspaceGroups": "Grupy workspace'ów", - "workspaceGroupsDesc": "Grupuj powiązane workspace'y w zwijane, nazwane sekcje w pasku bocznym. Każda grupa należy do kotwicowego workspace'a i może zostać przypięta, zmieniona nazwa oraz otrzymać własną ikonę i kolor. Nowe workspace'y można tworzyć bezpośrednio wewnątrz grupy z poziomu jej nagłówka.", + "workspaceGroupsDesc": "Grupuj powiązane workspace’y w zwijane, nazwane sekcje w pasku bocznym. Każda grupa ma nagłówek w stylu folderu i kotwicowy workspace do ustalania kolejności; można ją przypiąć, zmienić jej nazwę oraz nadać jej własną ikonę i kolor. Nowe workspace’y można tworzyć bezpośrednio wewnątrz grupy z poziomu jej nagłówka.", "workspaceGroupsLink": "Przeczytaj pełny przewodnik po grupach workspace'ów" }, "workspaceGroups": { "metaTitle": "Grupy workspace'ów", "metaDescription": "Zagnieżdżaj przestrzenie robocze w zwijanych, nazwanych sekcjach na pasku bocznym cmux. Model kotwicy, sposób tworzenia grupy i zarządzania nią oraz interfejs CLI workspace-group.", "title": "Grupy workspace'ów", - "intro": "Grupy workspace'ów pozwalają zagnieżdżać workspace'y w zwijanych, nazwanych sekcjach w pasku bocznym. Każda grupa ma niejawny kotwicowy workspace, konfigurowalny przycisk + do tworzenia nowych workspace'ów w jej wnętrzu oraz akcje dostępne po kliknięciu prawym przyciskiem do zmiany nazwy, przypinania, rozgrupowywania i edycji jej konfiguracji.", + "intro": "Grupy workspace’ów pozwalają zagnieżdżać workspace’y w zwijanych, nazwanych sekcjach paska bocznego. Każda grupa ma nagłówek w stylu folderu, niejawny kotwicowy workspace używany do ustalania kolejności, konfigurowalny przycisk + do tworzenia nowych workspace’ów wewnątrz niej oraz akcje pod prawym przyciskiem myszy do zmiany nazwy, przypinania, rozgrupowywania i edycji konfiguracji.", "concepts": "Koncepty", "anchorTitle": "Kotwicowy workspace", - "anchorDesc": "Każda grupa należy do dokładnie jednego workspace'a zwanego kotwicą. Nagłówek grupy w pasku bocznym jest reprezentacją kotwicy — nie ma dla niej osobnego wiersza. Kliknięcie obszaru nazwy w nagłówku ustawia fokus na panelach kotwicy; kliknięcie strzałki przełącza zwinięcie.", - "anchorNew": "Kotwice są zawsze całkowicie nowe w momencie utworzenia grupy. Nigdy nie są awansowane z istniejącego workspace'a. Katalog roboczy kotwicy jest dziedziczony z pierwszego zaznaczonego workspace'a przy grupowaniu zaznaczenia lub z aktywnego workspace'a przy tworzeniu za pomocą CLI bez --cwd.", + "anchorDesc": "Każda grupa ma dokładnie jeden workspace nazywany kotwicą. Kotwica jest istniejącym workspace’em należącym do grupy, używanym do pozycjonowania grupy i zapewnienia referencyjnego katalogu roboczego. Nagłówek grupy jest osobnym wierszem w stylu folderu; gdy grupa jest rozwinięta, kotwica pozostaje widoczna jako zwykły wiersz workspace’a pod nagłówkiem. Kliknięcie chevrona przełącza zwinięcie.", + "anchorNew": "Podczas tworzenia grupy cmux awansuje pierwszy kwalifikujący się wybrany workspace na kotwicę. Nie tworzy nowego terminalowego workspace’a tylko po to, aby pełnił rolę nagłówka grupy.", "anchorClose": "Zamknięcie kotwicowego workspace'a rozwiązuje grupę: każdy pozostały członek staje się niezgrupowanym workspace'em i pozostaje na liście kart. Nic poza samą kotwicą nie jest zamykane. Przed tym aplikacja wyświetla okno potwierdzenia z przełącznikiem „Nie pytaj ponownie”.", "identityTitle": "Tożsamość grupy", - "identityDesc": "Grupa ma nazwę, ikonę (SF Symbol, domyślnie folder.fill) oraz opcjonalny niestandardowy kolor. Są one niezależne od własnych dostosowań kotwicowego workspace'a. Kolor i ikona kotwicy są przy tworzeniu inicjowane z grupy, ale później mogą się różnić.", + "identityDesc": "Grupa ma nazwę, ikonę (SF Symbol, domyślnie folder.fill) oraz opcjonalny kolor niestandardowy. Są one niezależne od własnych dostosowań kotwicowego workspace’a.", "pinningTitle": "Przypinanie", "pinningDesc": "Grupy można przypinać niezależnie od przypięć poszczególnych workspace'ów. Przypięte wiersze najwyższego poziomu, niezależnie od tego, czy są to pojedyncze workspace'y, czy grupy, pozostają nad wierszami nieprzypiętymi. W obrębie każdego poziomu grupy i workspace'y zachowują kolejność, w jakiej je przeciągniesz.", "layoutIntro": "Pasek boczny układa wiersze najwyższego poziomu od góry do dołu:", "layoutItem1": "Przypięte wiersze najwyższego poziomu (workspace'y i grupy).", "layoutItem2": "Nieprzypięte wiersze najwyższego poziomu (workspace'y i grupy).", "creatingTitle": "Tworzenie grupy", - "creatingIntro": "Grupę można utworzyć na jeden z dwóch sposobów: za pomocą skrótu klawiszowego lub menu kontekstowego workspace’u. Oba wstawiają nowy workspace kotwiczący nad zaznaczeniem i przenoszą zaznaczone workspace’y do grupy. Gdy grupa już istnieje, zarządzasz nią i dodajesz do niej workspace’y z nagłówka grupy (zobacz Zarządzanie grupą poniżej).", + "creatingIntro": "Grupę tworzy się na jeden z dwóch sposobów: skrótem klawiaturowym albo z menu kontekstowego workspace’a. Oba przenoszą istniejące workspace’y pod nagłówek grupy w stylu folderu i awansują pierwszy kwalifikujący się wybrany workspace na kotwicę porządkującą. Żaden z nich nie tworzy nowego terminalowego workspace’a dla nagłówka. Gdy grupa już istnieje, zarządzasz nią i dodajesz do niej workspace’y z nagłówka grupy (zobacz Zarządzanie grupą poniżej).", "keyboardTitle": "Z klawiatury ({shortcut})", - "keyboardDesc": "Zaznacz dwa lub więcej workspace'ów w pasku bocznym, a następnie naciśnij {shortcut}. Nad zaznaczeniem zostaje wstawiony nowy kotwicowy workspace, a wszystkie zaznaczone workspace'y stają się jego elementami podrzędnymi. Grupa otrzymuje automatyczną nazwę Group 1, Group 2 i tak dalej — możesz ją zmienić w dowolnej chwili z menu kontekstowego nagłówka.", + "keyboardDesc": "Zaznacz dwa lub więcej workspace’ów w pasku bocznym, a następnie naciśnij {shortcut}. Zaznaczone workspace’y zostają przeniesione pod nowy nagłówek grupy w stylu folderu, a pierwszy zaznaczony workspace staje się kotwicą grupy. Nie jest tworzony żaden dodatkowy terminalowy workspace. Grupa otrzymuje automatyczną nazwę Group 1, Group 2 i tak dalej — możesz ją zmienić w dowolnej chwili z menu kontekstowego nagłówka.", "keyboardNote": "{shortcut} koliduje z domyślnym skrótem React Grab. Obsługa grup wykorzystuje ten akord tylko wtedy, gdy w pasku bocznym istnieje jawne wielokrotne zaznaczenie co najmniej dwóch workspace'ów, więc React Grab nadal działa przy pojedynczym zaznaczeniu oraz w kontekście przeglądarki lub terminala. Zmień przypisanie w Ustawienia → Klawiatura, jeśli wolisz, aby te dwie funkcje nie współdzieliły klawisza.", "keyboardSingle": "Grupy jednokartowe nie są tworzone za pomocą skrótu. W tym celu użyj pozycji „Nowa grupa z przestrzeni” w menu kontekstowym przestrzeni roboczej.", "contextMenuTitle": "Z menu kontekstowego workspace'a", diff --git a/web/messages/pt-BR.json b/web/messages/pt-BR.json index 1393909d81a8..c66f0b3db40f 100644 --- a/web/messages/pt-BR.json +++ b/web/messages/pt-BR.json @@ -675,30 +675,30 @@ "panelIdInternal": "ID do panel (interno)", "metaTitle": "Conceitos", "workspaceGroups": "Grupos de Workspaces", - "workspaceGroupsDesc": "Agrupe workspaces relacionados em seções nomeadas e recolhíveis na barra lateral. Cada grupo pertence a um workspace âncora e pode ser fixado, renomeado e receber seu próprio ícone e cor. Novos workspaces podem ser criados diretamente dentro de um grupo a partir do seu cabeçalho.", + "workspaceGroupsDesc": "Agrupe workspaces relacionados em seções nomeadas e recolhíveis na barra lateral. Cada grupo tem um cabeçalho em estilo de pasta e um workspace âncora para ordenação, e pode ser fixado, renomeado e receber seu próprio ícone e cor. Novos workspaces podem ser criados diretamente dentro de um grupo a partir do seu cabeçalho.", "workspaceGroupsLink": "Leia o guia completo de Grupos de Workspaces" }, "workspaceGroups": { "metaTitle": "Grupos de Workspaces", "metaDescription": "Aninhe áreas de trabalho em seções nomeadas recolhíveis na barra lateral do cmux. O modelo de âncora, como criar e gerenciar um grupo e a CLI workspace-group.", "title": "Grupos de Workspaces", - "intro": "Os grupos de workspaces permitem aninhar workspaces em seções nomeadas e recolhíveis na barra lateral. Cada grupo tem um workspace âncora implícito, um botão + personalizável para criar novos workspaces dentro dele e ações de clique com o botão direito para renomear, fixar, desagrupar e editar sua configuração.", + "intro": "Grupos de workspaces permitem aninhar workspaces em seções nomeadas e recolhíveis na barra lateral. Cada grupo tem um cabeçalho em estilo de pasta, um workspace âncora implícito usado para ordenação, um botão + personalizável para criar novos workspaces dentro dele e ações de clique direito para renomear, fixar, desagrupar e editar sua configuração.", "concepts": "Conceitos", "anchorTitle": "Workspace âncora", - "anchorDesc": "Todo grupo pertence a exatamente um workspace chamado âncora. O cabeçalho do grupo na barra lateral é a representação da âncora — não há uma linha separada para ela. Clicar na área do nome do cabeçalho foca os painéis da âncora; clicar no chevron alterna o recolhimento.", - "anchorNew": "As âncoras são sempre totalmente novas quando um grupo é criado. Elas nunca são promovidas a partir de um workspace existente. O diretório de trabalho da âncora é herdado do primeiro workspace selecionado ao agrupar uma seleção, ou do workspace ativo ao criar via CLI sem --cwd.", + "anchorDesc": "Todo grupo tem exatamente um workspace chamado de âncora. A âncora é um workspace membro existente usado para posicionar o grupo e fornecer um diretório de trabalho de referência. O cabeçalho do grupo é sua própria linha em estilo de pasta; quando o grupo está expandido, a âncora permanece visível como uma linha normal de workspace abaixo do cabeçalho. Clicar no chevron alterna o recolhimento.", + "anchorNew": "Quando um grupo é criado, o cmux promove o primeiro workspace selecionado elegível para ser a âncora. Ele não cria um novo workspace de terminal apenas para atuar como cabeçalho do grupo.", "anchorClose": "Fechar o workspace âncora dissolve o grupo: todos os outros membros tornam-se workspaces desagrupados e permanecem na lista de abas. Nada é fechado além da própria âncora. O aplicativo exibe uma caixa de diálogo de confirmação com uma opção \"Não perguntar novamente\" antes que isso aconteça.", "identityTitle": "Identidade do grupo", - "identityDesc": "Um grupo tem um nome, um ícone (um SF Symbol, por padrão folder.fill) e uma cor personalizada opcional. Esses elementos são independentes das próprias personalizações do workspace âncora. A cor e o ícone da âncora são inicializados a partir do grupo na criação, mas podem divergir depois.", + "identityDesc": "Um grupo tem um nome, um ícone (um SF Symbol, padrão folder.fill) e uma cor personalizada opcional. Esses itens são independentes das próprias personalizações do workspace âncora.", "pinningTitle": "Fixação", "pinningDesc": "Os grupos podem ser fixados de forma independente das fixações de workspaces individuais. As linhas de nível superior fixadas, sejam workspaces individuais ou grupos, permanecem acima das linhas não fixadas. Dentro de cada nível, grupos e workspaces mantêm a ordem na qual você os arrasta.", "layoutIntro": "A barra lateral organiza as linhas de nível superior de cima para baixo:", "layoutItem1": "Linhas de nível superior fixadas (workspaces e grupos).", "layoutItem2": "Linhas de nível superior não fixadas (workspaces e grupos).", "creatingTitle": "Criando um grupo", - "creatingIntro": "Um grupo é criado de uma de duas maneiras: por um atalho de teclado ou pelo menu de contexto de um workspace. Ambos inserem um novo workspace âncora acima da seleção e movem os workspaces selecionados para o grupo. Depois que um grupo existe, você o gerencia e adiciona workspaces a ele a partir do cabeçalho do grupo (consulte Gerenciar um grupo abaixo).", + "creatingIntro": "Um grupo é criado de uma de duas formas: por um atalho de teclado ou por um menu de contexto de workspace. Ambas movem workspaces existentes para baixo de um cabeçalho de grupo em estilo de pasta e promovem o primeiro workspace selecionado elegível como âncora de ordenação. Nenhuma delas cria um novo workspace de terminal para o cabeçalho. Depois que um grupo existe, você o gerencia e adiciona workspaces a ele a partir do cabeçalho do grupo (veja Gerenciando um grupo abaixo).", "keyboardTitle": "Pelo teclado ({shortcut})", - "keyboardDesc": "Selecione dois ou mais workspaces na barra lateral e pressione {shortcut}. Um novo workspace âncora é inserido acima da seleção e todos os workspaces selecionados tornam-se filhos. O grupo é nomeado automaticamente como Group 1, Group 2 e assim por diante — renomeie-o a qualquer momento pelo menu de contexto do cabeçalho.", + "keyboardDesc": "Selecione dois ou mais workspaces na barra lateral e pressione {shortcut}. Os workspaces selecionados são movidos para baixo de um novo cabeçalho de grupo em estilo de pasta, e o primeiro workspace selecionado se torna a âncora do grupo. Nenhum workspace de terminal extra é criado. O grupo é nomeado automaticamente como Group 1, Group 2 e assim por diante — renomeie-o a qualquer momento pelo menu de contexto do cabeçalho.", "keyboardNote": "{shortcut} colide com o padrão do React Grab. O manipulador de grupo só consome o atalho quando há uma multisseleção explícita de pelo menos dois workspaces na barra lateral, então o React Grab ainda é acionado em seleção única e em contextos de navegador ou terminal. Reatribua-o em Configurações → Teclado se preferir que os dois não compartilhem uma tecla.", "keyboardSingle": "Grupos de aba única não são criados pelo atalho. Use a entrada Novo grupo a partir desta área no menu de contexto da área de trabalho para isso.", "contextMenuTitle": "Pelo menu de contexto de um workspace", diff --git a/web/messages/ru.json b/web/messages/ru.json index cf8019ea4b47..fe36b3ac2151 100644 --- a/web/messages/ru.json +++ b/web/messages/ru.json @@ -675,30 +675,30 @@ "panelIdInternal": "ID панели (внутренний)", "metaTitle": "Концепции", "workspaceGroups": "Группы рабочих пространств", - "workspaceGroupsDesc": "Объединяйте связанные рабочие пространства в сворачиваемые именованные секции на боковой панели. Каждая группа принадлежит якорному рабочему пространству, её можно закрепить, переименовать и задать ей собственный значок и цвет. Новые рабочие пространства можно создавать прямо внутри группы из её заголовка.", + "workspaceGroupsDesc": "Объединяйте связанные рабочие пространства в сворачиваемые именованные секции на боковой панели. У каждой группы есть заголовок в стиле папки и якорное рабочее пространство для порядка; группу можно закрепить, переименовать и задать ей собственные значок и цвет. Новые рабочие пространства можно создавать прямо внутри группы из её заголовка.", "workspaceGroupsLink": "Читать полное руководство по группам рабочих пространств" }, "workspaceGroups": { "metaTitle": "Группы рабочих пространств", "metaDescription": "Размещайте рабочие пространства во вложенных сворачиваемых именованных секциях на боковой панели cmux. Якорная модель, как создавать группу и управлять ею, а также CLI workspace-group.", "title": "Группы рабочих пространств", - "intro": "Группы рабочих пространств позволяют вкладывать рабочие пространства в сворачиваемые именованные секции на боковой панели. У каждой группы есть неявное якорное рабочее пространство, настраиваемая кнопка + для создания новых рабочих пространств внутри неё и действия по правому клику для переименования, закрепления, разгруппировки и редактирования конфигурации.", + "intro": "Группы рабочих пространств позволяют вкладывать рабочие пространства в сворачиваемые именованные секции на боковой панели. У каждой группы есть заголовок в стиле папки, неявное якорное рабочее пространство, используемое для порядка, настраиваемая кнопка + для создания новых рабочих пространств внутри неё и действия по правому клику для переименования, закрепления, разгруппировки и редактирования конфигурации.", "concepts": "Концепции", "anchorTitle": "Якорное рабочее пространство", - "anchorDesc": "Каждая группа принадлежит ровно одному рабочему пространству, называемому якорем. Заголовок группы на боковой панели — это представление якоря; отдельной строки для него нет. Клик по области имени в заголовке фокусирует панели якоря; клик по шеврону сворачивает или разворачивает группу.", - "anchorNew": "Якоря всегда создаются с нуля при создании группы. Они никогда не повышаются из существующего рабочего пространства. Рабочий каталог якоря наследуется от первого выбранного рабочего пространства при группировке выделения или от активного рабочего пространства при создании через CLI без --cwd.", + "anchorDesc": "У каждой группы есть ровно одно рабочее пространство, называемое якорем. Якорь — это существующее рабочее пространство-участник, используемое для позиционирования группы и как эталонный рабочий каталог. Заголовок группы является отдельной строкой в стиле папки; когда группа развернута, якорь остаётся видимым как обычная строка рабочего пространства под заголовком. Щелчок по шеврону переключает сворачивание.", + "anchorNew": "При создании группы cmux повышает первое подходящее выбранное рабочее пространство до якоря. Он не создаёт новое терминальное рабочее пространство только для того, чтобы оно служило заголовком группы.", "anchorClose": "Закрытие якорного рабочего пространства распускает группу: все остальные участники становятся несгруппированными рабочими пространствами и остаются в списке вкладок. Кроме самого якоря ничего не закрывается. Перед этим приложение показывает диалог подтверждения с переключателем «Больше не спрашивать».", "identityTitle": "Идентичность группы", - "identityDesc": "У группы есть имя, значок (SF Symbol, по умолчанию folder.fill) и необязательный пользовательский цвет. Они независимы от собственных настроек якорного рабочего пространства. Цвет и значок якоря при создании берутся из группы, но затем могут разойтись.", + "identityDesc": "У группы есть имя, значок (SF Symbol, по умолчанию folder.fill) и необязательный пользовательский цвет. Они независимы от собственных настроек якорного рабочего пространства.", "pinningTitle": "Закрепление", "pinningDesc": "Группы можно закреплять независимо от закрепления отдельных рабочих пространств. Закреплённые строки верхнего уровня, будь то отдельные рабочие пространства или группы, остаются выше незакреплённых строк. Внутри каждого уровня группы и рабочие пространства сохраняют порядок, в котором вы их перетащили.", "layoutIntro": "Боковая панель располагает строки верхнего уровня сверху вниз:", "layoutItem1": "Закреплённые строки верхнего уровня (рабочие пространства и группы).", "layoutItem2": "Незакреплённые строки верхнего уровня (рабочие пространства и группы).", "creatingTitle": "Создание группы", - "creatingIntro": "Группу можно создать одним из двух способов: с помощью сочетания клавиш или через контекстное меню рабочего пространства. В обоих случаях над выделением вставляется новое опорное рабочее пространство, а выбранные рабочие пространства перемещаются в группу. После создания группы вы управляете ею и добавляете в неё рабочие пространства из заголовка группы (см. «Управление группой» ниже).", + "creatingIntro": "Группа создаётся одним из двух способов: сочетанием клавиш или через контекстное меню рабочего пространства. Оба способа перемещают существующие рабочие пространства под заголовок группы в стиле папки и повышают первое подходящее выбранное рабочее пространство до якоря порядка. Ни один способ не создаёт новое терминальное рабочее пространство для заголовка. После создания группы вы управляете ею и добавляете в неё рабочие пространства из заголовка группы (см. Управление группой ниже).", "keyboardTitle": "С клавиатуры ({shortcut})", - "keyboardDesc": "Выберите два или более рабочих пространства на боковой панели, затем нажмите {shortcut}. Новое якорное рабочее пространство вставляется над выделением, и все выбранные рабочие пространства становятся дочерними. Группе автоматически присваивается имя Group 1, Group 2 и так далее — переименуйте её в любой момент через контекстное меню заголовка.", + "keyboardDesc": "Выберите два или более рабочих пространства на боковой панели, затем нажмите {shortcut}. Выбранные рабочие пространства перемещаются под новый заголовок группы в стиле папки, а первое выбранное рабочее пространство становится якорем группы. Дополнительное терминальное рабочее пространство не создаётся. Группе автоматически присваивается имя Group 1, Group 2 и так далее — переименуйте её в любой момент через контекстное меню заголовка.", "keyboardNote": "{shortcut} конфликтует со значением по умолчанию в React Grab. Обработчик группы перехватывает аккорд только при явном множественном выделении на боковой панели как минимум двух рабочих пространств, поэтому React Grab по-прежнему срабатывает при одиночном выделении, а также в контексте браузера или терминала. Переназначьте его в Настройки → Клавиатура, если не хотите, чтобы они делили одну клавишу.", "keyboardSingle": "Группы из одной вкладки не создаются с помощью этого сочетания клавиш. Для этого используйте пункт «Новая группа из этого пространства» в контекстном меню рабочего пространства.", "contextMenuTitle": "Из контекстного меню рабочего пространства", diff --git a/web/messages/th.json b/web/messages/th.json index 4dfc593f2067..d223cf51b75b 100644 --- a/web/messages/th.json +++ b/web/messages/th.json @@ -675,30 +675,30 @@ "panelIdInternal": "Panel ID (ภายใน)", "metaTitle": "แนวคิด", "workspaceGroups": "กลุ่มเวิร์กสเปซ", - "workspaceGroupsDesc": "จัดกลุ่มเวิร์กสเปซที่เกี่ยวข้องกันให้เป็นส่วนที่มีชื่อและยุบ-ขยายได้ในแถบด้านข้าง แต่ละกลุ่มมีเวิร์กสเปซแองเคอร์เป็นเจ้าของ และสามารถปักหมุด เปลี่ยนชื่อ รวมถึงกำหนดไอคอนและสีของตัวเองได้ คุณสามารถสร้างเวิร์กสเปซใหม่ภายในกลุ่มได้โดยตรงจากส่วนหัวของกลุ่ม", + "workspaceGroupsDesc": "จัดกลุ่มเวิร์กสเปซที่เกี่ยวข้องกันให้เป็นส่วนที่มีชื่อและยุบ-ขยายได้ในแถบด้านข้าง แต่ละกลุ่มมีส่วนหัวแบบโฟลเดอร์และเวิร์กสเปซแองเคอร์สำหรับการจัดลำดับ และสามารถปักหมุด เปลี่ยนชื่อ รวมถึงกำหนดไอคอนและสีของตัวเองได้ คุณสามารถสร้างเวิร์กสเปซใหม่ภายในกลุ่มได้โดยตรงจากส่วนหัวของกลุ่ม", "workspaceGroupsLink": "อ่านคู่มือกลุ่มเวิร์กสเปซฉบับเต็ม" }, "workspaceGroups": { "metaTitle": "กลุ่มเวิร์กสเปซ", "metaDescription": "จัดเวิร์กสเปซให้ซ้อนอยู่ในส่วนที่มีชื่อและยุบได้ในแถบด้านข้างของ cmux ครอบคลุมโมเดลแองเคอร์ วิธีสร้างและจัดการกลุ่ม และ CLI ของ workspace-group", "title": "กลุ่มเวิร์กสเปซ", - "intro": "กลุ่มเวิร์กสเปซช่วยให้คุณซ้อนเวิร์กสเปซเป็นส่วนที่มีชื่อและยุบ-ขยายได้ในแถบด้านข้าง แต่ละกลุ่มมีเวิร์กสเปซแองเคอร์โดยปริยาย มีปุ่ม + ที่ปรับแต่งได้สำหรับสร้างเวิร์กสเปซใหม่ภายในกลุ่ม และมีการกระทำจากการคลิกขวาสำหรับเปลี่ยนชื่อ ปักหมุด ยกเลิกการจัดกลุ่ม และแก้ไขการตั้งค่าของกลุ่ม", + "intro": "กลุ่มเวิร์กสเปซช่วยให้คุณซ้อนเวิร์กสเปซไว้ในส่วนที่มีชื่อและยุบ-ขยายได้ในแถบด้านข้าง แต่ละกลุ่มมีส่วนหัวแบบโฟลเดอร์ เวิร์กสเปซแองเคอร์โดยนัยที่ใช้สำหรับการจัดลำดับ ปุ่ม + ที่ปรับแต่งได้สำหรับสร้างเวิร์กสเปซใหม่ภายในกลุ่ม และการคลิกขวาสำหรับเปลี่ยนชื่อ ปักหมุด ยกเลิกการจัดกลุ่ม และแก้ไขการกำหนดค่า", "concepts": "แนวคิด", "anchorTitle": "เวิร์กสเปซแองเคอร์", - "anchorDesc": "ทุกกลุ่มมีเวิร์กสเปซเพียงหนึ่งเดียวเป็นเจ้าของ เรียกว่าแองเคอร์ ส่วนหัวของกลุ่มในแถบด้านข้างคือตัวแทนของแองเคอร์ — ไม่มีแถวแยกต่างหากสำหรับมัน การคลิกที่บริเวณชื่อในส่วนหัวจะโฟกัสไปยังแผงของแองเคอร์ ส่วนการคลิกที่ลูกศรชี้จะสลับการยุบ-ขยาย", - "anchorNew": "แองเคอร์จะเป็นเวิร์กสเปซใหม่เอี่ยมเสมอเมื่อมีการสร้างกลุ่ม โดยจะไม่ถูกเลื่อนขึ้นมาจากเวิร์กสเปซที่มีอยู่เดิม ไดเรกทอรีการทำงานของแองเคอร์จะสืบทอดมาจากเวิร์กสเปซแรกที่เลือกเมื่อจัดกลุ่มจากการเลือกหลายรายการ หรือจากเวิร์กสเปซที่กำลังใช้งานอยู่เมื่อสร้างผ่าน CLI โดยไม่ระบุ --cwd", + "anchorDesc": "ทุกกลุ่มมีเวิร์กสเปซหนึ่งรายการที่เรียกว่าแองเคอร์ แองเคอร์คือเวิร์กสเปซสมาชิกที่มีอยู่แล้ว ใช้สำหรับวางตำแหน่งกลุ่มและเป็นไดเรกทอรีทำงานอ้างอิง ส่วนหัวของกลุ่มเป็นแถวแบบโฟลเดอร์ของตัวเอง เมื่อกลุ่มถูกขยาย แองเคอร์จะยังมองเห็นเป็นแถวเวิร์กสเปซปกติใต้ส่วนหัว คลิกเชฟรอนเพื่อสลับการยุบ", + "anchorNew": "เมื่อสร้างกลุ่ม cmux จะเลื่อนเวิร์กสเปซที่เลือกและมีสิทธิ์รายการแรกให้เป็นแองเคอร์ โดยจะไม่สร้างเวิร์กสเปซเทอร์มินัลใหม่เพียงเพื่อใช้เป็นส่วนหัวของกลุ่ม", "anchorClose": "การปิดเวิร์กสเปซแองเคอร์จะสลายกลุ่ม สมาชิกอื่นทุกตัวจะกลายเป็นเวิร์กสเปซที่ไม่ได้จัดกลุ่มและยังคงอยู่ในรายการแท็บ ไม่มีสิ่งใดถูกปิดนอกจากตัวแองเคอร์เอง แอปจะแสดงกล่องโต้ตอบยืนยันพร้อมตัวสลับ \"อย่าถามอีก\" ก่อนที่สิ่งนี้จะเกิดขึ้น", "identityTitle": "อัตลักษณ์ของกลุ่ม", - "identityDesc": "กลุ่มมีชื่อ ไอคอน (เป็น SF Symbol โดยค่าเริ่มต้นคือ folder.fill) และสีกำหนดเองที่ใส่หรือไม่ใส่ก็ได้ สิ่งเหล่านี้เป็นอิสระจากการปรับแต่งของเวิร์กสเปซแองเคอร์เอง สีและไอคอนของแองเคอร์จะถูกตั้งค่าเริ่มต้นจากกลุ่มเมื่อสร้าง แต่หลังจากนั้นสามารถแยกต่างกันได้", + "identityDesc": "กลุ่มมีชื่อ ไอคอน (SF Symbol ค่าเริ่มต้นคือ folder.fill) และสีที่กำหนดเองได้เป็นตัวเลือก สิ่งเหล่านี้เป็นอิสระจากการปรับแต่งของเวิร์กสเปซแองเคอร์เอง", "pinningTitle": "การปักหมุด", "pinningDesc": "กลุ่มสามารถปักหมุดได้อย่างเป็นอิสระจากการปักหมุดเวิร์กสเปซแต่ละตัว แถวระดับบนสุดที่ปักหมุดไว้ ไม่ว่าจะเป็นเวิร์กสเปซเดี่ยวหรือกลุ่ม จะอยู่เหนือแถวที่ไม่ได้ปักหมุด ภายในแต่ละชั้น กลุ่มและเวิร์กสเปซจะคงลำดับตามที่คุณลากเข้าไป", "layoutIntro": "แถบด้านข้างจัดวางแถวระดับบนสุดจากบนลงล่างดังนี้:", "layoutItem1": "แถวระดับบนสุดที่ปักหมุดไว้ (เวิร์กสเปซและกลุ่ม)", "layoutItem2": "แถวระดับบนสุดที่ไม่ได้ปักหมุด (เวิร์กสเปซและกลุ่ม)", "creatingTitle": "การสร้างกลุ่ม", - "creatingIntro": "กลุ่มสามารถสร้างได้สองวิธี ได้แก่ แป้นพิมพ์ลัดหรือเมนูบริบทของ workspace ทั้งสองวิธีจะแทรก workspace ที่เป็นจุดยึดใหม่ไว้เหนือรายการที่เลือก และย้าย workspace ที่เลือกเข้าไปในกลุ่ม เมื่อมีกลุ่มแล้ว คุณสามารถจัดการกลุ่มและเพิ่ม workspace เข้าไปได้จากส่วนหัวของกลุ่ม (ดูที่ การจัดการกลุ่ม ด้านล่าง)", + "creatingIntro": "กลุ่มถูกสร้างได้สองวิธี: แป้นพิมพ์ลัดหรือเมนูบริบทของเวิร์กสเปซ ทั้งสองวิธีย้ายเวิร์กสเปซที่มีอยู่เดิมไปไว้ใต้ส่วนหัวกลุ่มแบบโฟลเดอร์ และเลื่อนเวิร์กสเปซที่เลือกและมีสิทธิ์รายการแรกเป็นแองเคอร์สำหรับการจัดลำดับ ทั้งสองวิธีไม่สร้างเวิร์กสเปซเทอร์มินัลใหม่สำหรับส่วนหัว เมื่อมีกลุ่มแล้ว คุณจัดการกลุ่มและเพิ่มเวิร์กสเปซเข้าไปได้จากส่วนหัวของกลุ่ม (ดู การจัดการกลุ่ม ด้านล่าง)", "keyboardTitle": "จากแป้นพิมพ์ ({shortcut})", - "keyboardDesc": "เลือกเวิร์กสเปซตั้งแต่สองตัวขึ้นไปในแถบด้านข้าง แล้วกด {shortcut} เวิร์กสเปซแองเคอร์ใหม่จะถูกแทรกไว้เหนือรายการที่เลือก และเวิร์กสเปซที่เลือกทั้งหมดจะกลายเป็นสมาชิกย่อย กลุ่มจะถูกตั้งชื่ออัตโนมัติเป็น Group 1, Group 2 และต่อไปเรื่อย ๆ — เปลี่ยนชื่อได้ทุกเมื่อผ่านเมนูบริบทของส่วนหัว", + "keyboardDesc": "เลือกเวิร์กสเปซตั้งแต่สองตัวขึ้นไปในแถบด้านข้าง แล้วกด {shortcut} เวิร์กสเปซที่เลือกจะย้ายไปอยู่ใต้ส่วนหัวกลุ่มใหม่แบบโฟลเดอร์ และเวิร์กสเปซที่เลือกตัวแรกจะกลายเป็นแองเคอร์ของกลุ่ม ไม่มีการสร้างเวิร์กสเปซเทอร์มินัลเพิ่มเติม กลุ่มจะถูกตั้งชื่ออัตโนมัติเป็น Group 1, Group 2 และต่อไปเรื่อย ๆ — เปลี่ยนชื่อได้ทุกเมื่อผ่านเมนูบริบทของส่วนหัว", "keyboardNote": "{shortcut} ชนกับค่าเริ่มต้นของ React Grab ตัวจัดการกลุ่มจะใช้คอร์ดนี้เฉพาะเมื่อมีการเลือกหลายรายการในแถบด้านข้างอย่างชัดเจนตั้งแต่สองเวิร์กสเปซขึ้นไป ดังนั้น React Grab จึงยังคงทำงานในการเลือกรายการเดียว และในบริบทของเบราว์เซอร์หรือเทอร์มินัล ปรับการผูกปุ่มใหม่ได้ใน Settings → Keyboard หากคุณไม่ต้องการให้ทั้งสองใช้ปุ่มร่วมกัน", "keyboardSingle": "กลุ่มแบบแท็บเดียวจะไม่ถูกสร้างจากแป้นพิมพ์ลัด ให้ใช้รายการ สร้างกลุ่มใหม่จากเวิร์กสเปซนี้ ในเมนูคลิกขวาของเวิร์กสเปซสำหรับกรณีนั้น", "contextMenuTitle": "จากเมนูบริบทของเวิร์กสเปซ", diff --git a/web/messages/tr.json b/web/messages/tr.json index 5232077bb788..1f001cfc095f 100644 --- a/web/messages/tr.json +++ b/web/messages/tr.json @@ -675,30 +675,30 @@ "panelIdInternal": "Panel ID (dahili)", "metaTitle": "Kavramlar", "workspaceGroups": "Çalışma Alanı Grupları", - "workspaceGroupsDesc": "İlgili çalışma alanlarını kenar çubuğunda daraltılabilir, adlandırılmış bölümler halinde gruplayın. Her grup bir bağlantı (anchor) çalışma alanına aittir; sabitlenebilir, yeniden adlandırılabilir ve kendi simgesi ile rengine sahip olabilir. Yeni çalışma alanları doğrudan bir grubun başlığından grubun içinde oluşturulabilir.", + "workspaceGroupsDesc": "İlgili çalışma alanlarını kenar çubuğunda daraltılabilir, adlandırılmış bölümler halinde gruplayın. Her grubun klasör tarzı bir başlığı ve sıralama için bir bağlantı çalışma alanı vardır; sabitlenebilir, yeniden adlandırılabilir ve kendi simgesi ile rengine sahip olabilir. Yeni çalışma alanları doğrudan grubun başlığından grubun içinde oluşturulabilir.", "workspaceGroupsLink": "Çalışma Alanı Grupları kılavuzunun tamamını okuyun" }, "workspaceGroups": { "metaTitle": "Çalışma Alanı Grupları", "metaDescription": "Çalışma alanlarını cmux kenar çubuğunda daraltılabilir adlandırılmış bölümler halinde gruplandırın. Çapa modeli, bir grubun nasıl oluşturulup yönetileceği ve workspace-group CLI.", "title": "Çalışma Alanı Grupları", - "intro": "Çalışma alanı grupları, çalışma alanlarını kenar çubuğunda daraltılabilir, adlandırılmış bölümler halinde iç içe yerleştirmenize olanak tanır. Her grubun örtük bir bağlantı (anchor) çalışma alanı, içinde yeni çalışma alanları oluşturmak için özelleştirilebilir bir + düğmesi ve yeniden adlandırma, sabitleme, grubu çözme ile yapılandırmasını düzenleme için sağ tıklama eylemleri vardır.", + "intro": "Çalışma alanı grupları, çalışma alanlarını kenar çubuğunda daraltılabilir, adlandırılmış bölümlere yerleştirmenizi sağlar. Her grubun klasör tarzı bir başlığı, sıralama için kullanılan örtük bir bağlantı çalışma alanı, içinde yeni çalışma alanları oluşturmak için özelleştirilebilir bir + düğmesi ve yeniden adlandırma, sabitleme, grubu çözme ve yapılandırmasını düzenleme için sağ tık eylemleri vardır.", "concepts": "Kavramlar", "anchorTitle": "Bağlantı (anchor) çalışma alanı", - "anchorDesc": "Her grup, bağlantı (anchor) adı verilen tam olarak bir çalışma alanına aittir. Kenar çubuğundaki grup başlığı, bağlantının temsilidir; onun için ayrı bir satır yoktur. Başlık adı alanına tıklamak bağlantının panellerini odaklar; çevron simgesine tıklamak ise daraltmayı açıp kapatır.", - "anchorNew": "Bir grup oluşturulduğunda bağlantılar her zaman yepyenidir. Mevcut bir çalışma alanından asla yükseltilmezler. Bağlantının çalışma dizini, bir seçim gruplanırken seçilen ilk çalışma alanından ya da CLI üzerinden --cwd olmadan oluşturulurken etkin çalışma alanından devralınır.", + "anchorDesc": "Her grubun bağlantı adı verilen tam olarak bir çalışma alanı vardır. Bağlantı, grubu konumlandırmak ve referans çalışma dizini sağlamak için kullanılan mevcut bir üye çalışma alanıdır. Grup başlığı klasör tarzında kendi satırıdır; grup genişletildiğinde bağlantı, başlığın altında normal bir çalışma alanı satırı olarak görünmeye devam eder. Chevron’a tıklamak daraltmayı değiştirir.", + "anchorNew": "Bir grup oluşturulduğunda cmux, uygun olan ilk seçili çalışma alanını bağlantı olarak yükseltir. Yalnızca grup başlığı görevi görmesi için yeni bir terminal çalışma alanı oluşturmaz.", "anchorClose": "Bağlantı çalışma alanını kapatmak grubu dağıtır: diğer her üye gruplanmamış bir çalışma alanı haline gelir ve sekmeler listesinde kalır. Bağlantının kendisi dışında hiçbir şey kapatılmaz. Uygulama bu gerçekleşmeden önce \"Bir daha sorma\" geçişine sahip bir onay penceresi gösterir.", "identityTitle": "Grup kimliği", - "identityDesc": "Bir grubun adı, bir simgesi (bir SF Symbol, varsayılan olarak folder.fill) ve isteğe bağlı bir özel rengi vardır. Bunlar bağlantı çalışma alanının kendi özelleştirmelerinden bağımsızdır. Bağlantının rengi ve simgesi oluşturma sırasında gruptan alınır, ancak sonrasında farklılaşabilirler.", + "identityDesc": "Bir grubun adı, simgesi (SF Symbol, varsayılan folder.fill) ve isteğe bağlı özel rengi vardır. Bunlar bağlantı çalışma alanının kendi özelleştirmelerinden bağımsızdır.", "pinningTitle": "Sabitleme", "pinningDesc": "Gruplar, tek tek çalışma alanı sabitlemelerinden bağımsız olarak sabitlenebilir. Sabitlenmiş üst düzey satırlar, ister tek tek çalışma alanları ister gruplar olsun, sabitlenmemiş satırların üzerinde kalır. Her katman içinde gruplar ve çalışma alanları, onları sürüklediğiniz sırayı korur.", "layoutIntro": "Kenar çubuğu, üst düzey satırları yukarıdan aşağıya doğru düzenler:", "layoutItem1": "Sabitlenmiş üst düzey satırlar (çalışma alanları ve gruplar).", "layoutItem2": "Sabitlenmemiş üst düzey satırlar (çalışma alanları ve gruplar).", "creatingTitle": "Bir grup oluşturma", - "creatingIntro": "Bir grup iki yoldan biriyle oluşturulur: klavye kısayolu veya bir workspace bağlam menüsü. Her ikisi de seçimin üzerine yeni bir çapa workspace ekler ve seçili workspace’leri gruba taşır. Bir grup oluşturulduktan sonra, onu grup başlığından yönetir ve ona workspace eklersiniz (aşağıdaki Bir grubu yönetme bölümüne bakın).", + "creatingIntro": "Bir grup iki yoldan biriyle oluşturulur: klavye kısayolu veya çalışma alanı bağlam menüsü. Her ikisi de mevcut çalışma alanlarını klasör tarzı bir grup başlığının altına taşır ve uygun olan ilk seçili çalışma alanını sıralama bağlantısı olarak yükseltir. Hiçbiri başlık için yeni bir terminal çalışma alanı oluşturmaz. Grup var olduktan sonra onu yönetir ve çalışma alanlarını grup başlığından eklersiniz (aşağıdaki Grup yönetme bölümüne bakın).", "keyboardTitle": "Klavyeden ({shortcut})", - "keyboardDesc": "Kenar çubuğunda iki veya daha fazla çalışma alanı seçin, ardından {shortcut} tuşlarına basın. Seçimin üzerine yeni bir bağlantı çalışma alanı eklenir ve seçilen tüm çalışma alanları alt öğeler haline gelir. Grup otomatik olarak Group 1, Group 2 ve bu şekilde adlandırılır; başlık bağlam menüsü aracılığıyla istediğiniz zaman yeniden adlandırın.", + "keyboardDesc": "Kenar çubuğunda iki veya daha fazla çalışma alanı seçin, ardından {shortcut} tuşlarına basın. Seçili çalışma alanları yeni bir klasör tarzı grup başlığının altına taşınır ve ilk seçili çalışma alanı grubun bağlantısı olur. Ek bir terminal çalışma alanı oluşturulmaz. Grup otomatik olarak Group 1, Group 2 ve bu şekilde adlandırılır; başlık bağlam menüsü aracılığıyla istediğiniz zaman yeniden adlandırın.", "keyboardNote": "{shortcut}, React Grab'in varsayılanıyla çakışır. Grup işleyicisi yalnızca kenar çubuğunda en az iki çalışma alanından oluşan açık bir çoklu seçim olduğunda bu tuş bileşimini tüketir, bu nedenle React Grab tek seçimde ve tarayıcı ya da terminal bağlamlarında hâlâ çalışır. İkisinin bir tuşu paylaşmasını istemiyorsanız Ayarlar → Klavye bölümünden yeniden atayın.", "keyboardSingle": "Tek sekmeli gruplar kısayoldan oluşturulmaz. Bunun için çalışma alanı bağlam menüsündeki Bu çalışma alanından yeni grup girişini kullanın.", "contextMenuTitle": "Bir çalışma alanı bağlam menüsünden", diff --git a/web/messages/uk.json b/web/messages/uk.json index fe9466b85f74..9be460ad3b91 100644 --- a/web/messages/uk.json +++ b/web/messages/uk.json @@ -675,30 +675,30 @@ "paneIdSocket": "Pane ID (socket API)", "panelIdInternal": "Panel ID (внутрішній)", "workspaceGroups": "Групи робочих просторів", - "workspaceGroupsDesc": "Об'єднуйте пов'язані робочі простори у згортувані іменовані розділи на бічній панелі. Кожна група належить якірному робочому простору, її можна закріпити, перейменувати та призначити власну іконку й колір. Нові робочі простори можна створювати безпосередньо всередині групи з її заголовка.", + "workspaceGroupsDesc": "Об’єднуйте пов’язані робочі простори у згортувані іменовані розділи на бічній панелі. Кожна група має заголовок у стилі папки та якірний робочий простір для впорядкування; її можна закріпити, перейменувати та призначити власну іконку й колір. Нові робочі простори можна створювати безпосередньо всередині групи з її заголовка.", "workspaceGroupsLink": "Прочитати повний посібник із груп робочих просторів" }, "workspaceGroups": { "metaTitle": "Групи робочих просторів", "metaDescription": "Вкладайте робочі області у згортувані іменовані розділи на бічній панелі cmux. Модель прив'язки, як створювати групу та керувати нею, і CLI workspace-group.", "title": "Групи робочих просторів", - "intro": "Групи робочих просторів дозволяють вкладати робочі простори у згортувані іменовані розділи на бічній панелі. Кожна група має неявний якірний робочий простір, налаштовувану кнопку + для створення нових робочих просторів усередині неї та дії правою кнопкою миші для перейменування, закріплення, розгрупування й редагування її конфігурації.", + "intro": "Групи робочих просторів дають змогу вкладати робочі простори у згортувані іменовані розділи на бічній панелі. Кожна група має заголовок у стилі папки, неявний якірний робочий простір для впорядкування, настроювану кнопку + для створення нових робочих просторів усередині неї та дії правою кнопкою миші для перейменування, закріплення, розгрупування й редагування конфігурації.", "concepts": "Концепції", "anchorTitle": "Якірний робочий простір", - "anchorDesc": "Кожна група належить рівно одному робочому простору, який називається якорем. Заголовок групи на бічній панелі — це представлення якоря; для нього немає окремого рядка. Натискання на область імені заголовка фокусує панелі якоря; натискання на стрілку перемикає згортання.", - "anchorNew": "Якорі завжди абсолютно нові під час створення групи. Вони ніколи не підвищуються з наявного робочого простору. Робочий каталог якоря успадковується від першого вибраного робочого простору під час групування виділення або від активного робочого простору під час створення через CLI без --cwd.", + "anchorDesc": "Кожна група має рівно один робочий простір, який називається якорем. Якір — це наявний робочий простір-учасник, який використовується для позиціонування групи та як довідковий робочий каталог. Заголовок групи є окремим рядком у стилі папки; коли групу розгорнуто, якір залишається видимим як звичайний рядок робочого простору під заголовком. Клацання шеврона перемикає згортання.", + "anchorNew": "Під час створення групи cmux підвищує перший придатний вибраний робочий простір до якоря. Він не створює новий термінальний робочий простір лише для ролі заголовка групи.", "anchorClose": "Закриття якірного робочого простору розпускає групу: кожен інший учасник стає незгрупованим робочим простором і залишається у списку вкладок. Нічого, окрім самого якоря, не закривається. Перед цим застосунок показує діалог підтвердження з перемикачем «Не питати знову».", "identityTitle": "Ідентичність групи", - "identityDesc": "Група має назву, іконку (SF Symbol, за замовчуванням folder.fill) та необов'язковий власний колір. Вони незалежні від власних налаштувань якірного робочого простору. Колір та іконка якоря беруться з групи під час створення, але згодом можуть розходитися.", + "identityDesc": "Група має назву, іконку (SF Symbol, типово folder.fill) і необов’язковий власний колір. Вони незалежні від власних налаштувань якірного робочого простору.", "pinningTitle": "Закріплення", "pinningDesc": "Групи можна закріплювати незалежно від закріплень окремих робочих просторів. Закріплені рядки верхнього рівня, як окремі робочі простори, так і групи, залишаються над незакріпленими рядками. У межах кожного рівня групи та робочі простори зберігають порядок, у якому ви їх перетягнули.", "layoutIntro": "Бічна панель розташовує рядки верхнього рівня згори донизу:", "layoutItem1": "Закріплені рядки верхнього рівня (робочі простори та групи).", "layoutItem2": "Незакріплені рядки верхнього рівня (робочі простори та групи).", "creatingTitle": "Створення групи", - "creatingIntro": "Групу можна створити одним із двох способів: за допомогою комбінації клавіш або контекстного меню робочого простору. В обох випадках над виділенням вставляється новий опорний робочий простір, а вибрані робочі простори переміщуються до групи. Після створення групи ви керуєте нею та додаєте до неї робочі простори із заголовка групи (див. «Керування групою» нижче).", + "creatingIntro": "Групу створюють одним із двох способів: клавіатурним скороченням або з контекстного меню робочого простору. Обидва переміщують наявні робочі простори під заголовок групи у стилі папки та підвищують перший придатний вибраний робочий простір до якоря впорядкування. Жоден із них не створює новий термінальний робочий простір для заголовка. Коли група вже існує, ви керуєте нею та додаєте до неї робочі простори із заголовка групи (див. Керування групою нижче).", "keyboardTitle": "З клавіатури ({shortcut})", - "keyboardDesc": "Виберіть два або більше робочих просторів на бічній панелі, потім натисніть {shortcut}. Новий якірний робочий простір вставляється над виділенням, і всі вибрані робочі простори стають дочірніми. Група автоматично називається Group 1, Group 2 тощо — перейменуйте її будь-коли через контекстне меню заголовка.", + "keyboardDesc": "Виберіть два або більше робочих просторів на бічній панелі, потім натисніть {shortcut}. Вибрані робочі простори переміщуються під новий заголовок групи у стилі папки, а перший вибраний робочий простір стає якорем групи. Додатковий термінальний робочий простір не створюється. Група автоматично називається Group 1, Group 2 тощо — перейменуйте її будь-коли через контекстне меню заголовка.", "keyboardNote": "{shortcut} конфліктує зі значенням за замовчуванням React Grab. Обробник групи споживає комбінацію лише тоді, коли на бічній панелі є явне множинне виділення щонайменше двох робочих просторів, тож React Grab усе одно спрацьовує під час одиночного виділення та в контекстах браузера чи терміналу. Перепризначте її в Settings → Keyboard, якщо ви не хочете, щоб ці дві дії спільно використовували клавішу.", "keyboardSingle": "Групи з однієї вкладки не створюються за допомогою комбінації клавіш. Для цього скористайтеся пунктом контекстного меню робочої області «Нова група з робочої області».", "contextMenuTitle": "З контекстного меню робочого простору", diff --git a/web/messages/zh-CN.json b/web/messages/zh-CN.json index d772d55688bc..cf067bd547aa 100644 --- a/web/messages/zh-CN.json +++ b/web/messages/zh-CN.json @@ -675,30 +675,30 @@ "panelIdInternal": "Panel ID(内部)", "metaTitle": "概念", "workspaceGroups": "工作区分组", - "workspaceGroupsDesc": "将相关的工作区归入侧边栏中可折叠的命名分组。每个分组由一个锚点工作区拥有,并且可以被固定、重命名,以及设置自己的图标和颜色。可以直接从分组的标题栏在分组内创建新的工作区。", + "workspaceGroupsDesc": "将相关工作区归入侧边栏中可折叠的命名分组。每个分组都有一个文件夹样式的标题栏和一个用于排序的锚点工作区,并且可以被固定、重命名,以及设置自己的图标和颜色。可以直接从分组标题栏在分组内创建新的工作区。", "workspaceGroupsLink": "阅读完整的工作区分组指南" }, "workspaceGroups": { "metaTitle": "工作区分组", "metaDescription": "在 cmux 侧边栏中将工作区嵌套到可折叠的命名分组中。锚点模型、如何创建和管理分组,以及 workspace-group CLI。", "title": "工作区分组", - "intro": "工作区分组让你能够将工作区嵌入侧边栏中可折叠的命名分组。每个分组都有一个隐式的锚点工作区、一个用于在分组内创建新工作区的可自定义 + 按钮,以及用于重命名、固定、取消分组和编辑配置的右键操作。", + "intro": "工作区分组让你可以把工作区嵌套到侧边栏中可折叠的命名分组里。每个分组都有一个文件夹样式的标题栏、一个用于排序的隐式锚点工作区、一个可自定义的 + 按钮用于在其中创建新工作区,以及用于重命名、固定、取消分组和编辑配置的右键操作。", "concepts": "核心概念", "anchorTitle": "锚点工作区", - "anchorDesc": "每个分组都恰好由一个称为锚点的工作区拥有。侧边栏中的分组标题栏就是锚点的呈现形式——锚点没有单独的行。点击标题名称区域会聚焦到锚点的面板;点击箭头则切换折叠状态。", - "anchorNew": "创建分组时,锚点始终是全新的。它们永远不会从已有的工作区提升而来。锚点的工作目录:在对选中项进行分组时继承自第一个选中的工作区,在通过 CLI 创建而未指定 --cwd 时则继承自当前活动的工作区。", + "anchorDesc": "每个分组都有且只有一个称为锚点的工作区。锚点是一个已有的成员工作区,用于定位分组并提供参考工作目录。分组标题栏本身是一行文件夹样式的条目;当分组展开时,锚点仍作为普通工作区行显示在标题栏下方。点击折叠箭头可切换折叠状态。", + "anchorNew": "创建分组时,cmux 会将第一个符合条件的已选工作区提升为锚点。它不会为了充当分组标题栏而创建新的终端工作区。", "anchorClose": "关闭锚点工作区会解散该分组:其余每个成员都会变成未分组的工作区并保留在标签页列表中。除了锚点本身,不会关闭其他任何内容。在此操作发生前,应用会显示一个带有“不再询问”开关的确认对话框。", "identityTitle": "分组标识", - "identityDesc": "一个分组拥有名称、图标(一个 SF Symbol,默认为 folder.fill)以及一个可选的自定义颜色。这些与锚点工作区自身的自定义设置相互独立。锚点的颜色和图标在创建时取自分组,但之后两者可以各自变化。", + "identityDesc": "分组有名称、图标(SF Symbol,默认 folder.fill)以及可选的自定义颜色。这些都独立于锚点工作区自身的自定义设置。", "pinningTitle": "固定", "pinningDesc": "分组可以独立于单个工作区的固定状态进行固定。被固定的顶级行,无论是单个工作区还是分组,都会排在未固定的行之上。在每个层级内,分组和工作区会保持你拖动它们时形成的顺序。", "layoutIntro": "侧边栏自上而下排列顶级行:", "layoutItem1": "已固定的顶级行(工作区和分组)。", "layoutItem2": "未固定的顶级行(工作区和分组)。", "creatingTitle": "创建分组", - "creatingIntro": "可以通过两种方式创建分组:键盘快捷键或工作区右键菜单。两者都会在所选项上方插入一个新的锚点工作区,并将所选工作区移入该分组。分组创建后,你可以从分组标题处管理它并向其中添加工作区(参见下方的“管理分组”)。", + "creatingIntro": "可以通过两种方式创建分组:键盘快捷键或工作区上下文菜单。两者都会把已有工作区移到一个文件夹样式的分组标题栏下,并将第一个符合条件的已选工作区提升为排序锚点。两者都不会为标题栏创建新的终端工作区。分组创建后,你可以从分组标题栏管理它并向其中添加工作区(见下方“管理分组”)。", "keyboardTitle": "通过键盘({shortcut})", - "keyboardDesc": "在侧边栏中选中两个或更多工作区,然后按 {shortcut}。一个全新的锚点工作区会插入在选中项的上方,所有选中的工作区都会成为它的子项。分组会被自动命名为 Group 1、Group 2,依此类推——你随时可以通过标题栏右键菜单重命名它。", + "keyboardDesc": "在侧边栏中选中两个或更多工作区,然后按 {shortcut}。选中的工作区会移动到一个新的文件夹样式分组标题栏下方,第一个选中的工作区会成为分组的锚点。不会创建额外的终端工作区。分组会被自动命名为 Group 1、Group 2,依此类推——你随时可以通过标题栏右键菜单重命名它。", "keyboardNote": "{shortcut} 与 React Grab 的默认快捷键冲突。分组处理程序只有在侧边栏中明确多选了至少两个工作区时才会占用该组合键,因此在单选以及浏览器或终端环境中 React Grab 仍会触发。如果你不希望两者共用一个按键,可在 设置 → 键盘 中重新绑定它。", "keyboardSingle": "单标签分组无法通过快捷键创建。请使用工作区右键菜单中的 New Group from Workspace 项来创建。", "contextMenuTitle": "通过工作区右键菜单", diff --git a/web/messages/zh-TW.json b/web/messages/zh-TW.json index bc0ef87b9809..a9bf596cce92 100644 --- a/web/messages/zh-TW.json +++ b/web/messages/zh-TW.json @@ -675,30 +675,30 @@ "panelIdInternal": "面板 ID(內部)", "metaTitle": "概念", "workspaceGroups": "工作區群組", - "workspaceGroupsDesc": "將相關的工作區歸入側邊欄中可折疊的具名區段。每個群組都由一個錨點工作區擁有,並可釘選、重新命名,以及設定專屬的圖示與顏色。新工作區可以直接從群組標頭在群組內建立。", + "workspaceGroupsDesc": "將相關工作區歸入側邊欄中可折疊的具名群組。每個群組都有一個資料夾樣式的標頭,以及一個用於排序的錨點工作區,並可釘選、重新命名,以及設定專屬的圖示與顏色。可以直接從群組標頭在群組內建立新的工作區。", "workspaceGroupsLink": "閱讀完整的工作區群組指南" }, "workspaceGroups": { "metaTitle": "工作區群組", "metaDescription": "在 cmux 側邊欄中將工作區巢狀整理成可折疊的具名區段。錨點模型、如何建立與管理群組,以及 workspace-group CLI。", "title": "工作區群組", - "intro": "工作區群組讓你能將工作區巢狀歸入側邊欄中可折疊的具名區段。每個群組都有一個隱含的錨點工作區、一個可自訂用來在群組內建立新工作區的 + 按鈕,以及用於重新命名、釘選、解除群組和編輯其設定的右鍵動作。", + "intro": "工作區群組可讓你將工作區巢狀放入側邊欄中可折疊的具名區段。每個群組都有資料夾樣式的標頭、用於排序的隱含錨點工作區、可自訂的 + 按鈕用於在其中建立新工作區,以及用於重新命名、釘選、取消群組和編輯設定的右鍵動作。", "concepts": "概念", "anchorTitle": "錨點工作區", - "anchorDesc": "每個群組都恰好由一個稱為錨點的工作區擁有。側邊欄中的群組標頭即是錨點的呈現方式——它沒有獨立的列。點按標頭名稱區域會聚焦於錨點的面板;點按箭號則切換折疊狀態。", - "anchorNew": "建立群組時,錨點一律是全新的。它們絕不會由現有的工作區升格而成。對選取項目進行群組化時,錨點的工作目錄會繼承自第一個選取的工作區;透過 CLI 在未指定 --cwd 的情況下建立時,則繼承自目前作用中的工作區。", + "anchorDesc": "每個群組都有且只有一個稱為錨點的工作區。錨點是既有的成員工作區,用於定位群組並提供參考工作目錄。群組標頭本身是一列資料夾樣式的項目;群組展開時,錨點仍會以一般工作區列顯示在標頭下方。按一下展開箭頭可切換折疊狀態。", + "anchorNew": "建立群組時,cmux 會將第一個符合條件的已選工作區提升為錨點。它不會只為了充當群組標頭而建立新的終端工作區。", "anchorClose": "關閉錨點工作區會解散群組:其他每個成員都會變成未群組的工作區,並保留在分頁列表中。除了錨點本身之外,不會關閉任何項目。在此發生之前,應用程式會顯示一個帶有「不再詢問」切換開關的確認對話框。", "identityTitle": "群組識別", - "identityDesc": "群組具有名稱、圖示(一個 SF Symbol,預設為 folder.fill),以及一個選用的自訂顏色。這些都獨立於錨點工作區本身的自訂設定。錨點的顏色和圖示在建立時會以群組為種子值,但之後可以各自不同。", + "identityDesc": "群組有名稱、圖示(SF Symbol,預設為 folder.fill)以及選用的自訂顏色。這些都獨立於錨點工作區本身的自訂設定。", "pinningTitle": "釘選", "pinningDesc": "群組可以獨立於個別工作區的釘選而釘選。已釘選的頂層列,無論是個別工作區還是群組,都會位於未釘選列的上方。在每個層級之內,群組和工作區會保留你拖曳放入的順序。", "layoutIntro": "側邊欄由上而下排列頂層列:", "layoutItem1": "已釘選的頂層列(工作區與群組)。", "layoutItem2": "未釘選的頂層列(工作區與群組)。", "creatingTitle": "建立群組", - "creatingIntro": "可以透過兩種方式建立群組:鍵盤快速鍵或工作區右鍵選單。兩者都會在所選項目上方插入一個新的錨點工作區,並將所選工作區移入該群組。群組建立後,你可以從群組標題處管理它並向其中新增工作區(請參閱下方的「管理群組」)。", + "creatingIntro": "可以透過兩種方式建立群組:鍵盤快速鍵或工作區內容選單。兩者都會將既有工作區移到資料夾樣式的群組標頭下,並將第一個符合條件的已選工作區提升為排序錨點。兩者都不會為標頭建立新的終端工作區。群組建立後,你可以從群組標頭管理它並新增工作區到其中(見下方「管理群組」)。", "keyboardTitle": "透過鍵盤({shortcut})", - "keyboardDesc": "在側邊欄中選取兩個或更多工作區,然後按下 {shortcut}。一個全新的錨點工作區會插入在所選項目上方,且所有選取的工作區都會成為子項。群組會自動命名為 Group 1、Group 2 等等——你可以隨時透過標頭情境選單重新命名。", + "keyboardDesc": "在側邊欄中選取兩個或更多工作區,然後按下 {shortcut}。選取的工作區會移到新的資料夾樣式群組標頭下方,第一個選取的工作區會成為群組的錨點。不會建立額外的終端工作區。群組會自動命名為 Group 1、Group 2 等等——你可以隨時透過標頭情境選單重新命名。", "keyboardNote": "{shortcut} 與 React Grab 的預設快捷鍵相衝突。群組處理常式只有在側邊欄明確多選至少兩個工作區時才會消耗此和弦,因此在單選以及瀏覽器或終端機情境中,React Grab 仍會觸發。如果你不希望兩者共用同一個按鍵,可以在「設定 → 鍵盤」中重新繫結。", "keyboardSingle": "單一分頁群組無法透過快捷鍵建立。請改用工作區右鍵選單中的 New Group from Workspace 項目來建立。", "contextMenuTitle": "透過工作區情境選單", From 83da1602b5107caf7513427595a1c2b36e95d249 Mon Sep 17 00:00:00 2001 From: leegisang Date: Tue, 16 Jun 2026 03:32:51 +0900 Subject: [PATCH 3/7] Address workspace group review checks --- .../MobileWorkspaceGroupPreview.swift | 2 +- .../WorkspaceCoordinatorTests.swift | 4 +- .../App/TerminalDirectoryOpenSupport.swift | 64 ------------------- Sources/AppDelegate.swift | 10 ++- Sources/TabManager+WorkspaceShortcuts.swift | 25 ++++++++ Sources/WorkspaceShortcutMapper.swift | 41 ++++++++++++ cmux.xcodeproj/project.pbxproj | 8 +++ 7 files changed, 85 insertions(+), 69 deletions(-) create mode 100644 Sources/TabManager+WorkspaceShortcuts.swift create mode 100644 Sources/WorkspaceShortcutMapper.swift diff --git a/Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceGroupPreview.swift b/Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceGroupPreview.swift index a99df9973f71..f8fa83bb60cc 100644 --- a/Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceGroupPreview.swift +++ b/Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceGroupPreview.swift @@ -45,7 +45,7 @@ public struct MobileWorkspaceGroupPreview: Identifiable, Equatable, Sendable { /// - name: The group's user-facing name. /// - isCollapsed: Whether the group is collapsed. Defaults to `false`. /// - isPinned: Whether the group is pinned. Defaults to `false`. - /// - anchorWorkspaceID: The anchor workspace that owns the group. + /// - anchorWorkspaceID: The anchor workspace that positions the group. public init( id: ID, name: String, diff --git a/Packages/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceCoordinatorTests.swift b/Packages/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceCoordinatorTests.swift index 24cb7a3c4f05..566532d37172 100644 --- a/Packages/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceCoordinatorTests.swift +++ b/Packages/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceCoordinatorTests.swift @@ -259,12 +259,12 @@ struct WorkspaceCoordinatorTests { } @Test - func collapseToggleMovesFocusToAnchorAndStripsHiddenSelection() { + func collapseToggleMovesFocusToAnchorAndStripsHiddenSelection() throws { let (model, host, groups, _) = makeWorld() let a = CoordinatorStubTab() let b = CoordinatorStubTab() model.tabs = [a, b] - let groupId = try! #require(groups.createWorkspaceGroup(name: "G", childWorkspaceIds: [a.id, b.id])) + let groupId = try #require(groups.createWorkspaceGroup(name: "G", childWorkspaceIds: [a.id, b.id])) let anchorId = model.workspaceGroups[0].anchorWorkspaceId model.selectedTabId = b.id host.sidebarSelectedWorkspaceIds = [b.id] diff --git a/Sources/App/TerminalDirectoryOpenSupport.swift b/Sources/App/TerminalDirectoryOpenSupport.swift index bf4115bc2669..73c94983f08c 100644 --- a/Sources/App/TerminalDirectoryOpenSupport.swift +++ b/Sources/App/TerminalDirectoryOpenSupport.swift @@ -913,70 +913,6 @@ final class ServeWebOutputCollector { } } -enum WorkspaceShortcutMapper { - /// Maps numbered workspace shortcuts to a zero-based workspace index. - /// 1...8 target fixed indices; 9 always targets the last workspace. - static func workspaceIndex(forDigit digit: Int, workspaceCount: Int) -> Int? { - guard workspaceCount > 0 else { return nil } - guard (1...9).contains(digit) else { return nil } - - if digit == 9 { - return workspaceCount - 1 - } - - let index = digit - 1 - return index < workspaceCount ? index : nil - } - - /// Returns the primary digit badge to display for a workspace row. - /// Picks the lowest digit that maps to that row index. - static func digitForWorkspace(at index: Int, workspaceCount: Int) -> Int? { - guard index >= 0 && index < workspaceCount else { return nil } - for digit in 1...9 { - if workspaceIndex(forDigit: digit, workspaceCount: workspaceCount) == index { - return digit - } - } - return nil - } - - static func workspaceId(forDigit digit: Int, workspaceIds: [UUID]) -> UUID? { - guard let index = workspaceIndex(forDigit: digit, workspaceCount: workspaceIds.count) else { - return nil - } - return workspaceIds[index] - } - - static func digitForWorkspace(id workspaceId: UUID, workspaceIds: [UUID]) -> Int? { - guard let index = workspaceIds.firstIndex(of: workspaceId) else { return nil } - return digitForWorkspace(at: index, workspaceCount: workspaceIds.count) - } -} - -@MainActor -extension TabManager { - var numberedWorkspaceShortcutWorkspaceIds: [UUID] { - let groupsById = Dictionary(uniqueKeysWithValues: workspaceGroups.map { ($0.id, $0) }) - return SidebarWorkspaceRenderItem.numberedShortcutWorkspaceIds( - tabs: tabs, - groupsById: groupsById - ) - } - - @discardableResult - func selectWorkspaceByShortcutDigit(_ digit: Int) -> Bool { - guard let workspaceId = WorkspaceShortcutMapper.workspaceId( - forDigit: digit, - workspaceIds: numberedWorkspaceShortcutWorkspaceIds - ), - let workspace = tabs.first(where: { $0.id == workspaceId }) else { - return false - } - selectWorkspace(workspace) - return true - } -} - extension CommandPaletteContextKeys { /// Typed app-side overload over the package's raw-value key builder, so /// palette context keys keep the exact `terminal.openTarget..available` diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index e367700382fd..d3f546bd52ec 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -9123,7 +9123,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent } #endif cleanupObservers() - NSLog("Command send: surface not ready after 3.0s") +#if DEBUG + cmuxDebugLog("command.send.surfaceNotReady timeout=3.0") +#endif onFailure?() } } @@ -15460,9 +15462,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent "durationMs": durationMs ]) +#if DEBUG if registerStatus != noErr { - NSLog("LaunchServices registration failed (status: \(registerStatus)) for \(normalizedURL.path)") + cmuxDebugLog( + "launchservices.register.failed status=\(registerStatus) path=\(normalizedURL.path)" + ) } +#endif } } diff --git a/Sources/TabManager+WorkspaceShortcuts.swift b/Sources/TabManager+WorkspaceShortcuts.swift new file mode 100644 index 000000000000..48faaca83216 --- /dev/null +++ b/Sources/TabManager+WorkspaceShortcuts.swift @@ -0,0 +1,25 @@ +import Foundation + +@MainActor +extension TabManager { + var numberedWorkspaceShortcutWorkspaceIds: [UUID] { + let groupsById = Dictionary(uniqueKeysWithValues: workspaceGroups.map { ($0.id, $0) }) + return SidebarWorkspaceRenderItem.numberedShortcutWorkspaceIds( + tabs: tabs, + groupsById: groupsById + ) + } + + @discardableResult + func selectWorkspaceByShortcutDigit(_ digit: Int) -> Bool { + guard let workspaceId = WorkspaceShortcutMapper.workspaceId( + forDigit: digit, + workspaceIds: numberedWorkspaceShortcutWorkspaceIds + ), + let workspace = tabs.first(where: { $0.id == workspaceId }) else { + return false + } + selectWorkspace(workspace) + return true + } +} diff --git a/Sources/WorkspaceShortcutMapper.swift b/Sources/WorkspaceShortcutMapper.swift new file mode 100644 index 000000000000..60bb62c06755 --- /dev/null +++ b/Sources/WorkspaceShortcutMapper.swift @@ -0,0 +1,41 @@ +import Foundation + +enum WorkspaceShortcutMapper { + /// Maps numbered workspace shortcuts to a zero-based workspace index. + /// 1...8 target fixed indices; 9 always targets the last workspace. + static func workspaceIndex(forDigit digit: Int, workspaceCount: Int) -> Int? { + guard workspaceCount > 0 else { return nil } + guard (1...9).contains(digit) else { return nil } + + if digit == 9 { + return workspaceCount - 1 + } + + let index = digit - 1 + return index < workspaceCount ? index : nil + } + + /// Returns the primary digit badge to display for a workspace row. + /// Picks the lowest digit that maps to that row index. + static func digitForWorkspace(at index: Int, workspaceCount: Int) -> Int? { + guard index >= 0 && index < workspaceCount else { return nil } + for digit in 1...9 { + if workspaceIndex(forDigit: digit, workspaceCount: workspaceCount) == index { + return digit + } + } + return nil + } + + static func workspaceId(forDigit digit: Int, workspaceIds: [UUID]) -> UUID? { + guard let index = workspaceIndex(forDigit: digit, workspaceCount: workspaceIds.count) else { + return nil + } + return workspaceIds[index] + } + + static func digitForWorkspace(id workspaceId: UUID, workspaceIds: [UUID]) -> Int? { + guard let index = workspaceIds.firstIndex(of: workspaceId) else { return nil } + return digitForWorkspace(at: index, workspaceCount: workspaceIds.count) + } +} diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index e6bcb1aa93c1..dfae0dfc7fbe 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -712,6 +712,7 @@ E3B7A4000000000000000009 /* TabManager+NotificationDismissalHosting.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3B7A400000000000000000A /* TabManager+NotificationDismissalHosting.swift */; }; C5B6A10000000000000000B1 /* TabManager+SidebarGitHosting.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B6A10000000000000000B2 /* TabManager+SidebarGitHosting.swift */; }; 604500100000000000000007 /* TabManager+WindowTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604500100000000000000008 /* TabManager+WindowTitle.swift */; }; + C9A57601C9A57601C9A57601 /* TabManager+WorkspaceShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A57602C9A57602C9A57602 /* TabManager+WorkspaceShortcuts.swift */; }; A5001003 /* TabManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001013 /* TabManager.swift */; }; 2BB56A710BB1FC50367E5BCF /* TabManagerSessionSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10D684CFFB8CDEF89CE2D9E1 /* TabManagerSessionSnapshotTests.swift */; }; B6BF3DC98DB1495E57900199 /* TabManagerUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42092CDB2109E250F7F2A76E /* TabManagerUnitTests.swift */; }; @@ -873,6 +874,7 @@ EE30D6000000000000000004 /* WorkspaceRemoteSessionBuildInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE30D6000000000000000003 /* WorkspaceRemoteSessionBuildInfo.swift */; }; EE30D6000000000000000002 /* WorkspaceRemoteSessionHostAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE30D6000000000000000001 /* WorkspaceRemoteSessionHostAdapter.swift */; }; 619D509BC9B1EA946CBD6A8A /* WorkspaceRuntimeSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9960CC3992F3C44489E7627C /* WorkspaceRuntimeSettings.swift */; }; + C9A57603C9A57603C9A57603 /* WorkspaceShortcutMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A57604C9A57604C9A57604 /* WorkspaceShortcutMapper.swift */; }; 5659A0015659A0015659A001 /* WorkspaceSidebarObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659A0025659A0025659A002 /* WorkspaceSidebarObservation.swift */; }; 5659A0035659A0035659A003 /* WorkspaceSidebarObservationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659A0045659A0045659A004 /* WorkspaceSidebarObservationTests.swift */; }; F0F0A001A1B2C3D4E5F60718 /* WorkspaceSidebarScrollUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F0A002A1B2C3D4E5F60718 /* WorkspaceSidebarScrollUITests.swift */; }; @@ -1579,6 +1581,7 @@ E3B7A400000000000000000A /* TabManager+NotificationDismissalHosting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabManager+NotificationDismissalHosting.swift"; sourceTree = ""; }; C5B6A10000000000000000B2 /* TabManager+SidebarGitHosting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabManager+SidebarGitHosting.swift"; sourceTree = ""; }; 604500100000000000000008 /* TabManager+WindowTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabManager+WindowTitle.swift"; sourceTree = ""; }; + C9A57602C9A57602C9A57602 /* TabManager+WorkspaceShortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabManager+WorkspaceShortcuts.swift"; sourceTree = ""; }; A5001013 /* TabManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManager.swift; sourceTree = ""; }; 10D684CFFB8CDEF89CE2D9E1 /* TabManagerSessionSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManagerSessionSnapshotTests.swift; sourceTree = ""; }; 42092CDB2109E250F7F2A76E /* TabManagerUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManagerUnitTests.swift; sourceTree = ""; }; @@ -1740,6 +1743,7 @@ EE30D6000000000000000003 /* WorkspaceRemoteSessionBuildInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceRemoteSessionBuildInfo.swift; sourceTree = ""; }; EE30D6000000000000000001 /* WorkspaceRemoteSessionHostAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceRemoteSessionHostAdapter.swift; sourceTree = ""; }; 9960CC3992F3C44489E7627C /* WorkspaceRuntimeSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/WorkspaceRuntimeSettings.swift; sourceTree = ""; }; + C9A57604C9A57604C9A57604 /* WorkspaceShortcutMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceShortcutMapper.swift; sourceTree = ""; }; 5659A0025659A0025659A002 /* WorkspaceSidebarObservation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceSidebarObservation.swift; sourceTree = ""; }; 5659A0045659A0045659A004 /* WorkspaceSidebarObservationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceSidebarObservationTests.swift; sourceTree = ""; }; F0F0A002A1B2C3D4E5F60718 /* WorkspaceSidebarScrollUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceSidebarScrollUITests.swift; sourceTree = ""; }; @@ -2108,6 +2112,7 @@ B42A82C6AA614E74873D9A5F /* ShortcutRoutingSupport.swift */, 81B8CFAF8FCA4231C702D16A /* WindowKeyDownReplayGuard.swift */, 604500200000000000000002 /* WindowTitleTemplateContext.swift */, + C9A57604C9A57604C9A57604 /* WorkspaceShortcutMapper.swift */, 604500100000000000000002 /* WindowTitleTemplate.swift */, AB7F2E9143904957AAA70726 /* ShortcutBareStartRouting.swift */, E5C0F1A1E5C0F1A1E5C0F1A1 /* TerminalFindEscapeRouting.swift */, @@ -2154,6 +2159,7 @@ C5CC15CEAA80B779ADEE78AA /* WorkspaceActionDispatcher.swift */, D7AB00000000000000000014 /* TabManager+DetachedWorkspace.swift */, 604500100000000000000008 /* TabManager+WindowTitle.swift */, + C9A57602C9A57602C9A57602 /* TabManager+WorkspaceShortcuts.swift */, E30750000000000000000001 /* WorkspaceTabColorResolution.swift */, E3B7A4000000000000000002 /* WorkspaceTabColorSettings.swift */, E3B7A4000000000000000004 /* WorkspaceTabColorEntry.swift */, @@ -3586,6 +3592,7 @@ E3B7A4000000000000000009 /* TabManager+NotificationDismissalHosting.swift in Sources */, C5B6A10000000000000000B1 /* TabManager+SidebarGitHosting.swift in Sources */, 604500100000000000000007 /* TabManager+WindowTitle.swift in Sources */, + C9A57601C9A57601C9A57601 /* TabManager+WorkspaceShortcuts.swift in Sources */, A5001003 /* TabManager.swift in Sources */, C7A503000000000000000002 /* TaskManagerSnapshot.swift in Sources */, C7A504000000000000000002 /* TaskManagerTypes.swift in Sources */, @@ -3706,6 +3713,7 @@ EE30D6000000000000000004 /* WorkspaceRemoteSessionBuildInfo.swift in Sources */, EE30D6000000000000000002 /* WorkspaceRemoteSessionHostAdapter.swift in Sources */, 619D509BC9B1EA946CBD6A8A /* WorkspaceRuntimeSettings.swift in Sources */, + C9A57603C9A57603C9A57603 /* WorkspaceShortcutMapper.swift in Sources */, 5659A0015659A0015659A001 /* WorkspaceSidebarObservation.swift in Sources */, D7AB34400000000000000001 /* WorkspaceSurfaceConfig.swift in Sources */, C0DE32470000000000000003 /* WorkspaceSurfaceIdentifierClipboardText.swift in Sources */, From 355449b8b4879c3c3d7582b2c8162b80e78e83a1 Mon Sep 17 00:00:00 2001 From: leegisang Date: Tue, 16 Jun 2026 03:39:58 +0900 Subject: [PATCH 4/7] Address workspace group unread review --- Sources/ContentView.swift | 14 +++++++++++++- Sources/VerticalTabsSidebar+WorkspaceGroups.swift | 9 +++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 7821eedc2b24..602935f676e8 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -10611,6 +10611,7 @@ struct VerticalTabsSidebar: View { let workspaceRenderItems: [SidebarWorkspaceRenderItem] let visibleWorkspaceRowIds: [UUID] let numberedWorkspaceShortcutIds: [UUID] + let workspaceGroupUnreadCountById: [UUID: Int] var workspaceIds: [UUID] { tabIds } } @@ -10645,6 +10646,16 @@ struct VerticalTabsSidebar: View { tabs: tabs, groupsById: workspaceGroupById ) + let workspaceGroupUnreadCountById = workspaceRenderItems.reduce(into: [UUID: Int]()) { counts, item in + guard case .groupHeader(let group, let memberWorkspaceIds) = item else { return } + guard group.isCollapsed else { + counts[group.id] = 0 + return + } + counts[group.id] = memberWorkspaceIds.reduce(0) { partial, workspaceId in + partial + sidebarUnread.unreadCount(forWorkspaceId: workspaceId) + } + } let visibleWorkspaceRowIds = workspaceRenderItems.map(\.rowWorkspaceId) let numberedWorkspaceShortcutIds = workspaceRenderItems.compactMap(\.numberedShortcutWorkspaceId) let draggedSidebarTabId = dragState.draggedTabId @@ -10674,7 +10685,8 @@ struct VerticalTabsSidebar: View { workspaceGroupMenuSnapshot: workspaceGroupMenuSnapshot, workspaceRenderItems: workspaceRenderItems, visibleWorkspaceRowIds: visibleWorkspaceRowIds, - numberedWorkspaceShortcutIds: numberedWorkspaceShortcutIds + numberedWorkspaceShortcutIds: numberedWorkspaceShortcutIds, + workspaceGroupUnreadCountById: workspaceGroupUnreadCountById ) ZStack(alignment: .bottomLeading) { diff --git a/Sources/VerticalTabsSidebar+WorkspaceGroups.swift b/Sources/VerticalTabsSidebar+WorkspaceGroups.swift index 4f8c57fc3991..1ab7010381ac 100644 --- a/Sources/VerticalTabsSidebar+WorkspaceGroups.swift +++ b/Sources/VerticalTabsSidebar+WorkspaceGroups.swift @@ -24,12 +24,9 @@ extension VerticalTabsSidebar { ) let cwdContextMenuItems = resolvedConfig?.contextMenuItems ?? [] let newWorkspacePlacement = resolvedConfig?.newWorkspacePlacement - let anchorUnreadCount: Int = { - guard group.isCollapsed else { return 0 } - return memberWorkspaceIds.reduce(0) { partial, workspaceId in - partial + notificationStore.unreadCount(forTabId: workspaceId) - } - }() + let anchorUnreadCount = group.isCollapsed + ? renderContext.workspaceGroupUnreadCountById[group.id, default: 0] + : 0 let showsHintForAnchor = false let topDropIndicatorVisible = SidebarTabDropIndicatorPredicate().topVisible( forTabId: group.anchorWorkspaceId, From 5228be06ee5210ff8152d3d0446fe723c09571dd Mon Sep 17 00:00:00 2001 From: leegisang Date: Tue, 16 Jun 2026 03:52:49 +0900 Subject: [PATCH 5/7] Precompute workspace shortcut digits --- Sources/ContentView.swift | 19 +++++++++++++------ Sources/WorkspaceShortcutMapper.swift | 4 ---- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 602935f676e8..20e1dbae0ab0 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -10610,7 +10610,7 @@ struct VerticalTabsSidebar: View { let workspaceGroupMenuSnapshot: WorkspaceGroupMenuSnapshot let workspaceRenderItems: [SidebarWorkspaceRenderItem] let visibleWorkspaceRowIds: [UUID] - let numberedWorkspaceShortcutIds: [UUID] + let workspaceShortcutDigitById: [UUID: Int] let workspaceGroupUnreadCountById: [UUID: Int] var workspaceIds: [UUID] { tabIds } @@ -10658,6 +10658,16 @@ struct VerticalTabsSidebar: View { } let visibleWorkspaceRowIds = workspaceRenderItems.map(\.rowWorkspaceId) let numberedWorkspaceShortcutIds = workspaceRenderItems.compactMap(\.numberedShortcutWorkspaceId) + let workspaceShortcutDigitById = (1...9).reduce(into: [UUID: Int]()) { digits, digit in + guard let workspaceId = WorkspaceShortcutMapper.workspaceId( + forDigit: digit, + workspaceIds: numberedWorkspaceShortcutIds + ), + digits[workspaceId] == nil else { + return + } + digits[workspaceId] = digit + } let draggedSidebarTabId = dragState.draggedTabId let sidebarReorderIds = draggedSidebarTabId.map { tabManager.sidebarReorderWorkspaceIds( @@ -10685,7 +10695,7 @@ struct VerticalTabsSidebar: View { workspaceGroupMenuSnapshot: workspaceGroupMenuSnapshot, workspaceRenderItems: workspaceRenderItems, visibleWorkspaceRowIds: visibleWorkspaceRowIds, - numberedWorkspaceShortcutIds: numberedWorkspaceShortcutIds, + workspaceShortcutDigitById: workspaceShortcutDigitById, workspaceGroupUnreadCountById: workspaceGroupUnreadCountById ) @@ -12332,10 +12342,7 @@ struct VerticalTabsSidebar: View { tab: tab, index: index, isActive: tabManager.selectedTabId == tab.id, - workspaceShortcutDigit: WorkspaceShortcutMapper.digitForWorkspace( - id: tab.id, - workspaceIds: renderContext.numberedWorkspaceShortcutIds - ), + workspaceShortcutDigit: renderContext.workspaceShortcutDigitById[tab.id], workspaceShortcutModifierSymbol: renderContext.workspaceNumberShortcut.numberedDigitHintPrefix, canCloseWorkspace: renderContext.canCloseWorkspace, accessibilityWorkspaceCount: renderContext.workspaceCount, diff --git a/Sources/WorkspaceShortcutMapper.swift b/Sources/WorkspaceShortcutMapper.swift index 60bb62c06755..af63373d9aba 100644 --- a/Sources/WorkspaceShortcutMapper.swift +++ b/Sources/WorkspaceShortcutMapper.swift @@ -34,8 +34,4 @@ enum WorkspaceShortcutMapper { return workspaceIds[index] } - static func digitForWorkspace(id workspaceId: UUID, workspaceIds: [UUID]) -> Int? { - guard let index = workspaceIds.firstIndex(of: workspaceId) else { return nil } - return digitForWorkspace(at: index, workspaceCount: workspaceIds.count) - } } From 706b5e7281cb0c29ae6f616d806cb5e85b58685e Mon Sep 17 00:00:00 2001 From: leegisang Date: Tue, 16 Jun 2026 03:54:02 +0900 Subject: [PATCH 6/7] Clean up workspace group test requires --- .../WorkspaceCoordinatorTests.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Packages/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceCoordinatorTests.swift b/Packages/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceCoordinatorTests.swift index 566532d37172..08e38846cf06 100644 --- a/Packages/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceCoordinatorTests.swift +++ b/Packages/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceCoordinatorTests.swift @@ -184,7 +184,7 @@ struct WorkspaceCoordinatorTests { // MARK: Groups @Test - func createWorkspaceGroupAdoptsChildrenAndKeepsSectionContiguous() { + func createWorkspaceGroupAdoptsChildrenAndKeepsSectionContiguous() throws { let (model, host, groups, _) = makeWorld() let child1 = CoordinatorStubTab() let other = CoordinatorStubTab() @@ -196,7 +196,7 @@ struct WorkspaceCoordinatorTests { childWorkspaceIds: [child1.id, child2.id] ) - let group = try! #require(model.workspaceGroups.first(where: { $0.id == groupId })) + let group = try #require(model.workspaceGroups.first(where: { $0.id == groupId })) #expect(group.name == "Group 1") let anchorId = group.anchorWorkspaceId #expect(anchorId == child1.id) @@ -223,12 +223,12 @@ struct WorkspaceCoordinatorTests { } @Test - func deleteWorkspaceGroupClosesMembersAndClearsLastHoldout() { + func deleteWorkspaceGroupClosesMembersAndClearsLastHoldout() throws { let (model, host, groups, _) = makeWorld() let a = CoordinatorStubTab() let b = CoordinatorStubTab() model.tabs = [a, b] - let groupId = try! #require(groups.createWorkspaceGroup(name: "G", childWorkspaceIds: [a.id, b.id])) + let groupId = try #require(groups.createWorkspaceGroup(name: "G", childWorkspaceIds: [a.id, b.id])) let closed = groups.deleteWorkspaceGroup(groupId: groupId) @@ -243,12 +243,12 @@ struct WorkspaceCoordinatorTests { } @Test - func ungroupKeepsMemberPositionsAndDropsMembership() { + func ungroupKeepsMemberPositionsAndDropsMembership() throws { let (model, host, groups, _) = makeWorld() _ = host let a = CoordinatorStubTab() model.tabs = [a] - let groupId = try! #require(groups.createWorkspaceGroup(name: "G", childWorkspaceIds: [a.id])) + let groupId = try #require(groups.createWorkspaceGroup(name: "G", childWorkspaceIds: [a.id])) let orderBefore = model.tabs.map(\.id) groups.ungroupWorkspaceGroup(groupId: groupId) @@ -297,13 +297,13 @@ struct WorkspaceCoordinatorTests { } @Test - func setWorkspaceGroupAnchorHoistsNewAnchorToSectionFront() { + func setWorkspaceGroupAnchorHoistsNewAnchorToSectionFront() throws { let (model, host, groups, _) = makeWorld() _ = host let a = CoordinatorStubTab() let b = CoordinatorStubTab() model.tabs = [a, b] - let groupId = try! #require(groups.createWorkspaceGroup(name: "G", childWorkspaceIds: [a.id, b.id])) + let groupId = try #require(groups.createWorkspaceGroup(name: "G", childWorkspaceIds: [a.id, b.id])) groups.setWorkspaceGroupAnchor(groupId: groupId, workspaceId: b.id) From d1566364dd9ecf6ece90e91580c745c844b54ec9 Mon Sep 17 00:00:00 2001 From: leegisang Date: Tue, 16 Jun 2026 04:05:31 +0900 Subject: [PATCH 7/7] Restore workspace shortcut digit helper --- Sources/WorkspaceShortcutMapper.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/WorkspaceShortcutMapper.swift b/Sources/WorkspaceShortcutMapper.swift index af63373d9aba..60bb62c06755 100644 --- a/Sources/WorkspaceShortcutMapper.swift +++ b/Sources/WorkspaceShortcutMapper.swift @@ -34,4 +34,8 @@ enum WorkspaceShortcutMapper { return workspaceIds[index] } + static func digitForWorkspace(id workspaceId: UUID, workspaceIds: [UUID]) -> Int? { + guard let index = workspaceIds.firstIndex(of: workspaceId) else { return nil } + return digitForWorkspace(at: index, workspaceCount: workspaceIds.count) + } }