@brimveyn/aimux 1.13.2 → 1.14.1

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.
Files changed (54) hide show
  1. package/package.json +2 -2
  2. package/src/app-runtime/backend-attach-runtime.ts +1 -3
  3. package/src/app-runtime/multi-click-clipboard-guard.ts +24 -12
  4. package/src/app-runtime/pty-write.ts +6 -9
  5. package/src/app-runtime/side-effects.ts +149 -19
  6. package/src/app-runtime/snippet-actions.ts +2 -3
  7. package/src/app-runtime/split-drag-controller.ts +3 -1
  8. package/src/app-runtime/use-backend-runtime.ts +4 -7
  9. package/src/app-runtime/use-mouse-handlers.ts +30 -1
  10. package/src/app-runtime/use-renderer-bindings.ts +4 -12
  11. package/src/app-runtime/use-terminal-resize.ts +7 -22
  12. package/src/app.tsx +0 -6
  13. package/src/config.ts +1 -12
  14. package/src/daemon/daemon.ts +2 -19
  15. package/src/daemon/session-manager.ts +3 -15
  16. package/src/daemon/session-registry.ts +11 -30
  17. package/src/git/worktree-branch-poller.ts +54 -0
  18. package/src/input/modes/types.ts +2 -0
  19. package/src/input/terminal-text-extraction.ts +7 -8
  20. package/src/ipc/manager-protocol.ts +6 -38
  21. package/src/ipc/protocol.ts +9 -37
  22. package/src/pty/pty-manager.ts +20 -31
  23. package/src/pty/terminal-snapshot.ts +43 -0
  24. package/src/session-backend/local-session-backend.ts +7 -31
  25. package/src/session-backend/remote-session-backend.ts +5 -32
  26. package/src/session-backend/types.ts +2 -15
  27. package/src/state/layout-tree.ts +114 -4
  28. package/src/state/reducers/session-state.ts +20 -0
  29. package/src/state/reducers/tab-state.ts +22 -22
  30. package/src/state/reducers/ui-state.ts +0 -3
  31. package/src/state/session-persistence.ts +2 -22
  32. package/src/state/session-worktrees.ts +45 -2
  33. package/src/state/store.ts +0 -3
  34. package/src/state/tab-entries.ts +74 -0
  35. package/src/state/types.ts +4 -15
  36. package/src/state/validation.ts +0 -8
  37. package/src/state/workspace-save.ts +0 -1
  38. package/src/terminal-manager/manager-client.ts +5 -29
  39. package/src/terminal-manager/terminal-manager.ts +2 -17
  40. package/src/ui/components/layout/sidebar/sidebar.tsx +9 -340
  41. package/src/ui/components/layout/sidebar/tab-item.tsx +51 -42
  42. package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +10 -53
  43. package/src/ui/components/layout/sidebar/use-top-tab-bar-auto-scroll.ts +30 -0
  44. package/src/ui/components/layout/sidebar/workspace-list.tsx +398 -0
  45. package/src/ui/components/layout/sidebar/worktree-row.tsx +92 -0
  46. package/src/ui/components/layout/split-layout.tsx +20 -39
  47. package/src/ui/components/layout/terminal-pane.tsx +30 -0
  48. package/src/ui/components/layout/top-tab-bar.tsx +304 -0
  49. package/src/ui/components/modals/worktree/worktree-move-modal.tsx +1 -8
  50. package/src/ui/root.tsx +68 -63
  51. package/src/ui/components/layout/session-bar.tsx +0 -296
  52. package/src/ui/components/layout/sidebar/sidebar-group-metadata.ts +0 -44
  53. package/src/ui/components/layout/sidebar/sidebar-scroll.ts +0 -33
  54. package/src/ui/components/layout/sidebar/use-sidebar-branch.ts +0 -36
