@brimveyn/aimux 1.21.0 → 1.22.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -25
- package/package.json +2 -2
- package/src/app-runtime/auto-commit-driver.ts +22 -27
- package/src/app-runtime/backend-attach-runtime.ts +43 -36
- package/src/app-runtime/backend-runtime-events.ts +107 -88
- package/src/app-runtime/editor-actions.ts +201 -0
- package/src/app-runtime/git-actions.ts +197 -0
- package/src/app-runtime/measure-ref.ts +19 -0
- package/src/app-runtime/navigation-actions.ts +213 -0
- package/src/app-runtime/project-actions.ts +188 -0
- package/src/app-runtime/prompt-injection.ts +123 -0
- package/src/app-runtime/selection.ts +29 -0
- package/src/app-runtime/settings-actions.ts +156 -0
- package/src/app-runtime/setup-actions.ts +237 -0
- package/src/app-runtime/side-effect-context.ts +33 -0
- package/src/app-runtime/side-effects.ts +302 -1541
- package/src/app-runtime/snippet-actions.ts +3 -2
- package/src/app-runtime/tab-actions.ts +218 -0
- package/src/app-runtime/tab-runtime-timeouts.ts +1 -1
- package/src/app-runtime/use-auto-commit-driver.ts +14 -13
- package/src/app-runtime/use-backend-runtime.ts +12 -11
- package/src/app-runtime/use-directory-search.ts +5 -4
- package/src/app-runtime/use-mouse-handlers.ts +2 -1
- package/src/app-runtime/{use-workspace-autosave.ts → use-project-autosave.ts} +4 -4
- package/src/app-runtime/use-renderer-bindings.ts +2 -1
- package/src/app-runtime/use-setup-runner.ts +96 -0
- package/src/app-runtime/use-terminal-resize.ts +18 -14
- package/src/app-runtime/workspace-actions.ts +366 -0
- package/src/app-runtime/workspace-activity.ts +138 -0
- package/src/app-runtime/workspace-naming.ts +90 -0
- package/src/app.tsx +93 -48
- package/src/assets/sounds/bell.wav +0 -0
- package/src/assets/sounds/ding.wav +0 -0
- package/src/assets/sounds/plane.wav +0 -0
- package/src/cli/client/daemon-client.ts +2 -2
- package/src/cli/client/project-resolver.ts +112 -0
- package/src/cli/commands/project/close.ts +32 -0
- package/src/cli/commands/project/create.ts +93 -0
- package/src/cli/commands/project/list.ts +25 -0
- package/src/cli/commands/project/show.ts +32 -0
- package/src/cli/commands/{workspace → project}/switch.ts +19 -19
- package/src/cli/commands/tab/await.ts +2 -2
- package/src/cli/commands/tab/close.ts +3 -3
- package/src/cli/commands/tab/create.ts +74 -73
- package/src/cli/commands/tab/focus.ts +3 -3
- package/src/cli/commands/tab/list.ts +6 -6
- package/src/cli/commands/tab/run.ts +2 -2
- package/src/cli/commands/tab/send.ts +2 -2
- package/src/cli/commands/tab/snapshot.ts +2 -2
- package/src/cli/commands/tab/tail.ts +2 -2
- package/src/cli/commands/tab/wait.ts +2 -2
- package/src/cli/commands/worker/await.ts +3 -3
- package/src/cli/commands/worker/doctor.ts +30 -30
- package/src/cli/commands/worker/list.ts +15 -15
- package/src/cli/commands/worker/prompt.ts +3 -3
- package/src/cli/commands/worker/run.ts +23 -23
- package/src/cli/commands/worker/shared.ts +53 -59
- package/src/cli/commands/worker/stop.ts +29 -29
- package/src/cli/commands/worker/submit.ts +3 -3
- package/src/cli/commands/{worktree → workspace}/create-core.ts +26 -26
- package/src/cli/commands/workspace/create.ts +30 -68
- package/src/cli/commands/workspace/list.ts +52 -9
- package/src/cli/commands/workspace/remove.ts +81 -0
- package/src/cli/completion/plan.ts +2 -2
- package/src/cli/completion/sources.ts +18 -18
- package/src/cli/context.ts +13 -13
- package/src/cli/flags.ts +30 -10
- package/src/cli/index.ts +26 -16
- package/src/cli/output.ts +1 -1
- package/src/cli/registry.ts +12 -12
- package/src/config/loader.ts +16 -5
- package/src/config.ts +70 -112
- package/src/daemon/catalog-writer.ts +67 -67
- package/src/daemon/daemon.ts +205 -201
- package/src/daemon/session-manager.ts +39 -39
- package/src/daemon/session-registry.ts +14 -14
- package/src/git/divergence.ts +33 -2
- package/src/git/git-poller.ts +1 -1
- package/src/git/git-status.ts +2 -2
- package/src/git/{move-worktree.ts → move-workspace.ts} +5 -5
- package/src/git/pr-status.ts +1 -1
- package/src/git/repo-discovery.ts +1 -1
- package/src/git/use-repo-discovery.ts +1 -1
- package/src/git/workspace-branch-poller.ts +54 -0
- package/src/git/workspace-divergence-poller.ts +65 -0
- package/src/git/worktree.ts +29 -0
- package/src/index.tsx +4 -4
- package/src/input/keymap/help-entries.ts +7 -5
- package/src/input/modes/bridge.ts +13 -12
- package/src/input/modes/handlers/shared.ts +1 -1
- package/src/input/modes/transitions.ts +34 -21
- package/src/input/modes/types.ts +48 -31
- package/src/ipc/manager-protocol.ts +44 -44
- package/src/ipc/protocol.ts +192 -166
- package/src/platform/play-sound.ts +184 -0
- package/src/platform/project-search.ts +8 -8
- package/src/platform/worktree-paths.ts +8 -6
- package/src/pty/assistant-question-extractor.ts +1 -1
- package/src/pty/assistant-status-detection-loop.ts +79 -55
- package/src/pty/assistant-status-detector.ts +40 -15
- package/src/pty/command-registry.ts +81 -1
- package/src/restart-terminal-manager.ts +1 -1
- package/src/services/ai-usage/provider.ts +9 -2
- package/src/services/ai-usage/spawn.ts +19 -6
- package/src/session-backend/bootstrap.ts +4 -4
- package/src/session-backend/local-session-backend.ts +78 -75
- package/src/session-backend/remote-session-backend.ts +64 -52
- package/src/session-backend/types.ts +43 -32
- package/src/settings/live.ts +90 -0
- package/src/settings/search.ts +43 -0
- package/src/settings/sections/about.ts +49 -0
- package/src/settings/sections/appearance.ts +54 -0
- package/src/settings/sections/automation.ts +105 -0
- package/src/settings/sections/commands.ts +72 -0
- package/src/settings/sections/editor.ts +56 -0
- package/src/settings/sections/experimental.ts +57 -0
- package/src/settings/sections/git.ts +112 -0
- package/src/settings/sections/index.ts +95 -0
- package/src/settings/sections/integrations.ts +23 -0
- package/src/settings/sections/layout.ts +72 -0
- package/src/settings/sections/notifications.ts +99 -0
- package/src/settings/sections/setup.ts +54 -0
- package/src/settings/sections/status-bar.ts +63 -0
- package/src/settings/settings-store.ts +207 -0
- package/src/settings/types.ts +108 -0
- package/src/snippets/run-shell-var.ts +48 -22
- package/src/state/actions.ts +342 -0
- package/src/state/app-store.ts +2 -1
- package/src/state/bars.ts +1 -1
- package/src/state/dispatch-ref.ts +1 -1
- package/src/state/layout-resize.ts +2 -2
- package/src/state/project-catalog.ts +275 -0
- package/src/state/project-data.ts +143 -0
- package/src/state/{session-persistence.ts → project-persistence.ts} +74 -68
- package/src/state/project-save.ts +46 -0
- package/src/state/project-workspaces.ts +392 -0
- package/src/state/reducers/auto-commit-state.ts +11 -10
- package/src/state/reducers/git-commit-modal-state.ts +122 -0
- package/src/state/reducers/git-mode-state.ts +1 -1
- package/src/state/reducers/git-panel-state.ts +10 -6
- package/src/state/reducers/modal-state.ts +290 -681
- package/src/state/reducers/multi-repo-state.ts +3 -6
- package/src/state/reducers/project-state.ts +312 -0
- package/src/state/reducers/settings-state.ts +80 -0
- package/src/state/reducers/tab-state.ts +107 -60
- package/src/state/reducers/ui-state.ts +4 -3
- package/src/state/selectors.ts +49 -37
- package/src/state/store.ts +57 -24
- package/src/state/types.ts +244 -405
- package/src/state/validation.ts +14 -12
- package/src/terminal-manager/manager-client.ts +32 -32
- package/src/terminal-manager/terminal-manager.ts +24 -24
- package/src/ui/components/git/git-panel.tsx +2 -2
- package/src/ui/components/git/git-view.tsx +12 -12
- package/src/ui/components/git/image-diff/image-diff-view.tsx +1 -1
- package/src/ui/components/git/pane/git-pane-widget.tsx +7 -7
- package/src/ui/components/layout/sidebar/project-list.tsx +444 -0
- package/src/ui/components/layout/sidebar/tab-item.tsx +27 -12
- package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +1 -1
- package/src/ui/components/layout/sidebar/workspace-row.tsx +253 -0
- package/src/ui/components/layout/status-bar.tsx +6 -2
- package/src/ui/components/layout/terminal-pane.tsx +34 -29
- package/src/ui/components/layout/top-tab-bar.tsx +46 -36
- package/src/ui/components/modals/git/git-commit-modal.tsx +8 -8
- package/src/ui/components/modals/{sessions/create-session-modal.tsx → projects/create-project-modal.tsx} +12 -12
- package/src/ui/components/modals/{sessions/session-name-modal.tsx → projects/project-name-modal.tsx} +2 -2
- package/src/ui/components/modals/{sessions/session-picker-modal.tsx → projects/project-picker-modal.tsx} +33 -35
- package/src/ui/components/modals/settings/settings-search-modal.tsx +70 -0
- package/src/ui/components/modals/shared/form.tsx +34 -22
- package/src/ui/components/modals/shared/picker.tsx +10 -10
- package/src/ui/components/modals/shared/{worktree-delete-confirm.tsx → workspace-delete-confirm.tsx} +9 -9
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +30 -324
- package/src/ui/components/modals/workspace/create-workspace-modal.tsx +101 -0
- package/src/ui/components/modals/{worktree/worktree-move-confirm-modal.tsx → workspace/workspace-move-confirm-modal.tsx} +5 -5
- package/src/ui/components/modals/{worktree/worktree-move-modal.tsx → workspace/workspace-move-modal.tsx} +29 -27
- package/src/ui/components/primitives/input-field.tsx +7 -3
- package/src/ui/components/primitives/surface.tsx +3 -0
- package/src/ui/components/settings/row-value.tsx +74 -0
- package/src/ui/components/settings/settings-row.tsx +78 -0
- package/src/ui/components/settings/settings-view.tsx +169 -0
- package/src/ui/components/setup/setup-widget.tsx +152 -0
- package/src/ui/flash/build-labels.ts +27 -27
- package/src/ui/hooks/use-activity-sprite.ts +73 -0
- package/src/ui/hooks/use-scroll-active-into-view.ts +26 -0
- package/src/ui/{session-ordering.ts → project-ordering.ts} +4 -4
- package/src/ui/root.tsx +139 -121
- package/src/ui/status-bar-model.ts +48 -34
- package/src/ui/terminal-graphics/kitty.ts +28 -2
- package/src/ui/terminal-graphics/sprites/done@330/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/done@330/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/idle@1000/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/idle@1000/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/waiting@170/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/waiting@170/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/2.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/3.png +0 -0
- package/src/ui/terminal-graphics/sprites.ts +246 -0
- package/src/ui/truncate.ts +7 -0
- package/src/ui/widgets/registry.tsx +6 -3
- package/src/ui/widgets/widget-context-menu.ts +26 -10
- package/src/update.ts +2 -2
- package/src/app-runtime/session-actions.ts +0 -187
- package/src/cli/client/workspace-resolver.ts +0 -110
- package/src/cli/commands/workspace/close.ts +0 -32
- package/src/cli/commands/workspace/show.ts +0 -32
- package/src/cli/commands/worktree/create.ts +0 -55
- package/src/cli/commands/worktree/list.ts +0 -68
- package/src/cli/commands/worktree/remove.ts +0 -81
- package/src/git/worktree-branch-poller.ts +0 -54
- package/src/git/worktree-divergence-poller.ts +0 -59
- package/src/state/reducers/session-state.ts +0 -262
- package/src/state/session-catalog.ts +0 -143
- package/src/state/session-worktrees.ts +0 -260
- package/src/state/workspace-save.ts +0 -43
- package/src/ui/components/layout/sidebar/use-top-tab-bar-auto-scroll.ts +0 -30
- package/src/ui/components/layout/sidebar/workspace-list.tsx +0 -418
- package/src/ui/components/layout/sidebar/worktree-row.tsx +0 -149
- /package/src/ui/{components/git/image-diff → terminal-graphics}/dimensions.ts +0 -0
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { AppState, SessionRecord } from './types'
|
|
2
|
-
|
|
3
|
-
import { loadConfig, saveConfig } from '../config'
|
|
4
|
-
import { saveSessionCatalog } from './session-catalog'
|
|
5
|
-
import { serializeWorkspace } from './session-persistence'
|
|
6
|
-
|
|
7
|
-
export function buildSessionsWithCurrentSnapshot(
|
|
8
|
-
sessions: SessionRecord[],
|
|
9
|
-
currentSessionId: string | null,
|
|
10
|
-
state: AppState
|
|
11
|
-
): SessionRecord[] {
|
|
12
|
-
return sessions.map((session) =>
|
|
13
|
-
session.id === currentSessionId
|
|
14
|
-
? {
|
|
15
|
-
...session,
|
|
16
|
-
updatedAt: new Date().toISOString(),
|
|
17
|
-
workspaceSnapshot: serializeWorkspace(state),
|
|
18
|
-
}
|
|
19
|
-
: session
|
|
20
|
-
)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function saveCurrentWorkspace(state: AppState): void {
|
|
24
|
-
saveConfig({
|
|
25
|
-
...loadConfig(),
|
|
26
|
-
bars: state.bars,
|
|
27
|
-
customCommands: state.customCommands,
|
|
28
|
-
gitPane: {
|
|
29
|
-
diffModeRatio: state.gitPane.diffModeRatio,
|
|
30
|
-
fileListMode: state.gitPane.fileListMode,
|
|
31
|
-
},
|
|
32
|
-
sessionBarVisible: state.sessionBar.visible,
|
|
33
|
-
// Legacy mirror of the left bar: lets an older build (and the workspace
|
|
34
|
-
// snapshot schema) still find a sidebar after a downgrade.
|
|
35
|
-
sidebar: {
|
|
36
|
-
visible: state.bars.left.visible,
|
|
37
|
-
width: state.bars.left.width,
|
|
38
|
-
},
|
|
39
|
-
})
|
|
40
|
-
saveSessionCatalog(
|
|
41
|
-
buildSessionsWithCurrentSnapshot(state.sessions, state.currentSessionId, state)
|
|
42
|
-
)
|
|
43
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { ScrollBoxRenderable } from '@opentui/core'
|
|
2
|
-
|
|
3
|
-
import { useEffect } from 'react'
|
|
4
|
-
|
|
5
|
-
interface Options {
|
|
6
|
-
scrollRef: React.RefObject<ScrollBoxRenderable | null>
|
|
7
|
-
visible: boolean
|
|
8
|
-
activeTabId: string | null
|
|
9
|
-
idPrefix: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Scroll the active tab into view whenever the active tab id changes (and on
|
|
14
|
-
* first reveal). The top bar is a single horizontal row, so this is just a
|
|
15
|
-
* thin wrapper around `scrollChildIntoView` — no wrap-around/edge heuristics.
|
|
16
|
-
*/
|
|
17
|
-
export function useTopTabBarAutoScroll({
|
|
18
|
-
activeTabId,
|
|
19
|
-
idPrefix,
|
|
20
|
-
scrollRef,
|
|
21
|
-
visible,
|
|
22
|
-
}: Options): void {
|
|
23
|
-
useEffect(() => {
|
|
24
|
-
if (!visible) return
|
|
25
|
-
if (activeTabId == null || activeTabId === '') return
|
|
26
|
-
const scrollbox = scrollRef.current
|
|
27
|
-
if (!scrollbox) return
|
|
28
|
-
scrollbox.scrollChildIntoView(`${idPrefix}${activeTabId}`)
|
|
29
|
-
}, [activeTabId, idPrefix, scrollRef, visible])
|
|
30
|
-
}
|
|
@@ -1,418 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
BoxRenderable,
|
|
3
|
-
MouseEvent as OtuiMouseEvent,
|
|
4
|
-
ScrollBoxRenderable,
|
|
5
|
-
} from '@opentui/core'
|
|
6
|
-
|
|
7
|
-
import { memo, type ReactNode, useCallback, useMemo, useRef, useState } from 'react'
|
|
8
|
-
|
|
9
|
-
import type { SessionRecord, SessionStatus, WorktreeRecord } from '../../../../state/types'
|
|
10
|
-
|
|
11
|
-
import { useAppStore } from '../../../../state/app-store'
|
|
12
|
-
import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
|
|
13
|
-
import { formatDivergence } from '../../../../state/session-worktrees'
|
|
14
|
-
// eslint-disable-next-line no-duplicate-imports
|
|
15
|
-
import { IDLE_SESSION_STATUS } from '../../../../state/types'
|
|
16
|
-
import { useBusySpinner } from '../../../hooks/use-busy-spinner'
|
|
17
|
-
import { moveIdToIdPosition, orderSessionsForDisplay } from '../../../session-ordering'
|
|
18
|
-
import { useBaseTheme, useTheme } from '../../../theme'
|
|
19
|
-
import { FlashLabelBadge } from '../../flash/flash-label-badge'
|
|
20
|
-
import { ContextMenuBox } from '../../overlays/context-menu/context-menu-box'
|
|
21
|
-
import { useSidebarAutoScroll } from './use-sidebar-auto-scroll'
|
|
22
|
-
import { WorktreeRow } from './worktree-row'
|
|
23
|
-
|
|
24
|
-
interface WorkspaceListProps {
|
|
25
|
-
contentWidth: number
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const COLUMN_CONTENT_OPTIONS = { flexDirection: 'column' as const, gap: 0 }
|
|
29
|
-
|
|
30
|
-
function arraysEqual(a: string[], b: string[]): boolean {
|
|
31
|
-
if (a.length !== b.length) return false
|
|
32
|
-
for (let i = 0; i < a.length; i++) {
|
|
33
|
-
if (a[i] !== b[i]) return false
|
|
34
|
-
}
|
|
35
|
-
return true
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function truncate(label: string, max: number): string {
|
|
39
|
-
if (max <= 0) return ''
|
|
40
|
-
if (label.length <= max) return label
|
|
41
|
-
if (max === 1) return '…'
|
|
42
|
-
return `${label.slice(0, max - 1)}…`
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function WorkspaceList({ contentWidth }: WorkspaceListProps) {
|
|
46
|
-
const t = useTheme()
|
|
47
|
-
const sessions = useAppStore((s) => s.sessions)
|
|
48
|
-
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
49
|
-
const statusMap = useAppStore((s) => s.sessionStatuses)
|
|
50
|
-
|
|
51
|
-
const [draggingId, setDraggingId] = useState<string | null>(null)
|
|
52
|
-
const [dragOrder, setDragOrder] = useState<string[] | null>(null)
|
|
53
|
-
const lastSwapWithRef = useRef<string | null>(null)
|
|
54
|
-
const rowRefs = useRef(new Map<string, BoxRenderable>())
|
|
55
|
-
const scrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
56
|
-
|
|
57
|
-
const ordered = useMemo(() => orderSessionsForDisplay(sessions), [sessions])
|
|
58
|
-
const baselineOrder = useMemo(() => ordered.map((s) => s.id), [ordered])
|
|
59
|
-
|
|
60
|
-
const currentSession = useMemo(
|
|
61
|
-
() =>
|
|
62
|
-
currentSessionId != null && currentSessionId !== ''
|
|
63
|
-
? sessions.find((s) => s.id === currentSessionId)
|
|
64
|
-
: undefined,
|
|
65
|
-
[currentSessionId, sessions]
|
|
66
|
-
)
|
|
67
|
-
// The active row can be either a worktree row OR the workspace row
|
|
68
|
-
// (when the primary worktree is active). Both must scroll into view —
|
|
69
|
-
// otherwise the cursor visually "disappears" off-screen when crossing
|
|
70
|
-
// a workspace boundary on a key press.
|
|
71
|
-
const currentWorktrees = currentSession?.worktrees ?? []
|
|
72
|
-
const currentPrimary = currentWorktrees.find((w) => w.source === 'primary') ?? currentWorktrees[0]
|
|
73
|
-
const rawActiveWorktreeId = currentSession?.activeWorktreeId
|
|
74
|
-
const activeOnNonPrimary =
|
|
75
|
-
rawActiveWorktreeId != null &&
|
|
76
|
-
rawActiveWorktreeId !== '' &&
|
|
77
|
-
rawActiveWorktreeId !== currentPrimary?.id
|
|
78
|
-
let activeRowId: string | null = null
|
|
79
|
-
if (activeOnNonPrimary) {
|
|
80
|
-
activeRowId = `sidebar-wt-${rawActiveWorktreeId}`
|
|
81
|
-
} else if (currentSessionId != null && currentSessionId !== '') {
|
|
82
|
-
activeRowId = `sidebar-ws-${currentSessionId}`
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
useSidebarAutoScroll({
|
|
86
|
-
activeRowId,
|
|
87
|
-
scrollRef,
|
|
88
|
-
visible: true,
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
const setRowRef = useCallback((id: string, ref: BoxRenderable | null): void => {
|
|
92
|
-
if (ref) rowRefs.current.set(id, ref)
|
|
93
|
-
else rowRefs.current.delete(id)
|
|
94
|
-
}, [])
|
|
95
|
-
|
|
96
|
-
const findRowAtY = useCallback((y: number): string | null => {
|
|
97
|
-
for (const [id, ref] of rowRefs.current) {
|
|
98
|
-
if (y >= ref.y && y < ref.y + ref.height) return id
|
|
99
|
-
}
|
|
100
|
-
return null
|
|
101
|
-
}, [])
|
|
102
|
-
|
|
103
|
-
const handleRowDragStart = useCallback(
|
|
104
|
-
(id: string) => {
|
|
105
|
-
setDraggingId(id)
|
|
106
|
-
setDragOrder(baselineOrder)
|
|
107
|
-
lastSwapWithRef.current = null
|
|
108
|
-
},
|
|
109
|
-
[baselineOrder]
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
const handleRowDrag = useCallback(
|
|
113
|
-
(event: OtuiMouseEvent) => {
|
|
114
|
-
if (!(draggingId != null && draggingId !== '')) return
|
|
115
|
-
const hit = findRowAtY(event.y)
|
|
116
|
-
if (hit === null) {
|
|
117
|
-
lastSwapWithRef.current = null
|
|
118
|
-
return
|
|
119
|
-
}
|
|
120
|
-
if (hit === draggingId) {
|
|
121
|
-
lastSwapWithRef.current = null
|
|
122
|
-
return
|
|
123
|
-
}
|
|
124
|
-
if (hit === lastSwapWithRef.current) return
|
|
125
|
-
setDragOrder((prev) => (prev ? moveIdToIdPosition(prev, draggingId, hit) : prev))
|
|
126
|
-
lastSwapWithRef.current = hit
|
|
127
|
-
},
|
|
128
|
-
[draggingId, findRowAtY]
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
const commitDrop = useCallback(() => {
|
|
132
|
-
const source = draggingId
|
|
133
|
-
const finalOrder = dragOrder
|
|
134
|
-
setDraggingId(null)
|
|
135
|
-
setDragOrder(null)
|
|
136
|
-
lastSwapWithRef.current = null
|
|
137
|
-
|
|
138
|
-
if (source == null || source === '' || !finalOrder) return
|
|
139
|
-
|
|
140
|
-
const changed = !arraysEqual(finalOrder, baselineOrder)
|
|
141
|
-
if (changed) {
|
|
142
|
-
dispatchGlobal({ orderedIds: finalOrder, type: 'reorder-sessions' })
|
|
143
|
-
return
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const idx = baselineOrder.indexOf(source)
|
|
147
|
-
if (idx >= 0) {
|
|
148
|
-
// The workspace row visually anchors the session's primary worktree
|
|
149
|
-
// (its branch line shows the primary's branch). Clicking it should
|
|
150
|
-
// land on the primary — same semantics as j/k cycling onto a workspace
|
|
151
|
-
// item — instead of preserving whatever non-primary worktree happened
|
|
152
|
-
// to be active last time we left this session.
|
|
153
|
-
const sourceSession = ordered.find((s) => s.id === source)
|
|
154
|
-
const sourceWorktrees = sourceSession?.worktrees ?? []
|
|
155
|
-
const sourcePrimaryId = (
|
|
156
|
-
sourceWorktrees.find((w) => w.source === 'primary') ?? sourceWorktrees[0]
|
|
157
|
-
)?.id
|
|
158
|
-
runSideEffectGlobal({
|
|
159
|
-
index: idx + 1,
|
|
160
|
-
type: 'switch-session-by-index',
|
|
161
|
-
worktreeId: sourcePrimaryId,
|
|
162
|
-
})
|
|
163
|
-
}
|
|
164
|
-
}, [baselineOrder, dragOrder, draggingId, ordered])
|
|
165
|
-
|
|
166
|
-
const cancelDrag = useCallback(() => {
|
|
167
|
-
setDraggingId(null)
|
|
168
|
-
setDragOrder(null)
|
|
169
|
-
lastSwapWithRef.current = null
|
|
170
|
-
}, [])
|
|
171
|
-
|
|
172
|
-
const handleNewSession = useCallback((e: OtuiMouseEvent) => {
|
|
173
|
-
e.stopPropagation()
|
|
174
|
-
e.preventDefault()
|
|
175
|
-
dispatchGlobal({ returnToSessionPicker: false, type: 'open-create-session-modal' })
|
|
176
|
-
}, [])
|
|
177
|
-
|
|
178
|
-
const visibleSessions =
|
|
179
|
-
dragOrder !== null
|
|
180
|
-
? dragOrder
|
|
181
|
-
.map((id) => ordered.find((s) => s.id === id))
|
|
182
|
-
.filter((s): s is SessionRecord => !!s)
|
|
183
|
-
: ordered
|
|
184
|
-
|
|
185
|
-
return (
|
|
186
|
-
<box flexDirection="column" flexGrow={1} flexShrink={1} overflow="hidden">
|
|
187
|
-
<scrollbox
|
|
188
|
-
ref={scrollRef}
|
|
189
|
-
scrollY
|
|
190
|
-
flexGrow={1}
|
|
191
|
-
flexShrink={1}
|
|
192
|
-
contentOptions={COLUMN_CONTENT_OPTIONS}
|
|
193
|
-
>
|
|
194
|
-
{(() => {
|
|
195
|
-
// Build a single flat list of items — workspace rows interleaved
|
|
196
|
-
// with their non-primary worktrees. One map, one React keypath per
|
|
197
|
-
// visible row; transitions are a single atomic reconciliation.
|
|
198
|
-
const rows: ReactNode[] = []
|
|
199
|
-
for (const [visibleIdx, session] of visibleSessions.entries()) {
|
|
200
|
-
const sessionIndex = baselineOrder.indexOf(session.id) + 1
|
|
201
|
-
const isCurrentSession = session.id === currentSessionId
|
|
202
|
-
const worktrees = session.worktrees ?? []
|
|
203
|
-
const primaryWorktree = worktrees.find((w) => w.source === 'primary') ?? worktrees[0]
|
|
204
|
-
const extraWorktrees = worktrees.filter((w) => w.id !== primaryWorktree?.id)
|
|
205
|
-
const workspaceIsActiveItem =
|
|
206
|
-
isCurrentSession &&
|
|
207
|
-
(session.activeWorktreeId == null ||
|
|
208
|
-
session.activeWorktreeId === '' ||
|
|
209
|
-
session.activeWorktreeId === primaryWorktree?.id)
|
|
210
|
-
rows.push(
|
|
211
|
-
<WorkspaceRow
|
|
212
|
-
key={`ws:${session.id}`}
|
|
213
|
-
session={session}
|
|
214
|
-
isActiveItem={workspaceIsActiveItem}
|
|
215
|
-
inCurrentGroup={isCurrentSession}
|
|
216
|
-
primaryWorktree={primaryWorktree}
|
|
217
|
-
hasExtraWorktrees={extraWorktrees.length > 0}
|
|
218
|
-
status={statusMap[session.id] ?? IDLE_SESSION_STATUS}
|
|
219
|
-
dragging={draggingId === session.id}
|
|
220
|
-
contentWidth={contentWidth}
|
|
221
|
-
marginTop={visibleIdx > 0 ? 1 : 0}
|
|
222
|
-
setRowRef={setRowRef}
|
|
223
|
-
onDragStart={handleRowDragStart}
|
|
224
|
-
onDrag={handleRowDrag}
|
|
225
|
-
onDrop={commitDrop}
|
|
226
|
-
onDragCancel={cancelDrag}
|
|
227
|
-
/>
|
|
228
|
-
)
|
|
229
|
-
for (const [wtIdx, worktree] of extraWorktrees.entries()) {
|
|
230
|
-
rows.push(
|
|
231
|
-
<WorktreeRow
|
|
232
|
-
key={`wt:${worktree.id}`}
|
|
233
|
-
session={session}
|
|
234
|
-
worktree={worktree}
|
|
235
|
-
sessionIndex={sessionIndex}
|
|
236
|
-
isActiveItem={isCurrentSession && worktree.id === session.activeWorktreeId}
|
|
237
|
-
inCurrentGroup={isCurrentSession}
|
|
238
|
-
isLast={wtIdx === extraWorktrees.length - 1}
|
|
239
|
-
/>
|
|
240
|
-
)
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
return rows
|
|
244
|
-
})()}
|
|
245
|
-
</scrollbox>
|
|
246
|
-
<box
|
|
247
|
-
flexDirection="row"
|
|
248
|
-
flexShrink={0}
|
|
249
|
-
marginTop={1}
|
|
250
|
-
backgroundColor={t.backgroundPanel}
|
|
251
|
-
justifyContent="center"
|
|
252
|
-
onMouseDown={handleNewSession}
|
|
253
|
-
>
|
|
254
|
-
<text fg={t.text} selectable={false}>
|
|
255
|
-
+ New workspace
|
|
256
|
-
</text>
|
|
257
|
-
</box>
|
|
258
|
-
</box>
|
|
259
|
-
)
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
interface WorkspaceRowProps {
|
|
263
|
-
session: SessionRecord
|
|
264
|
-
/** True when this row is the active cursor item (workspace's primary active). */
|
|
265
|
-
isActiveItem: boolean
|
|
266
|
-
/** True when this row belongs to the current workspace (selection scope). */
|
|
267
|
-
inCurrentGroup: boolean
|
|
268
|
-
/** The session's primary worktree — its git branch is shown as the workspace's anchor identity. */
|
|
269
|
-
primaryWorktree: WorktreeRecord | undefined
|
|
270
|
-
/** True when at least one non-primary worktree follows — used to draw the tree continuator. */
|
|
271
|
-
hasExtraWorktrees: boolean
|
|
272
|
-
status: SessionStatus
|
|
273
|
-
dragging: boolean
|
|
274
|
-
contentWidth: number
|
|
275
|
-
/** Vertical spacing above this row — used to separate workspace blocks. */
|
|
276
|
-
marginTop: number
|
|
277
|
-
setRowRef: (id: string, ref: BoxRenderable | null) => void
|
|
278
|
-
onDragStart: (id: string) => void
|
|
279
|
-
onDrag: (event: OtuiMouseEvent) => void
|
|
280
|
-
onDrop: () => void
|
|
281
|
-
onDragCancel: () => void
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const WorkspaceRow = memo(function WorkspaceRow({
|
|
285
|
-
contentWidth,
|
|
286
|
-
dragging,
|
|
287
|
-
hasExtraWorktrees,
|
|
288
|
-
inCurrentGroup,
|
|
289
|
-
isActiveItem,
|
|
290
|
-
marginTop,
|
|
291
|
-
onDrag,
|
|
292
|
-
onDragCancel,
|
|
293
|
-
onDragStart,
|
|
294
|
-
onDrop,
|
|
295
|
-
primaryWorktree,
|
|
296
|
-
session,
|
|
297
|
-
setRowRef,
|
|
298
|
-
status,
|
|
299
|
-
}: WorkspaceRowProps) {
|
|
300
|
-
const t = useTheme()
|
|
301
|
-
// Selection highlight must stay opaque in transparent mode — otherwise the
|
|
302
|
-
// cursor row visually disappears against the see-through chrome.
|
|
303
|
-
const base = useBaseTheme()
|
|
304
|
-
const showSpinner = status.working
|
|
305
|
-
const showWaiting = status.waiting
|
|
306
|
-
const spinner = useBusySpinner(showSpinner)
|
|
307
|
-
let bgColor: string | undefined
|
|
308
|
-
if (dragging || isActiveItem) {
|
|
309
|
-
bgColor = base.backgroundElement
|
|
310
|
-
} else if (inCurrentGroup) {
|
|
311
|
-
bgColor = base.backgroundPanel
|
|
312
|
-
}
|
|
313
|
-
const workingColor = t.primary
|
|
314
|
-
const waitingColor = t.warning
|
|
315
|
-
const divergence = useAppStore((s) =>
|
|
316
|
-
primaryWorktree ? s.worktreeDivergence[primaryWorktree.id] : undefined
|
|
317
|
-
)
|
|
318
|
-
|
|
319
|
-
const handleRef = useCallback(
|
|
320
|
-
(r: BoxRenderable | null) => setRowRef(session.id, r),
|
|
321
|
-
[setRowRef, session.id]
|
|
322
|
-
)
|
|
323
|
-
const handleMouseDown = useCallback(
|
|
324
|
-
(e: OtuiMouseEvent) => {
|
|
325
|
-
e.preventDefault()
|
|
326
|
-
e.stopPropagation()
|
|
327
|
-
onDragStart(session.id)
|
|
328
|
-
},
|
|
329
|
-
[onDragStart, session.id]
|
|
330
|
-
)
|
|
331
|
-
const handleMouseUp = useCallback(
|
|
332
|
-
(e: OtuiMouseEvent) => {
|
|
333
|
-
e.preventDefault()
|
|
334
|
-
onDrop()
|
|
335
|
-
},
|
|
336
|
-
[onDrop]
|
|
337
|
-
)
|
|
338
|
-
const rightClickMenu = useMemo<[string, () => void][]>(
|
|
339
|
-
() => [
|
|
340
|
-
[
|
|
341
|
-
'Rename workspace',
|
|
342
|
-
() =>
|
|
343
|
-
dispatchGlobal({
|
|
344
|
-
initialName: session.name,
|
|
345
|
-
returnToSessionPicker: false,
|
|
346
|
-
sessionTargetId: session.id,
|
|
347
|
-
type: 'open-session-name-modal',
|
|
348
|
-
}),
|
|
349
|
-
],
|
|
350
|
-
[
|
|
351
|
-
'Delete workspace',
|
|
352
|
-
() => runSideEffectGlobal({ sessionId: session.id, type: 'delete-session' }),
|
|
353
|
-
],
|
|
354
|
-
],
|
|
355
|
-
[session.id, session.name]
|
|
356
|
-
)
|
|
357
|
-
|
|
358
|
-
// Neutral muted marker; working/waiting overrides it. Keeping a glyph
|
|
359
|
-
// in the slot avoids name shifts when state indicators come and go.
|
|
360
|
-
let leadingGlyph = '•'
|
|
361
|
-
let leadingColor = t.textMuted
|
|
362
|
-
if (showWaiting) {
|
|
363
|
-
leadingGlyph = '?'
|
|
364
|
-
leadingColor = waitingColor
|
|
365
|
-
} else if (showSpinner) {
|
|
366
|
-
leadingGlyph = spinner
|
|
367
|
-
leadingColor = workingColor
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const branchText = primaryWorktree?.branch ?? ''
|
|
371
|
-
const divergenceText = formatDivergence(divergence)
|
|
372
|
-
const showBranch = branchText !== ''
|
|
373
|
-
const nameLabel = truncate(session.name, Math.max(0, contentWidth - 4))
|
|
374
|
-
const branchLabel = truncate(
|
|
375
|
-
branchText,
|
|
376
|
-
Math.max(0, contentWidth - 5 - (divergenceText.length + 1))
|
|
377
|
-
)
|
|
378
|
-
|
|
379
|
-
return (
|
|
380
|
-
<ContextMenuBox
|
|
381
|
-
ref={handleRef}
|
|
382
|
-
id={`sidebar-ws-${session.id}`}
|
|
383
|
-
flexDirection="column"
|
|
384
|
-
flexShrink={0}
|
|
385
|
-
marginTop={marginTop}
|
|
386
|
-
paddingLeft={1}
|
|
387
|
-
paddingRight={1}
|
|
388
|
-
backgroundColor={bgColor}
|
|
389
|
-
rightClickMenu={rightClickMenu}
|
|
390
|
-
onMouseDown={handleMouseDown}
|
|
391
|
-
onMouseDrag={onDrag}
|
|
392
|
-
onMouseUp={handleMouseUp}
|
|
393
|
-
onMouseDragEnd={onDragCancel}
|
|
394
|
-
>
|
|
395
|
-
<box flexDirection="row" alignItems="center">
|
|
396
|
-
<text fg={leadingColor} selectable={false} wrapMode="none">
|
|
397
|
-
{leadingGlyph}
|
|
398
|
-
</text>
|
|
399
|
-
<text fg={t.text} selectable={false} wrapMode="none">
|
|
400
|
-
{' '}
|
|
401
|
-
</text>
|
|
402
|
-
<FlashLabelBadge rowKey={`ws:${session.id}`} />
|
|
403
|
-
<text fg={t.text} selectable={false} wrapMode="none">
|
|
404
|
-
{nameLabel}
|
|
405
|
-
</text>
|
|
406
|
-
</box>
|
|
407
|
-
{showBranch ? (
|
|
408
|
-
<box flexDirection="row">
|
|
409
|
-
<text fg={t.textMuted} selectable={false} wrapMode="none">
|
|
410
|
-
{hasExtraWorktrees ? '│ ' : ' '}
|
|
411
|
-
{'\u{e702}'} {branchLabel}
|
|
412
|
-
{divergenceText !== '' ? ` ${divergenceText}` : ''}
|
|
413
|
-
</text>
|
|
414
|
-
</box>
|
|
415
|
-
) : null}
|
|
416
|
-
</ContextMenuBox>
|
|
417
|
-
)
|
|
418
|
-
})
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import type { MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
2
|
-
|
|
3
|
-
import { memo, useCallback, useMemo } from 'react'
|
|
4
|
-
|
|
5
|
-
import type { SessionRecord, WorktreeRecord } from '../../../../state/types'
|
|
6
|
-
|
|
7
|
-
import { useAppStore } from '../../../../state/app-store'
|
|
8
|
-
import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
|
|
9
|
-
import { formatDivergence } from '../../../../state/session-worktrees'
|
|
10
|
-
import { useBaseTheme, useTheme } from '../../../theme'
|
|
11
|
-
import { FlashLabelBadge } from '../../flash/flash-label-badge'
|
|
12
|
-
import { ContextMenuBox } from '../../overlays/context-menu/context-menu-box'
|
|
13
|
-
|
|
14
|
-
interface WorktreeRowProps {
|
|
15
|
-
session: SessionRecord
|
|
16
|
-
worktree: WorktreeRecord
|
|
17
|
-
/** 1-based index of the workspace in the visible order (for switch-session-by-index). */
|
|
18
|
-
sessionIndex: number
|
|
19
|
-
/** True when this row is the active cursor item. */
|
|
20
|
-
isActiveItem: boolean
|
|
21
|
-
/** True when this row's workspace is the current session (selection scope). */
|
|
22
|
-
inCurrentGroup: boolean
|
|
23
|
-
/** True when this is the last non-primary worktree of its workspace. Drives └─ vs ├─. */
|
|
24
|
-
isLast: boolean
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const WorktreeRow = memo(function WorktreeRow({
|
|
28
|
-
inCurrentGroup,
|
|
29
|
-
isActiveItem,
|
|
30
|
-
isLast,
|
|
31
|
-
session,
|
|
32
|
-
sessionIndex,
|
|
33
|
-
worktree,
|
|
34
|
-
}: WorktreeRowProps) {
|
|
35
|
-
const t = useTheme()
|
|
36
|
-
// Selection highlight must stay opaque in transparent mode.
|
|
37
|
-
const base = useBaseTheme()
|
|
38
|
-
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
39
|
-
const isCurrentSession = session.id === currentSessionId
|
|
40
|
-
const divergence = useAppStore((s) => s.worktreeDivergence[worktree.id])
|
|
41
|
-
|
|
42
|
-
const handleMouseDown = useCallback(
|
|
43
|
-
(event: OtuiMouseEvent) => {
|
|
44
|
-
if (event.button !== 0) return
|
|
45
|
-
event.preventDefault()
|
|
46
|
-
event.stopPropagation()
|
|
47
|
-
if (isCurrentSession) {
|
|
48
|
-
dispatchGlobal({
|
|
49
|
-
sessionId: session.id,
|
|
50
|
-
type: 'set-active-worktree',
|
|
51
|
-
worktreeId: worktree.id,
|
|
52
|
-
})
|
|
53
|
-
return
|
|
54
|
-
}
|
|
55
|
-
// Cross-workspace click: send the worktree id along with the switch so
|
|
56
|
-
// the side effect can apply it atomically. Splitting it into a separate
|
|
57
|
-
// dispatch+switch leaves a window where subscribers/re-renders can
|
|
58
|
-
// re-assert the target session's last-persisted worktree.
|
|
59
|
-
runSideEffectGlobal({
|
|
60
|
-
index: sessionIndex,
|
|
61
|
-
type: 'switch-session-by-index',
|
|
62
|
-
worktreeId: worktree.id,
|
|
63
|
-
})
|
|
64
|
-
},
|
|
65
|
-
[isCurrentSession, session.id, sessionIndex, worktree.id]
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
const rightClickMenu = useMemo<[string, () => void][] | undefined>(() => {
|
|
69
|
-
const entries: [string, () => void][] = [
|
|
70
|
-
[
|
|
71
|
-
'Rename',
|
|
72
|
-
() =>
|
|
73
|
-
dispatchGlobal({
|
|
74
|
-
initialName: worktree.name,
|
|
75
|
-
sessionId: session.id,
|
|
76
|
-
type: 'open-rename-worktree-modal',
|
|
77
|
-
worktreeId: worktree.id,
|
|
78
|
-
}),
|
|
79
|
-
],
|
|
80
|
-
]
|
|
81
|
-
if (worktree.source !== 'primary') {
|
|
82
|
-
entries.push([
|
|
83
|
-
'Remove worktree',
|
|
84
|
-
() =>
|
|
85
|
-
// Always confirm first. Confirming routes through the full delete side
|
|
86
|
-
// effect (closes the worktree's tabs, disposes their PTYs, prunes the
|
|
87
|
-
// snapshot, removes the git worktree). closeTabs (not force) cleans up
|
|
88
|
-
// the tabs while keeping the non-force `git worktree remove`, so
|
|
89
|
-
// uncommitted work in a temp worktree is still protected — a dirty
|
|
90
|
-
// worktree re-prompts for an explicit force-delete.
|
|
91
|
-
dispatchGlobal({
|
|
92
|
-
closeTabs: true,
|
|
93
|
-
force: false,
|
|
94
|
-
reason: 'Its assistant tabs will be closed and the worktree removed.',
|
|
95
|
-
sessionId: session.id,
|
|
96
|
-
type: 'open-worktree-delete-confirm',
|
|
97
|
-
worktreeId: worktree.id,
|
|
98
|
-
worktreeLabel: worktree.branch ?? worktree.name,
|
|
99
|
-
}),
|
|
100
|
-
])
|
|
101
|
-
}
|
|
102
|
-
return entries
|
|
103
|
-
}, [session.id, worktree.branch, worktree.id, worktree.name, worktree.source])
|
|
104
|
-
|
|
105
|
-
let bgColor: string | undefined
|
|
106
|
-
if (isActiveItem) {
|
|
107
|
-
bgColor = base.backgroundElement
|
|
108
|
-
} else if (inCurrentGroup) {
|
|
109
|
-
bgColor = base.backgroundPanel
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const connector = isLast ? '└─' : '├─'
|
|
113
|
-
const branchCont = isLast ? ' ' : '│ '
|
|
114
|
-
const branchText = worktree.branch ?? ''
|
|
115
|
-
const showBranch = branchText !== ''
|
|
116
|
-
const divergenceText = formatDivergence(divergence)
|
|
117
|
-
|
|
118
|
-
return (
|
|
119
|
-
<ContextMenuBox
|
|
120
|
-
id={`sidebar-wt-${worktree.id}`}
|
|
121
|
-
flexDirection="column"
|
|
122
|
-
flexShrink={0}
|
|
123
|
-
paddingLeft={1}
|
|
124
|
-
paddingRight={1}
|
|
125
|
-
backgroundColor={bgColor}
|
|
126
|
-
rightClickMenu={rightClickMenu}
|
|
127
|
-
onMouseDown={handleMouseDown}
|
|
128
|
-
>
|
|
129
|
-
<box flexDirection="row" alignItems="center">
|
|
130
|
-
<text fg={t.textMuted} selectable={false} wrapMode="none">
|
|
131
|
-
{connector}{' '}
|
|
132
|
-
</text>
|
|
133
|
-
<FlashLabelBadge rowKey={`wt:${worktree.id}`} />
|
|
134
|
-
<text fg={t.text} selectable={false} wrapMode="none">
|
|
135
|
-
{worktree.name}
|
|
136
|
-
</text>
|
|
137
|
-
</box>
|
|
138
|
-
{showBranch ? (
|
|
139
|
-
<box flexDirection="row">
|
|
140
|
-
<text fg={t.textMuted} selectable={false} wrapMode="none">
|
|
141
|
-
{branchCont}
|
|
142
|
-
{'\u{e702}'} {branchText}
|
|
143
|
-
{divergenceText !== '' ? ` ${divergenceText}` : ''}
|
|
144
|
-
</text>
|
|
145
|
-
</box>
|
|
146
|
-
) : null}
|
|
147
|
-
</ContextMenuBox>
|
|
148
|
-
)
|
|
149
|
-
})
|
|
File without changes
|