@brimveyn/aimux 1.9.9 → 1.10.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.
- package/package.json +2 -2
- package/src/app-runtime/backend-runtime-events.ts +8 -1
- package/src/app-runtime/click-selection-resolver.ts +90 -56
- package/src/app-runtime/side-effects.ts +7 -1
- package/src/app-runtime/use-backend-runtime.ts +12 -1
- package/src/app-runtime/use-mouse-handlers.ts +299 -6
- package/src/app.tsx +56 -1
- package/src/config.ts +10 -1
- package/src/daemon/daemon.ts +26 -0
- package/src/daemon/session-manager.ts +13 -0
- package/src/daemon/session-registry.ts +8 -0
- package/src/index.tsx +14 -6
- package/src/input/modes/types.ts +1 -0
- package/src/integrations/claude-syntax-overlay.ts +460 -0
- package/src/integrations/claude-theme-sync.ts +118 -0
- package/src/ipc/manager-protocol.ts +14 -1
- package/src/pty/pty-manager.ts +37 -1
- package/src/terminal-manager/manager-client.ts +25 -0
- package/src/terminal-manager/terminal-manager.ts +54 -0
- package/src/ui/components/layout/split-layout.tsx +10 -0
- package/src/ui/components/layout/terminal-pane.tsx +19 -5
- package/src/ui/components/modals/themes/theme-picker-modal.tsx +3 -1
- package/src/ui/root.tsx +13 -1
- package/src/ui/theme-store.ts +18 -0
- package/src/ui/theme.ts +2 -0
- package/src/update.ts +21 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brimveyn/aimux",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.1",
|
|
4
4
|
"description": "A terminal multiplexer for AI CLIs. Run Claude, Codex, OpenCode side-by-side with tabbed navigation, split panes, and persistent sessions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"bump": "bun run scripts/bump.ts"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@brimveyn/aimux-config": "0.5.
|
|
63
|
+
"@brimveyn/aimux-config": "0.5.9",
|
|
64
64
|
"@opentui/core": "^0.1.90",
|
|
65
65
|
"@opentui/react": "^0.1.90",
|
|
66
66
|
"@xterm/headless": "^6.0.0",
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
} from '../state/types'
|
|
11
11
|
|
|
12
12
|
import { logInputDebug } from '../debug/input-log'
|
|
13
|
+
import { clearTabSyntaxState, highlightSnapshot } from '../integrations/claude-syntax-overlay'
|
|
13
14
|
import { type TabRuntimeTimeouts } from './tab-runtime-timeouts'
|
|
14
15
|
|
|
15
16
|
interface BindBackendRuntimeEventsOptions {
|
|
@@ -17,6 +18,7 @@ interface BindBackendRuntimeEventsOptions {
|
|
|
17
18
|
dispatch: (action: AppAction) => void
|
|
18
19
|
resizingRef: MutableRefObject<boolean>
|
|
19
20
|
timeouts: Pick<TabRuntimeTimeouts, 'clearIdleTimer' | 'clearStartupGrace' | 'clearAllTimers'>
|
|
21
|
+
syntaxOverlayEnabled: () => boolean
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
function clearTabRuntimeState(
|
|
@@ -31,6 +33,7 @@ export function bindBackendRuntimeEvents({
|
|
|
31
33
|
backend,
|
|
32
34
|
dispatch,
|
|
33
35
|
resizingRef,
|
|
36
|
+
syntaxOverlayEnabled,
|
|
34
37
|
timeouts,
|
|
35
38
|
}: BindBackendRuntimeEventsOptions): () => void {
|
|
36
39
|
const handleRender = (
|
|
@@ -48,12 +51,14 @@ export function bindBackendRuntimeEvents({
|
|
|
48
51
|
viewportY: viewport.viewportY,
|
|
49
52
|
})
|
|
50
53
|
|
|
54
|
+
const transformed = syntaxOverlayEnabled() ? highlightSnapshot(viewport, tabId) : viewport
|
|
55
|
+
|
|
51
56
|
dispatch({
|
|
52
57
|
source: resizingRef.current ? 'resize' : 'data',
|
|
53
58
|
tabId,
|
|
54
59
|
terminalModes,
|
|
55
60
|
type: 'replace-tab-viewport',
|
|
56
|
-
viewport,
|
|
61
|
+
viewport: transformed,
|
|
57
62
|
})
|
|
58
63
|
// Per-tab activity is driven by the backend's status-detection loop via
|
|
59
64
|
// the `tabActivity` event — no client-side idle timer needed.
|
|
@@ -62,12 +67,14 @@ export function bindBackendRuntimeEvents({
|
|
|
62
67
|
const handleExit = (tabId: string, exitCode: number) => {
|
|
63
68
|
logInputDebug('app.backend.event.exit', { exitCode, tabId })
|
|
64
69
|
clearTabRuntimeState(timeouts, tabId)
|
|
70
|
+
clearTabSyntaxState(tabId)
|
|
65
71
|
dispatch({ tabId, type: 'close-tab' })
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
const handleError = (tabId: string, message: string) => {
|
|
69
75
|
logInputDebug('app.backend.event.error', { message, tabId })
|
|
70
76
|
clearTabRuntimeState(timeouts, tabId)
|
|
77
|
+
clearTabSyntaxState(tabId)
|
|
71
78
|
dispatch({ message, tabId, type: 'set-tab-error' })
|
|
72
79
|
}
|
|
73
80
|
|
|
@@ -13,6 +13,8 @@ interface PositionedNode {
|
|
|
13
13
|
y: number
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
export type MultiClickMode = 'word' | 'line'
|
|
17
|
+
|
|
16
18
|
export interface ClickSelectionResult {
|
|
17
19
|
selectedText: string
|
|
18
20
|
startCol: number
|
|
@@ -20,6 +22,22 @@ export interface ClickSelectionResult {
|
|
|
20
22
|
baseX: number
|
|
21
23
|
eventY: number
|
|
22
24
|
target: unknown
|
|
25
|
+
row: number
|
|
26
|
+
mode: MultiClickMode
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ViewportAnchor {
|
|
30
|
+
target: PositionedNode
|
|
31
|
+
baseX: number
|
|
32
|
+
baseY: number
|
|
33
|
+
col: number
|
|
34
|
+
row: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface MultiClickRange {
|
|
38
|
+
startCol: number
|
|
39
|
+
endCol: number
|
|
40
|
+
lineLength: number
|
|
23
41
|
}
|
|
24
42
|
|
|
25
43
|
export function isPositionedNode(value: unknown): value is PositionedNode {
|
|
@@ -31,100 +49,116 @@ export function isPositionedNode(value: unknown): value is PositionedNode {
|
|
|
31
49
|
)
|
|
32
50
|
}
|
|
33
51
|
|
|
52
|
+
export function getViewportAnchor(event: OtuiMouseEvent): ViewportAnchor | null {
|
|
53
|
+
if (!event.target || !isPositionedNode(event.target)) return null
|
|
54
|
+
const baseX = event.target.x
|
|
55
|
+
const baseY = event.target.y
|
|
56
|
+
return {
|
|
57
|
+
baseX,
|
|
58
|
+
baseY,
|
|
59
|
+
col: event.x - baseX,
|
|
60
|
+
row: event.y - baseY,
|
|
61
|
+
target: event.target,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function computeRangeFromLineText(
|
|
66
|
+
lineText: string,
|
|
67
|
+
col: number,
|
|
68
|
+
mode: MultiClickMode
|
|
69
|
+
): MultiClickRange | null {
|
|
70
|
+
if (mode === 'line') {
|
|
71
|
+
return { endCol: lineText.length, lineLength: lineText.length, startCol: 0 }
|
|
72
|
+
}
|
|
73
|
+
const word = getWordAtColumn(lineText, col)
|
|
74
|
+
if (word.text.length === 0) return null
|
|
75
|
+
return { endCol: word.endCol, lineLength: lineText.length, startCol: word.startCol }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function computeMultiClickRange(
|
|
79
|
+
tab: TabSession | undefined,
|
|
80
|
+
row: number,
|
|
81
|
+
col: number,
|
|
82
|
+
mode: MultiClickMode
|
|
83
|
+
): MultiClickRange | null {
|
|
84
|
+
const line = tab?.viewport?.lines[row]
|
|
85
|
+
if (!line) return null
|
|
86
|
+
return computeRangeFromLineText(getLineText(line), col, mode)
|
|
87
|
+
}
|
|
88
|
+
|
|
34
89
|
export function resolveClickSelection(
|
|
35
90
|
event: OtuiMouseEvent,
|
|
36
91
|
targetTabId: string,
|
|
37
92
|
tab: TabSession | undefined,
|
|
38
93
|
clickCount: number
|
|
39
94
|
): ClickSelectionResult | null {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const viewportText = event.target
|
|
45
|
-
if (!isPositionedNode(viewportText)) {
|
|
46
|
-
return null
|
|
47
|
-
}
|
|
95
|
+
const anchor = getViewportAnchor(event)
|
|
96
|
+
if (!anchor) return null
|
|
48
97
|
|
|
49
|
-
const
|
|
50
|
-
const row = event.y - viewportText.y
|
|
51
|
-
const baseX = viewportText.x
|
|
98
|
+
const mode: MultiClickMode = clickCount === 2 ? 'word' : 'line'
|
|
52
99
|
|
|
53
100
|
logInputDebug('click.detect', {
|
|
54
101
|
clickCount,
|
|
55
|
-
col,
|
|
102
|
+
col: anchor.col,
|
|
56
103
|
eventX: event.x,
|
|
57
104
|
eventY: event.y,
|
|
58
|
-
row,
|
|
59
|
-
targetId:
|
|
60
|
-
viewportX:
|
|
61
|
-
viewportY:
|
|
105
|
+
row: anchor.row,
|
|
106
|
+
targetId: anchor.target.id,
|
|
107
|
+
viewportX: anchor.baseX,
|
|
108
|
+
viewportY: anchor.baseY,
|
|
62
109
|
})
|
|
63
110
|
|
|
64
|
-
if (!tab?.viewport?.lines[row]) {
|
|
111
|
+
if (!tab?.viewport?.lines[anchor.row]) {
|
|
65
112
|
logInputDebug('click.noViewportLine', {
|
|
66
113
|
hasViewport: !!tab?.viewport,
|
|
67
114
|
lineCount: tab?.viewport?.lines.length ?? 0,
|
|
68
|
-
row,
|
|
115
|
+
row: anchor.row,
|
|
69
116
|
tabFound: !!tab,
|
|
70
117
|
targetTabId,
|
|
71
118
|
})
|
|
72
119
|
return null
|
|
73
120
|
}
|
|
74
121
|
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
let endCol: number
|
|
81
|
-
|
|
82
|
-
if (clickCount === 2) {
|
|
83
|
-
const word = getWordAtColumn(lineText, col)
|
|
84
|
-
if (word.text.length === 0) {
|
|
122
|
+
const range = computeMultiClickRange(tab, anchor.row, anchor.col, mode)
|
|
123
|
+
if (!range) {
|
|
124
|
+
if (mode === 'word') {
|
|
125
|
+
const line = tab.viewport.lines[anchor.row]
|
|
126
|
+
const lineText = line ? getLineText(line) : ''
|
|
85
127
|
logInputDebug('click.emptyWord', {
|
|
86
|
-
charAtCol: lineText[col] ?? 'OOB',
|
|
87
|
-
col,
|
|
128
|
+
charAtCol: lineText[anchor.col] ?? 'OOB',
|
|
129
|
+
col: anchor.col,
|
|
88
130
|
lineText,
|
|
89
|
-
row,
|
|
131
|
+
row: anchor.row,
|
|
90
132
|
})
|
|
91
|
-
return null
|
|
92
133
|
}
|
|
93
|
-
|
|
94
|
-
selectedText = word.text
|
|
95
|
-
startCol = word.startCol
|
|
96
|
-
endCol = word.endCol
|
|
97
|
-
} else {
|
|
98
|
-
selectedText = lineText
|
|
99
|
-
startCol = 0
|
|
100
|
-
endCol = lineText.length
|
|
134
|
+
return null
|
|
101
135
|
}
|
|
102
136
|
|
|
137
|
+
const line = tab.viewport.lines[anchor.row]
|
|
138
|
+
const lineText = line ? getLineText(line) : ''
|
|
139
|
+
const selectedText = lineText.slice(range.startCol, range.endCol)
|
|
140
|
+
|
|
103
141
|
logInputDebug('click.selection', {
|
|
104
|
-
baseX,
|
|
142
|
+
baseX: anchor.baseX,
|
|
105
143
|
clickCount,
|
|
106
|
-
endCol,
|
|
107
|
-
endX: baseX + endCol,
|
|
144
|
+
endCol: range.endCol,
|
|
145
|
+
endX: anchor.baseX + range.endCol,
|
|
108
146
|
lineText,
|
|
147
|
+
mode,
|
|
109
148
|
selectedText,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
bold: span.bold,
|
|
113
|
-
italic: span.italic,
|
|
114
|
-
underline: span.underline,
|
|
115
|
-
})),
|
|
116
|
-
spanTexts: line.spans.map((span) => span.text),
|
|
117
|
-
startCol,
|
|
118
|
-
startX: baseX + startCol,
|
|
149
|
+
startCol: range.startCol,
|
|
150
|
+
startX: anchor.baseX + range.startCol,
|
|
119
151
|
y: event.y,
|
|
120
152
|
})
|
|
121
153
|
|
|
122
154
|
return {
|
|
123
|
-
baseX,
|
|
124
|
-
endCol,
|
|
155
|
+
baseX: anchor.baseX,
|
|
156
|
+
endCol: range.endCol,
|
|
125
157
|
eventY: event.y,
|
|
158
|
+
mode,
|
|
159
|
+
row: anchor.row,
|
|
126
160
|
selectedText,
|
|
127
|
-
startCol,
|
|
128
|
-
target:
|
|
161
|
+
startCol: range.startCol,
|
|
162
|
+
target: anchor.target,
|
|
129
163
|
}
|
|
130
164
|
}
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
import { saveCurrentWorkspace } from '../state/workspace-save'
|
|
38
38
|
import { filterThemeIds } from '../ui/filter-themes'
|
|
39
39
|
import { scrollGitDiff } from '../ui/git-view-controls'
|
|
40
|
-
import { applyTheme, getTransparent, setTransparent } from '../ui/theme'
|
|
40
|
+
import { applyTheme, getCurrentMode, getTransparent, setMode, setTransparent } from '../ui/theme'
|
|
41
41
|
import { type ThemeId } from '../ui/themes'
|
|
42
42
|
import { triggerAutoCommitNow } from './auto-commit-ref'
|
|
43
43
|
import {
|
|
@@ -600,6 +600,12 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
|
|
|
600
600
|
saveConfig({ ...loadConfig(), themeTransparent: next })
|
|
601
601
|
return
|
|
602
602
|
}
|
|
603
|
+
case 'toggle-mode': {
|
|
604
|
+
const next = getCurrentMode() === 'dark' ? 'light' : 'dark'
|
|
605
|
+
setMode(next)
|
|
606
|
+
saveConfig({ ...loadConfig(), themeMode: next })
|
|
607
|
+
return
|
|
608
|
+
}
|
|
603
609
|
default:
|
|
604
610
|
effect satisfies never
|
|
605
611
|
}
|
|
@@ -16,6 +16,7 @@ interface BackendRuntimeOptions {
|
|
|
16
16
|
layoutRef: MutableRefObject<LayoutState>
|
|
17
17
|
resizingRef: MutableRefObject<boolean>
|
|
18
18
|
currentSessionWorkspaceSnapshot: Parameters<SessionBackend['attach']>[0]['workspaceSnapshot']
|
|
19
|
+
syntaxOverlayEnabled: () => boolean
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export interface TabRuntimeControls {
|
|
@@ -33,6 +34,7 @@ export function useBackendRuntime({
|
|
|
33
34
|
dispatch,
|
|
34
35
|
layoutRef,
|
|
35
36
|
resizingRef,
|
|
37
|
+
syntaxOverlayEnabled,
|
|
36
38
|
}: BackendRuntimeOptions): TabRuntimeControls {
|
|
37
39
|
const attachRequestIdRef = useRef(0)
|
|
38
40
|
const timeouts = useTabRuntimeTimeouts(dispatch)
|
|
@@ -70,13 +72,22 @@ export function useBackendRuntime({
|
|
|
70
72
|
backend,
|
|
71
73
|
dispatch,
|
|
72
74
|
resizingRef,
|
|
75
|
+
syntaxOverlayEnabled,
|
|
73
76
|
timeouts: {
|
|
74
77
|
clearAllTimers,
|
|
75
78
|
clearIdleTimer,
|
|
76
79
|
clearStartupGrace,
|
|
77
80
|
},
|
|
78
81
|
})
|
|
79
|
-
}, [
|
|
82
|
+
}, [
|
|
83
|
+
backend,
|
|
84
|
+
clearAllTimers,
|
|
85
|
+
clearIdleTimer,
|
|
86
|
+
clearStartupGrace,
|
|
87
|
+
dispatch,
|
|
88
|
+
resizingRef,
|
|
89
|
+
syntaxOverlayEnabled,
|
|
90
|
+
])
|
|
80
91
|
|
|
81
92
|
return {
|
|
82
93
|
clearIdleTimer,
|