@brimveyn/aimux 1.14.0 → 1.14.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/package.json +2 -2
- package/src/app-runtime/side-effects.ts +145 -1
- package/src/app-runtime/split-drag-controller.ts +3 -1
- package/src/app-runtime/use-terminal-resize.ts +1 -4
- package/src/app.tsx +2 -3
- package/src/config.ts +1 -12
- package/src/git/worktree-branch-poller.ts +54 -0
- package/src/input/modes/types.ts +2 -0
- package/src/state/layout-tree.ts +114 -4
- package/src/state/reducers/session-state.ts +20 -0
- package/src/state/reducers/tab-state.ts +20 -2
- package/src/state/reducers/ui-state.ts +0 -3
- package/src/state/session-worktrees.ts +45 -2
- package/src/state/store.ts +0 -3
- package/src/state/tab-entries.ts +74 -0
- package/src/state/types.ts +1 -4
- package/src/state/workspace-save.ts +0 -1
- package/src/ui/components/layout/sidebar/sidebar.tsx +9 -340
- package/src/ui/components/layout/sidebar/tab-item.tsx +51 -42
- package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +10 -53
- package/src/ui/components/layout/sidebar/use-top-tab-bar-auto-scroll.ts +30 -0
- package/src/ui/components/layout/sidebar/workspace-list.tsx +398 -0
- package/src/ui/components/layout/sidebar/worktree-row.tsx +92 -0
- package/src/ui/components/layout/split-layout.tsx +20 -39
- package/src/ui/components/layout/status-bar.tsx +168 -33
- package/src/ui/components/layout/terminal-pane.tsx +30 -0
- package/src/ui/components/layout/top-tab-bar.tsx +304 -0
- package/src/ui/components/modals/worktree/worktree-move-modal.tsx +1 -8
- package/src/ui/components/overlays/ai-usage/ai-usage-indicator.tsx +22 -93
- package/src/ui/root.tsx +68 -63
- package/src/ui/status-bar-model.ts +36 -37
- package/src/ui/components/layout/session-bar.tsx +0 -296
- package/src/ui/components/layout/sidebar/sidebar-group-metadata.ts +0 -44
- package/src/ui/components/layout/sidebar/sidebar-scroll.ts +0 -33
- package/src/ui/components/layout/sidebar/use-sidebar-branch.ts +0 -36
|
@@ -0,0 +1,92 @@
|
|
|
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 { useTheme } from '../../../theme'
|
|
10
|
+
import { ContextMenuBox } from '../../overlays/context-menu/context-menu-box'
|
|
11
|
+
|
|
12
|
+
interface WorktreeRowProps {
|
|
13
|
+
session: SessionRecord
|
|
14
|
+
worktree: WorktreeRecord
|
|
15
|
+
/** 1-based index of the workspace in the visible order (for switch-session-by-index). */
|
|
16
|
+
sessionIndex: number
|
|
17
|
+
/** True when this row is the active cursor item. */
|
|
18
|
+
isActiveItem: boolean
|
|
19
|
+
/** True when this row's workspace is the current session (selection scope). */
|
|
20
|
+
inCurrentGroup: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const WorktreeRow = memo(function WorktreeRow({
|
|
24
|
+
inCurrentGroup,
|
|
25
|
+
isActiveItem,
|
|
26
|
+
session,
|
|
27
|
+
sessionIndex,
|
|
28
|
+
worktree,
|
|
29
|
+
}: WorktreeRowProps) {
|
|
30
|
+
const t = useTheme()
|
|
31
|
+
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
32
|
+
const isCurrentSession = session.id === currentSessionId
|
|
33
|
+
|
|
34
|
+
const handleMouseDown = useCallback(
|
|
35
|
+
(event: OtuiMouseEvent) => {
|
|
36
|
+
if (event.button !== 0) return
|
|
37
|
+
event.preventDefault()
|
|
38
|
+
event.stopPropagation()
|
|
39
|
+
dispatchGlobal({
|
|
40
|
+
sessionId: session.id,
|
|
41
|
+
type: 'set-active-worktree',
|
|
42
|
+
worktreeId: worktree.id,
|
|
43
|
+
})
|
|
44
|
+
if (!isCurrentSession) {
|
|
45
|
+
runSideEffectGlobal({ index: sessionIndex, type: 'switch-session-by-index' })
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
[isCurrentSession, session.id, sessionIndex, worktree.id]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const rightClickMenu = useMemo<[string, () => void][] | undefined>(() => {
|
|
52
|
+
if (worktree.source === 'primary') return
|
|
53
|
+
return [
|
|
54
|
+
[
|
|
55
|
+
'Remove worktree',
|
|
56
|
+
() =>
|
|
57
|
+
dispatchGlobal({
|
|
58
|
+
sessionId: session.id,
|
|
59
|
+
type: 'remove-worktree-record',
|
|
60
|
+
worktreeId: worktree.id,
|
|
61
|
+
}),
|
|
62
|
+
],
|
|
63
|
+
]
|
|
64
|
+
}, [session.id, worktree.id, worktree.source])
|
|
65
|
+
|
|
66
|
+
let bgColor: string | undefined
|
|
67
|
+
if (isActiveItem) {
|
|
68
|
+
bgColor = t.backgroundElement
|
|
69
|
+
} else if (inCurrentGroup) {
|
|
70
|
+
bgColor = t.backgroundPanel
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<ContextMenuBox
|
|
75
|
+
id={`sidebar-wt-${worktree.id}`}
|
|
76
|
+
flexDirection="row"
|
|
77
|
+
paddingLeft={1}
|
|
78
|
+
paddingRight={1}
|
|
79
|
+
alignItems="center"
|
|
80
|
+
backgroundColor={bgColor}
|
|
81
|
+
rightClickMenu={rightClickMenu}
|
|
82
|
+
onMouseDown={handleMouseDown}
|
|
83
|
+
>
|
|
84
|
+
<text fg={t.textMuted} selectable={false} wrapMode="none">
|
|
85
|
+
{' > '}
|
|
86
|
+
</text>
|
|
87
|
+
<text fg={isActiveItem ? t.text : t.textMuted} selectable={false} wrapMode="none">
|
|
88
|
+
{worktree.name}
|
|
89
|
+
</text>
|
|
90
|
+
</ContextMenuBox>
|
|
91
|
+
)
|
|
92
|
+
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useMemo } from 'react'
|
|
4
4
|
|
|
5
5
|
import type { MeasuredPaneRect } from '../../../app-runtime/use-pane-size-report'
|
|
6
6
|
import type { TerminalContentOrigin } from '../../../input/raw-input-handler'
|
|
@@ -8,13 +8,14 @@ import type { FocusMode, TabSession } from '../../../state/types'
|
|
|
8
8
|
|
|
9
9
|
import { logInputDebug } from '../../../debug/input-log'
|
|
10
10
|
import {
|
|
11
|
+
computeJunctionEdges,
|
|
11
12
|
computePaneRects,
|
|
13
|
+
type JunctionEdges,
|
|
12
14
|
type LayoutNode,
|
|
13
15
|
PANE_BORDER,
|
|
14
16
|
type PaneRect,
|
|
15
17
|
type SplitDirection,
|
|
16
18
|
} from '../../../state/layout-tree'
|
|
17
|
-
import { useTheme } from '../../theme'
|
|
18
19
|
import { TerminalPane } from './terminal-pane'
|
|
19
20
|
|
|
20
21
|
const PANE_CHROME = PANE_BORDER
|
|
@@ -45,6 +46,7 @@ interface SplitLayoutProps {
|
|
|
45
46
|
onMeasure?: (tabId: string, rect: MeasuredPaneRect) => void
|
|
46
47
|
contentOrigin: TerminalContentOrigin
|
|
47
48
|
bounds: PaneRect
|
|
49
|
+
junctionEdgesMap?: Map<string, JunctionEdges>
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
export function SplitLayout({
|
|
@@ -52,6 +54,7 @@ export function SplitLayout({
|
|
|
52
54
|
bounds,
|
|
53
55
|
contentOrigin,
|
|
54
56
|
focusMode,
|
|
57
|
+
junctionEdgesMap: providedJunctionEdgesMap,
|
|
55
58
|
localScrollbackEnabled,
|
|
56
59
|
mouseForwardingEnabled,
|
|
57
60
|
node,
|
|
@@ -69,7 +72,12 @@ export function SplitLayout({
|
|
|
69
72
|
onTerminalScrollEvent,
|
|
70
73
|
tabs,
|
|
71
74
|
}: SplitLayoutProps) {
|
|
72
|
-
const
|
|
75
|
+
const junctionEdgesMap = useMemo(
|
|
76
|
+
() =>
|
|
77
|
+
providedJunctionEdgesMap ??
|
|
78
|
+
computeJunctionEdges(node, bounds, { x: contentOrigin.x, y: contentOrigin.y }),
|
|
79
|
+
[providedJunctionEdgesMap, node, bounds, contentOrigin]
|
|
80
|
+
)
|
|
73
81
|
const paneOrigin = useMemo<TerminalContentOrigin>(
|
|
74
82
|
() => ({
|
|
75
83
|
cols: Math.max(1, bounds.cols - PANE_CHROME * 2),
|
|
@@ -79,22 +87,6 @@ export function SplitLayout({
|
|
|
79
87
|
}),
|
|
80
88
|
[bounds, contentOrigin]
|
|
81
89
|
)
|
|
82
|
-
const handleSeparatorMouseDown = useCallback(
|
|
83
|
-
(e: OtuiMouseEvent) => {
|
|
84
|
-
e.preventDefault()
|
|
85
|
-
if (node.type !== 'split' || !onSeparatorDragStart) return
|
|
86
|
-
const leafId = getFirstLeafId(node.first)
|
|
87
|
-
if (!(leafId != null && leafId !== '')) return
|
|
88
|
-
onSeparatorDragStart({
|
|
89
|
-
direction: node.direction,
|
|
90
|
-
screenStart:
|
|
91
|
-
node.direction === 'vertical' ? contentOrigin.x + bounds.x : contentOrigin.y + bounds.y,
|
|
92
|
-
tabId: leafId,
|
|
93
|
-
totalSize: node.direction === 'vertical' ? bounds.cols : bounds.rows,
|
|
94
|
-
})
|
|
95
|
-
},
|
|
96
|
-
[bounds, contentOrigin, node, onSeparatorDragStart]
|
|
97
|
-
)
|
|
98
90
|
if (node.type === 'leaf') {
|
|
99
91
|
const tab = tabs.find((t) => t.id === node.tabId)
|
|
100
92
|
const isActive = node.tabId === activeTabId
|
|
@@ -117,6 +109,7 @@ export function SplitLayout({
|
|
|
117
109
|
focusMode={focusMode}
|
|
118
110
|
isActive={isActive}
|
|
119
111
|
contentOrigin={paneOrigin}
|
|
112
|
+
junctionEdges={junctionEdgesMap.get(node.tabId)}
|
|
120
113
|
mouseForwardingEnabled={isActive && mouseForwardingEnabled}
|
|
121
114
|
localScrollbackEnabled={isActive && localScrollbackEnabled}
|
|
122
115
|
onTerminalMouseEvent={onTerminalMouseEvent}
|
|
@@ -127,6 +120,7 @@ export function SplitLayout({
|
|
|
127
120
|
onPaneActivate={onPaneActivate}
|
|
128
121
|
onSeparatorDrag={onSeparatorDrag}
|
|
129
122
|
onSeparatorDragEnd={onSeparatorDragEnd}
|
|
123
|
+
onSeparatorDragStart={onSeparatorDragStart}
|
|
130
124
|
onLeftEdgeMouseDown={onLeftEdgeMouseDown}
|
|
131
125
|
onMeasure={onMeasure}
|
|
132
126
|
/>
|
|
@@ -134,23 +128,19 @@ export function SplitLayout({
|
|
|
134
128
|
}
|
|
135
129
|
|
|
136
130
|
const flexDir = node.direction === 'vertical' ? 'row' : 'column'
|
|
137
|
-
const firstGrow = Math.round(node.ratio * 100)
|
|
138
|
-
const secondGrow = 100 - firstGrow
|
|
139
131
|
|
|
140
|
-
// Compute sub-bounds for each child
|
|
141
132
|
const rects = computePaneRects(node, bounds)
|
|
142
|
-
|
|
143
|
-
// Compute the bounding rect for each subtree
|
|
144
133
|
const firstBounds = subtreeBounds(node.first, rects, bounds)
|
|
145
134
|
const secondBounds = subtreeBounds(node.second, rects, bounds)
|
|
146
135
|
|
|
147
|
-
|
|
148
|
-
|
|
136
|
+
const firstSize = node.direction === 'vertical' ? firstBounds.cols : firstBounds.rows
|
|
137
|
+
const firstSizeProp = node.direction === 'vertical' ? { width: firstSize } : { height: firstSize }
|
|
138
|
+
|
|
149
139
|
const secondLeftEdgeMouseDown = node.direction === 'horizontal' ? onLeftEdgeMouseDown : undefined
|
|
150
140
|
|
|
151
141
|
return (
|
|
152
142
|
<box flexDirection={flexDir} flexGrow={1} gap={0}>
|
|
153
|
-
<box
|
|
143
|
+
<box {...firstSizeProp} flexShrink={0} flexDirection="column" overflow="hidden">
|
|
154
144
|
<SplitLayout
|
|
155
145
|
node={node.first}
|
|
156
146
|
tabs={tabs}
|
|
@@ -172,15 +162,10 @@ export function SplitLayout({
|
|
|
172
162
|
onMeasure={onMeasure}
|
|
173
163
|
contentOrigin={contentOrigin}
|
|
174
164
|
bounds={firstBounds}
|
|
165
|
+
junctionEdgesMap={junctionEdgesMap}
|
|
175
166
|
/>
|
|
176
167
|
</box>
|
|
177
|
-
<box
|
|
178
|
-
minWidth={node.direction === 'vertical' ? 1 : undefined}
|
|
179
|
-
minHeight={node.direction === 'horizontal' ? 1 : undefined}
|
|
180
|
-
backgroundColor={t.border}
|
|
181
|
-
onMouseDown={handleSeparatorMouseDown}
|
|
182
|
-
/>
|
|
183
|
-
<box flexGrow={secondGrow} flexDirection="column" overflow="hidden">
|
|
168
|
+
<box flexGrow={1} flexDirection="column" overflow="hidden">
|
|
184
169
|
<SplitLayout
|
|
185
170
|
node={node.second}
|
|
186
171
|
tabs={tabs}
|
|
@@ -202,17 +187,13 @@ export function SplitLayout({
|
|
|
202
187
|
onMeasure={onMeasure}
|
|
203
188
|
contentOrigin={contentOrigin}
|
|
204
189
|
bounds={secondBounds}
|
|
190
|
+
junctionEdgesMap={junctionEdgesMap}
|
|
205
191
|
/>
|
|
206
192
|
</box>
|
|
207
193
|
</box>
|
|
208
194
|
)
|
|
209
195
|
}
|
|
210
196
|
|
|
211
|
-
function getFirstLeafId(node: LayoutNode): string | null {
|
|
212
|
-
if (node.type === 'leaf') return node.tabId
|
|
213
|
-
return getFirstLeafId(node.first)
|
|
214
|
-
}
|
|
215
|
-
|
|
216
197
|
function subtreeBounds(
|
|
217
198
|
node: LayoutNode,
|
|
218
199
|
rects: Map<string, PaneRect>,
|
|
@@ -1,76 +1,211 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getStatusBarSeparator,
|
|
3
|
+
type ResolvedTuiTheme,
|
|
4
|
+
type StatusBarSeparator,
|
|
5
|
+
} from '@brimveyn/aimux-config'
|
|
6
|
+
|
|
1
7
|
import type { AppState } from '../../../state/types'
|
|
2
8
|
|
|
3
9
|
import { version as APP_VERSION } from '../../../../package.json'
|
|
10
|
+
import { useAIUsageStore } from '../../../state/ai-usage-store'
|
|
4
11
|
import { useAppStore } from '../../../state/app-store'
|
|
5
12
|
import { useKeymap } from '../../keymap-context'
|
|
6
|
-
import { getStatusBarModel } from '../../status-bar-model'
|
|
7
|
-
import {
|
|
13
|
+
import { getStatusBarModel, type IdentitySegment } from '../../status-bar-model'
|
|
14
|
+
import { useTheme } from '../../theme'
|
|
8
15
|
import { AIUsageIndicator } from '../overlays/ai-usage/ai-usage-indicator'
|
|
9
16
|
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
// Powerline-style separator glyph pairs.
|
|
18
|
+
// `right` is rendered between left-anchored tiles (A→B, B→filler).
|
|
19
|
+
// `left` is rendered between right-anchored tiles (filler→Y, X→Y).
|
|
20
|
+
// `none` uses empty strings so bare background transitions remain visible.
|
|
21
|
+
const SEPARATOR_GLYPHS: Record<StatusBarSeparator, { left: string; right: string }> = {
|
|
22
|
+
arrow: { left: '\u{E0B2}', right: '\u{E0B0}' },
|
|
23
|
+
flame: { left: '\u{E0C2}', right: '\u{E0C0}' },
|
|
24
|
+
none: { left: '', right: '' },
|
|
25
|
+
round: { left: '\u{E0B6}', right: '\u{E0B4}' },
|
|
26
|
+
slant: { left: '\u{E0BA}', right: '\u{E0BC}' },
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Mode label is padded to a fixed width so the A tile never resizes
|
|
30
|
+
// when switching modes — keeps the rest of the bar visually stable.
|
|
31
|
+
const MODE_LABEL_WIDTH = 6
|
|
32
|
+
// A tile width = padded label (6) + paddingLeft (1) + paddingRight (1).
|
|
33
|
+
// Row 2 indents past A + separator glyph + B's paddingLeft so the
|
|
34
|
+
// hints line up with the start of B's content.
|
|
35
|
+
const ROW2_INDENT = MODE_LABEL_WIDTH + 2 + 1 + 1
|
|
36
|
+
|
|
37
|
+
function getModeLabel(focusMode: AppState['focusMode']): string {
|
|
12
38
|
switch (focusMode) {
|
|
13
39
|
case 'terminal-input':
|
|
14
|
-
return
|
|
40
|
+
return 'INSERT'
|
|
15
41
|
case 'modal':
|
|
42
|
+
return 'MODAL'
|
|
16
43
|
case 'command-edit':
|
|
17
|
-
return
|
|
44
|
+
return 'EDIT'
|
|
18
45
|
case 'git':
|
|
19
|
-
return
|
|
46
|
+
return 'GIT'
|
|
20
47
|
case 'navigation':
|
|
21
48
|
default:
|
|
22
|
-
return
|
|
49
|
+
return 'NORMAL'
|
|
23
50
|
}
|
|
24
51
|
}
|
|
25
52
|
|
|
26
|
-
function
|
|
53
|
+
function getModeBadge(focusMode: AppState['focusMode']): string {
|
|
54
|
+
const label = getModeLabel(focusMode)
|
|
55
|
+
const totalPad = Math.max(0, MODE_LABEL_WIDTH - label.length)
|
|
56
|
+
const left = Math.floor(totalPad / 2)
|
|
57
|
+
const right = totalPad - left
|
|
58
|
+
return ' '.repeat(left) + label + ' '.repeat(right)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getModeColor(focusMode: AppState['focusMode'], t: ResolvedTuiTheme): string {
|
|
27
62
|
switch (focusMode) {
|
|
28
63
|
case 'terminal-input':
|
|
29
|
-
return
|
|
64
|
+
return t.primary
|
|
30
65
|
case 'modal':
|
|
31
|
-
return 'modal'
|
|
32
66
|
case 'command-edit':
|
|
33
|
-
return
|
|
67
|
+
return t.warning
|
|
34
68
|
case 'git':
|
|
35
|
-
return
|
|
69
|
+
return t.success
|
|
36
70
|
case 'navigation':
|
|
37
71
|
default:
|
|
38
|
-
return
|
|
72
|
+
return t.text
|
|
39
73
|
}
|
|
40
74
|
}
|
|
41
75
|
|
|
76
|
+
function composeAmbient(right: string, help: string): string {
|
|
77
|
+
if (right === '' && help === '') return ''
|
|
78
|
+
if (right === '') return help
|
|
79
|
+
if (help === '') return right
|
|
80
|
+
return `${right} · ${help}`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function Segments({ segments, t }: { segments: IdentitySegment[]; t: ResolvedTuiTheme }) {
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
{segments.map((seg) => (
|
|
87
|
+
<text
|
|
88
|
+
key={seg.id}
|
|
89
|
+
fg={seg.tone === 'primary' ? t.text : t.textMuted}
|
|
90
|
+
wrapMode="none"
|
|
91
|
+
selectable={false}
|
|
92
|
+
>
|
|
93
|
+
{seg.text}
|
|
94
|
+
</text>
|
|
95
|
+
))}
|
|
96
|
+
</>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function Separator({ bg, fg, glyph }: { bg: string; fg: string; glyph: string }) {
|
|
101
|
+
if (glyph === '') return null
|
|
102
|
+
return (
|
|
103
|
+
<box backgroundColor={bg}>
|
|
104
|
+
<text fg={fg} selectable={false}>
|
|
105
|
+
{glyph}
|
|
106
|
+
</text>
|
|
107
|
+
</box>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
42
111
|
export function StatusBar() {
|
|
43
112
|
const t = useTheme()
|
|
44
|
-
const headerBg = t.backgroundPanel
|
|
45
113
|
const state = useAppStore((s) => s)
|
|
46
|
-
const activeTab = state.tabs.find((tab) => tab.id === state.activeTabId)
|
|
47
114
|
const config = useKeymap()
|
|
48
|
-
const model = getStatusBarModel(state,
|
|
115
|
+
const model = getStatusBarModel(state, config)
|
|
116
|
+
const modeColor = getModeColor(state.focusMode, t)
|
|
117
|
+
const ambient = composeAmbient(model.right, model.help)
|
|
118
|
+
const aiEnabled = useAIUsageStore((s) => s.enabled)
|
|
119
|
+
|
|
120
|
+
const glyphs = SEPARATOR_GLYPHS[getStatusBarSeparator()]
|
|
121
|
+
|
|
122
|
+
const tileB = t.backgroundElement
|
|
123
|
+
const tileFiller = t.backgroundPanel
|
|
124
|
+
const tileX = t.backgroundElement
|
|
125
|
+
const tileY = modeColor
|
|
126
|
+
|
|
127
|
+
const hasB = model.sessionSegments.length > 0
|
|
128
|
+
const hasX = aiEnabled
|
|
49
129
|
|
|
50
130
|
return (
|
|
51
131
|
<box
|
|
52
132
|
height={2}
|
|
53
133
|
flexShrink={0}
|
|
54
134
|
overflow="hidden"
|
|
55
|
-
paddingLeft={1}
|
|
56
|
-
paddingRight={1}
|
|
57
|
-
paddingTop={0}
|
|
58
|
-
paddingBottom={0}
|
|
59
135
|
flexDirection="column"
|
|
60
|
-
backgroundColor={
|
|
136
|
+
backgroundColor={tileFiller}
|
|
61
137
|
>
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<box flexDirection="row" gap={2}>
|
|
70
|
-
{model.help ? <text fg={t.textMuted}>{model.help}</text> : null}
|
|
71
|
-
<AIUsageIndicator />
|
|
72
|
-
<text fg={t.textMuted}>v{APP_VERSION}</text>
|
|
138
|
+
{/* Row 1 — lualine tiles */}
|
|
139
|
+
<box height={1} flexShrink={0} flexDirection="row" overflow="hidden">
|
|
140
|
+
{/* A: mode */}
|
|
141
|
+
<box backgroundColor={modeColor} paddingLeft={1} paddingRight={1}>
|
|
142
|
+
<text fg={t.background} selectable={false}>
|
|
143
|
+
{getModeBadge(state.focusMode)}
|
|
144
|
+
</text>
|
|
73
145
|
</box>
|
|
146
|
+
|
|
147
|
+
{/* A → B (or A → filler if B empty) */}
|
|
148
|
+
<Separator glyph={glyphs.right} bg={hasB ? tileB : tileFiller} fg={modeColor} />
|
|
149
|
+
|
|
150
|
+
{hasB ? (
|
|
151
|
+
<>
|
|
152
|
+
<box
|
|
153
|
+
backgroundColor={tileB}
|
|
154
|
+
paddingLeft={1}
|
|
155
|
+
paddingRight={1}
|
|
156
|
+
flexDirection="row"
|
|
157
|
+
flexShrink={1}
|
|
158
|
+
overflow="hidden"
|
|
159
|
+
>
|
|
160
|
+
<Segments segments={model.sessionSegments} t={t} />
|
|
161
|
+
</box>
|
|
162
|
+
<Separator glyph={glyphs.right} bg={tileFiller} fg={tileB} />
|
|
163
|
+
</>
|
|
164
|
+
) : null}
|
|
165
|
+
|
|
166
|
+
{/* Filler */}
|
|
167
|
+
<box flexGrow={1} flexShrink={1} backgroundColor={tileFiller} />
|
|
168
|
+
|
|
169
|
+
{hasX ? (
|
|
170
|
+
<>
|
|
171
|
+
<Separator glyph={glyphs.left} bg={tileFiller} fg={tileX} />
|
|
172
|
+
<box
|
|
173
|
+
backgroundColor={tileX}
|
|
174
|
+
paddingLeft={1}
|
|
175
|
+
paddingRight={1}
|
|
176
|
+
flexDirection="row"
|
|
177
|
+
flexShrink={0}
|
|
178
|
+
>
|
|
179
|
+
<AIUsageIndicator />
|
|
180
|
+
</box>
|
|
181
|
+
<Separator glyph={glyphs.left} bg={tileX} fg={tileY} />
|
|
182
|
+
</>
|
|
183
|
+
) : (
|
|
184
|
+
<Separator glyph={glyphs.left} bg={tileFiller} fg={tileY} />
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
{/* Y: version */}
|
|
188
|
+
<box backgroundColor={tileY} paddingLeft={1} paddingRight={1} flexShrink={0}>
|
|
189
|
+
<text fg={t.background} selectable={false}>
|
|
190
|
+
v{APP_VERSION}
|
|
191
|
+
</text>
|
|
192
|
+
</box>
|
|
193
|
+
</box>
|
|
194
|
+
|
|
195
|
+
{/* Row 2 — ambient hints */}
|
|
196
|
+
<box
|
|
197
|
+
height={1}
|
|
198
|
+
flexShrink={0}
|
|
199
|
+
flexDirection="row"
|
|
200
|
+
paddingLeft={ROW2_INDENT}
|
|
201
|
+
paddingRight={1}
|
|
202
|
+
overflow="hidden"
|
|
203
|
+
>
|
|
204
|
+
{ambient !== '' ? (
|
|
205
|
+
<text fg={t.textMuted} wrapMode="none" selectable={false}>
|
|
206
|
+
{ambient}
|
|
207
|
+
</text>
|
|
208
|
+
) : null}
|
|
74
209
|
</box>
|
|
75
210
|
</box>
|
|
76
211
|
)
|
|
@@ -3,6 +3,7 @@ import type { MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
|
3
3
|
import { memo, type ReactNode, useCallback, useMemo } from 'react'
|
|
4
4
|
|
|
5
5
|
import type { TerminalContentOrigin } from '../../../input/raw-input-handler'
|
|
6
|
+
import type { JunctionEdgeInfo, JunctionEdges } from '../../../state/layout-tree'
|
|
6
7
|
import type { FocusMode, TabSession, TerminalSnapshot, TerminalSpan } from '../../../state/types'
|
|
7
8
|
|
|
8
9
|
import { type MeasuredPaneRect, usePaneSizeReport } from '../../../app-runtime/use-pane-size-report'
|
|
@@ -28,8 +29,10 @@ interface TerminalPaneProps {
|
|
|
28
29
|
onPaneActivate?: (tabId: string) => void
|
|
29
30
|
onSeparatorDrag?: (event: OtuiMouseEvent) => boolean
|
|
30
31
|
onSeparatorDragEnd?: () => void
|
|
32
|
+
onSeparatorDragStart?: (info: JunctionEdgeInfo) => void
|
|
31
33
|
onLeftEdgeMouseDown?: (event: OtuiMouseEvent) => boolean
|
|
32
34
|
onMeasure?: (tabId: string, rect: MeasuredPaneRect) => void
|
|
35
|
+
junctionEdges?: JunctionEdges
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
function getTitle(
|
|
@@ -113,6 +116,7 @@ export function TerminalPane({
|
|
|
113
116
|
contentOrigin,
|
|
114
117
|
focusMode,
|
|
115
118
|
isActive,
|
|
119
|
+
junctionEdges,
|
|
116
120
|
localScrollbackEnabled,
|
|
117
121
|
mouseForwardingEnabled,
|
|
118
122
|
onLeftEdgeMouseDown,
|
|
@@ -120,6 +124,7 @@ export function TerminalPane({
|
|
|
120
124
|
onPaneActivate,
|
|
121
125
|
onSeparatorDrag,
|
|
122
126
|
onSeparatorDragEnd,
|
|
127
|
+
onSeparatorDragStart,
|
|
123
128
|
onTerminalClick,
|
|
124
129
|
onTerminalDrag,
|
|
125
130
|
onTerminalMouseEvent,
|
|
@@ -175,6 +180,16 @@ export function TerminalPane({
|
|
|
175
180
|
event.y === contentOrigin.y + contentOrigin.rows,
|
|
176
181
|
[contentOrigin]
|
|
177
182
|
)
|
|
183
|
+
const getJunctionSideAt = useCallback(
|
|
184
|
+
(event: OtuiMouseEvent): keyof JunctionEdges | null => {
|
|
185
|
+
if (event.x === contentOrigin.x - 1) return 'left'
|
|
186
|
+
if (event.x === contentOrigin.x + contentOrigin.cols) return 'right'
|
|
187
|
+
if (event.y === contentOrigin.y - 1) return 'top'
|
|
188
|
+
if (event.y === contentOrigin.y + contentOrigin.rows) return 'bottom'
|
|
189
|
+
return null
|
|
190
|
+
},
|
|
191
|
+
[contentOrigin]
|
|
192
|
+
)
|
|
178
193
|
const forwardMouseEvent = useCallback(
|
|
179
194
|
(event: OtuiMouseEvent) => {
|
|
180
195
|
if (event.type === 'down' && event.button === 2 && rightClickMenu) {
|
|
@@ -195,6 +210,18 @@ export function TerminalPane({
|
|
|
195
210
|
return
|
|
196
211
|
}
|
|
197
212
|
}
|
|
213
|
+
if (event.type === 'down' && event.button === 0 && junctionEdges && onSeparatorDragStart) {
|
|
214
|
+
const side = getJunctionSideAt(event)
|
|
215
|
+
if (side !== null) {
|
|
216
|
+
const info = junctionEdges[side]
|
|
217
|
+
if (info) {
|
|
218
|
+
event.preventDefault()
|
|
219
|
+
event.stopPropagation()
|
|
220
|
+
onSeparatorDragStart(info)
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
198
225
|
// Absorb left-button clicks on pane borders — they should not focus the
|
|
199
226
|
// pane or be forwarded to the terminal. This keeps borders available for
|
|
200
227
|
// resize actions (split separators, sidebar edge) without conflicting
|
|
@@ -248,11 +275,14 @@ export function TerminalPane({
|
|
|
248
275
|
[
|
|
249
276
|
canForwardMouse,
|
|
250
277
|
contentOrigin,
|
|
278
|
+
getJunctionSideAt,
|
|
251
279
|
isOnPaneBorder,
|
|
280
|
+
junctionEdges,
|
|
252
281
|
onLeftEdgeMouseDown,
|
|
253
282
|
onPaneActivate,
|
|
254
283
|
onSeparatorDrag,
|
|
255
284
|
onSeparatorDragEnd,
|
|
285
|
+
onSeparatorDragStart,
|
|
256
286
|
onTerminalClick,
|
|
257
287
|
onTerminalDrag,
|
|
258
288
|
onTerminalMouseEvent,
|