@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brimveyn/aimux",
3
- "version": "1.9.9",
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.8",
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
- if (!event.target) {
41
- return null
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 col = event.x - viewportText.x
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: event.target.id,
60
- viewportX: viewportText.x,
61
- viewportY: viewportText.y,
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 line = tab.viewport.lines[row]
76
- const lineText = getLineText(line)
77
-
78
- let selectedText: string
79
- let startCol: number
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
- spanCount: line.spans.length,
111
- spanStyles: line.spans.map((span) => ({
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: event.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
- }, [backend, clearAllTimers, clearIdleTimer, clearStartupGrace, dispatch, resizingRef])
82
+ }, [
83
+ backend,
84
+ clearAllTimers,
85
+ clearIdleTimer,
86
+ clearStartupGrace,
87
+ dispatch,
88
+ resizingRef,
89
+ syntaxOverlayEnabled,
90
+ ])
80
91
 
81
92
  return {
82
93
  clearIdleTimer,