@@ -1,296 +0,0 @@
1
- import type { BoxRenderable, MouseEvent as OtuiMouseEvent } from '@opentui/core'
2
-
3
- import { memo, useCallback, useMemo, useRef, useState } from 'react'
4
-
5
- import type { SessionRecord, SessionStatus } from '../../../state/types'
6
-
7
- import { useAppStore } from '../../../state/app-store'
8
- import { dispatchGlobal, runSideEffectGlobal } from '../../../state/dispatch-ref'
9
- // eslint-disable-next-line no-duplicate-imports
10
- import { IDLE_SESSION_STATUS } from '../../../state/types'
11
- import { useBusySpinner } from '../../hooks/use-busy-spinner'
12
- import { moveIdToIdPosition, orderSessionsForDisplay } from '../../session-ordering'
13
- import { useTheme } from '../../theme'
14
- import { ContextMenuBox } from '../overlays/context-menu/context-menu-box'
15
-
16
- interface SessionBarProps {
17
- forceVisible?: boolean
18
- }
19
-
20
- export function SessionBar({ forceVisible = false }: SessionBarProps) {
21
- const t = useTheme()
22
- const headerBg = t.backgroundPanel
23
- const sessions = useAppStore((s) => s.sessions)
24
- const currentId = useAppStore((s) => s.currentSessionId)
25
- const bar = useAppStore((s) => s.sessionBar)
26
- const statusMap = useAppStore((s) => s.sessionStatuses)
27
-
28
- const [draggingId, setDraggingId] = useState<string | null>(null)
29
- const [dragOrder, setDragOrder] = useState<string[] | null>(null)
30
- const lastSwapWithRef = useRef<string | null>(null)
31
- const chipRefs = useRef(new Map<string, BoxRenderable>())
32
-
33
- const ordered = useMemo(() => orderSessionsForDisplay(sessions), [sessions])
34
- const baselineOrder = useMemo(() => ordered.map((s) => s.id), [ordered])
35
-
36
- const setChipRef = useCallback((id: string, ref: BoxRenderable | null): void => {
37
- if (ref) chipRefs.current.set(id, ref)
38
- else chipRefs.current.delete(id)
39
- }, [])
40
-
41
- const findChipAtX = useCallback((x: number): string | null => {
42
- for (const [id, ref] of chipRefs.current) {
43
- if (x >= ref.x && x < ref.x + ref.width) return id
44
- }
45
- return null
46
- }, [])
47
-
48
- const handleMouseDown = useCallback(
49
- (id: string) => {
50
- setDraggingId(id)
51
- setDragOrder(baselineOrder)
52
- lastSwapWithRef.current = null
53
- },
54
- [baselineOrder]
55
- )
56
-
57
- const handleMouseDrag = useCallback(
58
- (event: OtuiMouseEvent) => {
59
- if (!(draggingId != null && draggingId !== '')) return
60
- const hit = findChipAtX(event.x)
61
- if (hit === null) {
62
- lastSwapWithRef.current = null
63
- return
64
- }
65
- if (hit === draggingId) {
66
- lastSwapWithRef.current = null
67
- return
68
- }
69
- if (hit === lastSwapWithRef.current) return
70
- setDragOrder((prev) => (prev ? moveIdToIdPosition(prev, draggingId, hit) : prev))
71
- lastSwapWithRef.current = hit
72
- },
73
- [draggingId, findChipAtX]
74
- )
75
-
76
- const commitDrop = useCallback(() => {
77
- const source = draggingId
78
- const finalOrder = dragOrder
79
- setDraggingId(null)
80
- setDragOrder(null)
81
- lastSwapWithRef.current = null
82
-
83
- if (source == null || source === '' || !finalOrder) return
84
-
85
- const changed = !arraysEqual(finalOrder, baselineOrder)
86
- if (changed) {
87
- dispatchGlobal({ orderedIds: finalOrder, type: 'reorder-sessions' })
88
- return
89
- }
90
-
91
- const idx = baselineOrder.indexOf(source)
92
- if (idx >= 0) {
93
- runSideEffectGlobal({ index: idx + 1, type: 'switch-session-by-index' })
94
- }
95
- }, [baselineOrder, dragOrder, draggingId])
96
-
97
- const cancelDrag = useCallback(() => {
98
- setDraggingId(null)
99
- setDragOrder(null)
100
- lastSwapWithRef.current = null
101
- }, [])
102
-
103
- const handleNewSession = useCallback((e: OtuiMouseEvent) => {
104
- e.stopPropagation()
105
- dispatchGlobal({ returnToSessionPicker: false, type: 'open-create-session-modal' })
106
- }, [])
107
-
108
- if ((!bar.visible && !forceVisible) || ordered.length === 0) return null
109
-
110
- const visibleSessions =
111
- dragOrder !== null
112
- ? dragOrder
113
- .map((id) => ordered.find((s) => s.id === id))
114
- .filter((s): s is SessionRecord => !!s)
115
- : ordered
116
-
117
- return (
118
- <box
119
- width="100%"
120
- flexDirection="row"
121
- flexShrink={0}
122
- paddingLeft={1}
123
- paddingRight={1}
124
- backgroundColor={headerBg}
125
- >
126
- {visibleSessions.map((session) => (
127
- <SessionChip
128
- key={session.id}
129
- session={session}
130
- index={baselineOrder.indexOf(session.id) + 1}
131
- active={session.id === currentId}
132
- status={statusMap[session.id] ?? IDLE_SESSION_STATUS}
133
- dragging={draggingId === session.id}
134
- setChipRef={setChipRef}
135
- onDragStart={handleMouseDown}
136
- onDrag={handleMouseDrag}
137
- onDrop={commitDrop}
138
- onDragCancel={cancelDrag}
139
- />
140
- ))}
141
- <box flexGrow={1} />
142
- <box
143
- flexDirection="row"
144
- paddingLeft={1}
145
- paddingRight={1}
146
- backgroundColor={t.backgroundElement}
147
- onMouseDown={handleNewSession}
148
- >
149
- <text fg={t.text} selectable={false}>
150
- + New
151
- </text>
152
- </box>
153
- </box>
154
- )
155
- }
156
-
157
- function arraysEqual(a: string[], b: string[]): boolean {
158
- if (a.length !== b.length) return false
159
- for (let i = 0; i < a.length; i++) {
160
- if (a[i] !== b[i]) return false
161
- }
162
- return true
163
- }
164
-
165
- interface SessionChipProps {
166
- session: SessionRecord
167
- index: number
168
- active: boolean
169
- status: SessionStatus
170
- dragging: boolean
171
- setChipRef: (id: string, ref: BoxRenderable | null) => void
172
- onDragStart: (id: string) => void
173
- onDrag: (event: OtuiMouseEvent) => void
174
- onDrop: () => void
175
- onDragCancel: () => void
176
- }
177
-
178
- const SessionChip = memo(function SessionChip({
179
- active,
180
- dragging,
181
- index,
182
- onDrag,
183
- onDragCancel,
184
- onDragStart,
185
- onDrop,
186
- session,
187
- setChipRef,
188
- status,
189
- }: SessionChipProps) {
190
- const t = useTheme()
191
- const selectionBg = t.backgroundElement
192
- const showSpinner = status.working
193
- const showWaiting = status.waiting
194
- const spinner = useBusySpinner(showSpinner)
195
- const labelColor = active ? t.text : t.textMuted
196
- const bgColor = dragging || active ? selectionBg : undefined
197
- const idleColor = t.success
198
- const workingColor = t.primary
199
- const waitingColor = t.warning
200
- const [hovered, setHovered] = useState(false)
201
-
202
- const handleRef = useCallback(
203
- (r: BoxRenderable | null) => setChipRef(session.id, r),
204
- [setChipRef, session.id]
205
- )
206
- const handleMouseDown = useCallback(
207
- (e: OtuiMouseEvent) => {
208
- e.preventDefault()
209
- onDragStart(session.id)
210
- },
211
- [onDragStart, session.id]
212
- )
213
- const handleMouseUp = useCallback(
214
- (e: OtuiMouseEvent) => {
215
- e.preventDefault()
216
- onDrop()
217
- },
218
- [onDrop]
219
- )
220
- const handleMouseOver = useCallback(() => setHovered(true), [])
221
- const handleMouseOut = useCallback(() => setHovered(false), [])
222
- const handleDeleteMouseDown = useCallback(
223
- (event: OtuiMouseEvent) => {
224
- event.preventDefault()
225
- event.stopPropagation()
226
- runSideEffectGlobal({ sessionId: session.id, type: 'delete-session' })
227
- },
228
- [session.id]
229
- )
230
- const rightClickMenu = useMemo<[string, () => void][]>(
231
- () => [
232
- [
233
- 'Rename workspace',
234
- () =>
235
- dispatchGlobal({
236
- initialName: session.name,
237
- returnToSessionPicker: false,
238
- sessionTargetId: session.id,
239
- type: 'open-session-name-modal',
240
- }),
241
- ],
242
- [
243
- 'Delete workspace',
244
- () => runSideEffectGlobal({ sessionId: session.id, type: 'delete-session' }),
245
- ],
246
- ],
247
- [session.id, session.name]
248
- )
249
-
250
- return (
251
- <ContextMenuBox
252
- ref={handleRef}
253
- flexDirection="row"
254
- paddingLeft={1}
255
- paddingRight={1}
256
- backgroundColor={bgColor}
257
- rightClickMenu={rightClickMenu}
258
- onMouseOver={handleMouseOver}
259
- onMouseOut={handleMouseOut}
260
- onMouseDown={handleMouseDown}
261
- onMouseDrag={onDrag}
262
- onMouseUp={handleMouseUp}
263
- onMouseDragEnd={onDragCancel}
264
- >
265
- {showWaiting ? (
266
- <text fg={waitingColor} selectable={false}>
267
- ?{' '}
268
- </text>
269
- ) : null}
270
- {showSpinner ? (
271
- <text fg={workingColor} selectable={false}>
272
- {spinner}{' '}
273
- </text>
274
- ) : null}
275
- {!showWaiting && !showSpinner ? (
276
- <text fg={active ? workingColor : idleColor} selectable={false}>
277
- {'● '}
278
- </text>
279
- ) : null}
280
- <text fg={labelColor} selectable={false}>
281
- [{index}] {session.name}
282
- </text>
283
- {hovered ? (
284
- <box paddingLeft={1} onMouseDown={handleDeleteMouseDown}>
285
- <text fg={t.textMuted} selectable={false}>
286
- ×
287
- </text>
288
- </box>
289
- ) : (
290
- <text fg={t.textMuted} selectable={false}>
291
- {' '}
292
- </text>
293
- )}
294
- </ContextMenuBox>
295
- )
296
- })
@@ -1,44 +0,0 @@
1
- import type { TabSession } from '../../../../state/types'
2
-
3
- import { allLeafIds, type LayoutNode } from '../../../../state/layout-tree'
4
-
5
- export interface TabGroupInfo {
6
- inLayout: boolean
7
- groupStart: number
8
- groupEnd: number
9
- }
10
-
11
- export function buildTabGroupInfo(
12
- layoutTrees: Record<string, LayoutNode>,
13
- tabs: TabSession[]
14
- ): Map<string, TabGroupInfo> {
15
- const tabGroupInfo = new Map<string, TabGroupInfo>()
16
-
17
- for (const tree of Object.values(layoutTrees)) {
18
- const ids = allLeafIds(tree)
19
- if (ids.length <= 1) {
20
- continue
21
- }
22
-
23
- const idSet = new Set(ids)
24
- let groupStart = -1
25
- let groupEnd = -1
26
-
27
- for (const [index, tab] of tabs.entries()) {
28
- if (!idSet.has(tab.id)) {
29
- continue
30
- }
31
-
32
- if (groupStart === -1) {
33
- groupStart = index
34
- }
35
- groupEnd = index
36
- }
37
-
38
- for (const id of ids) {
39
- tabGroupInfo.set(id, { groupEnd, groupStart, inLayout: true })
40
- }
41
- }
42
-
43
- return tabGroupInfo
44
- }
@@ -1,33 +0,0 @@
1
- export type SidebarScrollTarget = 'none' | 'active-item' | 'top' | 'bottom'
2
-
3
- interface SidebarScrollTargetOptions {
4
- previousActiveIndex: number
5
- nextActiveIndex: number
6
- tabCount: number
7
- }
8
-
9
- export function getSidebarScrollTarget({
10
- nextActiveIndex,
11
- previousActiveIndex,
12
- tabCount,
13
- }: SidebarScrollTargetOptions): SidebarScrollTarget {
14
- if (tabCount === 0 || nextActiveIndex < 0) {
15
- return 'none'
16
- }
17
-
18
- if (previousActiveIndex < 0 || previousActiveIndex === nextActiveIndex) {
19
- return previousActiveIndex < 0 ? 'active-item' : 'none'
20
- }
21
-
22
- const lastIndex = tabCount - 1
23
-
24
- if (previousActiveIndex === lastIndex && nextActiveIndex === 0) {
25
- return 'top'
26
- }
27
-
28
- if (previousActiveIndex === 0 && nextActiveIndex === lastIndex) {
29
- return 'bottom'
30
- }
31
-
32
- return 'active-item'
33
- }
@@ -1,36 +0,0 @@
1
- import { useEffect, useState } from 'react'
2
-
3
- import { getCurrentBranch } from '../../../git-branch'
4
-
5
- const BRANCH_POLL_INTERVAL_MS = 5_000
6
-
7
- export function useSidebarBranch(projectPath: string | undefined): string | null {
8
- const [branch, setBranch] = useState<string | null>(null)
9
-
10
- useEffect(() => {
11
- if (!(projectPath != null && projectPath !== '')) {
12
- setBranch(null)
13
- return
14
- }
15
-
16
- let isCurrent = true
17
- const updateBranch = async () => {
18
- const nextBranch = await getCurrentBranch(projectPath)
19
- if (isCurrent) {
20
- setBranch(nextBranch)
21
- }
22
- }
23
-
24
- void updateBranch()
25
- const interval = setInterval(() => {
26
- void updateBranch()
27
- }, BRANCH_POLL_INTERVAL_MS)
28
-
29
- return () => {
30
- isCurrent = false
31
- clearInterval(interval)
32
- }
33
- }, [projectPath])
34
-
35
- return branch
36
- }