@brimveyn/aimux 1.1.0

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 (113) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +206 -0
  3. package/package.json +79 -0
  4. package/src/app-runtime/backend-attach-runtime.ts +135 -0
  5. package/src/app-runtime/backend-runtime-events.ts +78 -0
  6. package/src/app-runtime/click-selection-resolver.ts +130 -0
  7. package/src/app-runtime/pty-write.ts +32 -0
  8. package/src/app-runtime/render-invalidation.ts +20 -0
  9. package/src/app-runtime/selection-clipboard.ts +69 -0
  10. package/src/app-runtime/selection-scroll.ts +143 -0
  11. package/src/app-runtime/session-actions.ts +170 -0
  12. package/src/app-runtime/side-effects.ts +434 -0
  13. package/src/app-runtime/snippet-actions.ts +90 -0
  14. package/src/app-runtime/split-drag-controller.ts +15 -0
  15. package/src/app-runtime/tab-runtime-timeouts.ts +86 -0
  16. package/src/app-runtime/terminal-mouse-adapter.ts +31 -0
  17. package/src/app-runtime/use-backend-runtime.ts +99 -0
  18. package/src/app-runtime/use-directory-search.ts +52 -0
  19. package/src/app-runtime/use-mouse-handlers.ts +183 -0
  20. package/src/app-runtime/use-renderer-bindings.ts +163 -0
  21. package/src/app-runtime/use-terminal-resize.ts +141 -0
  22. package/src/app-runtime/use-workspace-autosave.ts +39 -0
  23. package/src/app.tsx +247 -0
  24. package/src/config/loader.ts +36 -0
  25. package/src/config.ts +146 -0
  26. package/src/daemon/daemon.ts +236 -0
  27. package/src/daemon/runtime-paths.ts +69 -0
  28. package/src/daemon/session-manager.ts +109 -0
  29. package/src/daemon/session-registry.ts +207 -0
  30. package/src/debug/input-log.ts +29 -0
  31. package/src/doctor.ts +106 -0
  32. package/src/git/git-poller.ts +49 -0
  33. package/src/git/git-status.ts +183 -0
  34. package/src/index.tsx +56 -0
  35. package/src/input/keymap/build-handlers.ts +34 -0
  36. package/src/input/keymap/key-chord.ts +201 -0
  37. package/src/input/keymap/keymap-mode-handler.ts +88 -0
  38. package/src/input/keymap/sequence-resolver.ts +117 -0
  39. package/src/input/keymap/trie.ts +103 -0
  40. package/src/input/modes/bridge.ts +58 -0
  41. package/src/input/modes/handlers/index.ts +18 -0
  42. package/src/input/modes/handlers/shared.ts +70 -0
  43. package/src/input/modes/registry.ts +34 -0
  44. package/src/input/modes/transitions.ts +37 -0
  45. package/src/input/modes/types.ts +63 -0
  46. package/src/input/mouse-forwarding.ts +98 -0
  47. package/src/input/multi-click-detector.ts +55 -0
  48. package/src/input/paste.ts +12 -0
  49. package/src/input/raw-input-handler.ts +191 -0
  50. package/src/input/terminal-text-extraction.ts +78 -0
  51. package/src/ipc/protocol.ts +341 -0
  52. package/src/platform/clipboard.ts +13 -0
  53. package/src/platform/daemon-control.ts +62 -0
  54. package/src/platform/id.ts +7 -0
  55. package/src/platform/project-search.ts +75 -0
  56. package/src/pty/command-registry.ts +69 -0
  57. package/src/pty/pty-manager.ts +268 -0
  58. package/src/pty/terminal-snapshot.ts +210 -0
  59. package/src/restart-daemon.ts +28 -0
  60. package/src/session-backend/bootstrap.ts +107 -0
  61. package/src/session-backend/local-session-backend.ts +164 -0
  62. package/src/session-backend/remote-session-backend.ts +373 -0
  63. package/src/session-backend/types.ts +47 -0
  64. package/src/state/app-store.ts +19 -0
  65. package/src/state/layout-resize.ts +56 -0
  66. package/src/state/layout-tree.ts +323 -0
  67. package/src/state/reducers/git-panel-state.ts +102 -0
  68. package/src/state/reducers/modal-state.ts +358 -0
  69. package/src/state/reducers/session-state.ts +86 -0
  70. package/src/state/reducers/tab-state.ts +531 -0
  71. package/src/state/reducers/ui-state.ts +29 -0
  72. package/src/state/selectors.ts +30 -0
  73. package/src/state/session-catalog.ts +96 -0
  74. package/src/state/session-persistence.ts +210 -0
  75. package/src/state/snippet-catalog.ts +90 -0
  76. package/src/state/store.ts +93 -0
  77. package/src/state/terminal-modes.ts +11 -0
  78. package/src/state/types.ts +367 -0
  79. package/src/state/validation.ts +154 -0
  80. package/src/state/workspace-save.ts +33 -0
  81. package/src/ui/components/create-session-modal.tsx +100 -0
  82. package/src/ui/components/git-panel.tsx +200 -0
  83. package/src/ui/components/help-modal.tsx +79 -0
  84. package/src/ui/components/input-field.tsx +18 -0
  85. package/src/ui/components/list-item.tsx +30 -0
  86. package/src/ui/components/modal-filter-bar.tsx +18 -0
  87. package/src/ui/components/modal-shell.tsx +47 -0
  88. package/src/ui/components/new-tab-modal.tsx +52 -0
  89. package/src/ui/components/pending-chord-indicator.tsx +41 -0
  90. package/src/ui/components/session-name-modal.tsx +15 -0
  91. package/src/ui/components/session-picker-modal.tsx +93 -0
  92. package/src/ui/components/sidebar-group-metadata.ts +44 -0
  93. package/src/ui/components/sidebar-scroll.ts +33 -0
  94. package/src/ui/components/sidebar.tsx +225 -0
  95. package/src/ui/components/snippet-editor-modal.tsx +39 -0
  96. package/src/ui/components/snippet-picker-modal.tsx +56 -0
  97. package/src/ui/components/split-layout.tsx +201 -0
  98. package/src/ui/components/status-bar.tsx +60 -0
  99. package/src/ui/components/surface.tsx +63 -0
  100. package/src/ui/components/tab-item.tsx +118 -0
  101. package/src/ui/components/terminal-pane.tsx +215 -0
  102. package/src/ui/components/theme-picker-modal.tsx +35 -0
  103. package/src/ui/components/use-sidebar-auto-scroll.ts +63 -0
  104. package/src/ui/components/use-sidebar-branch.ts +36 -0
  105. package/src/ui/directory-search.ts +1 -0
  106. package/src/ui/git-branch.ts +11 -0
  107. package/src/ui/path-format.ts +6 -0
  108. package/src/ui/root.tsx +261 -0
  109. package/src/ui/status-bar-model.ts +87 -0
  110. package/src/ui/theme.ts +9 -0
  111. package/src/ui/themes.ts +255 -0
  112. package/src/ui/ui-tokens.ts +11 -0
  113. package/src/update.ts +62 -0
@@ -0,0 +1,56 @@
1
+ import type { WorkspaceSnapshotV1 } from './types'
2
+
3
+ import { computePaneRects, type LayoutNode, PANE_BORDER, type PaneRect } from './layout-tree'
4
+
5
+ export interface TerminalBounds {
6
+ x: number
7
+ y: number
8
+ cols: number
9
+ rows: number
10
+ }
11
+
12
+ export function getSnapshotTrees(snapshot: WorkspaceSnapshotV1 | undefined): LayoutNode[] {
13
+ if (snapshot?.layoutTrees) {
14
+ return Object.values(snapshot.layoutTrees)
15
+ }
16
+
17
+ if (snapshot?.layoutTree) {
18
+ return [snapshot.layoutTree]
19
+ }
20
+
21
+ return []
22
+ }
23
+
24
+ export function createTerminalBounds(cols: number, rows: number): TerminalBounds {
25
+ const chrome = PANE_BORDER * 2
26
+ return {
27
+ cols: cols + chrome,
28
+ rows: rows + chrome,
29
+ x: 0,
30
+ y: 0,
31
+ }
32
+ }
33
+
34
+ export function toTerminalContentSize(rect: PaneRect): { cols: number; rows: number } {
35
+ const chrome = PANE_BORDER * 2
36
+ return {
37
+ cols: Math.max(1, rect.cols - chrome),
38
+ rows: Math.max(1, rect.rows - chrome),
39
+ }
40
+ }
41
+
42
+ export function forEachSplitPaneRect(
43
+ trees: LayoutNode[],
44
+ bounds: TerminalBounds,
45
+ callback: (tabId: string, rect: PaneRect) => void
46
+ ): void {
47
+ for (const tree of trees) {
48
+ if (tree.type !== 'split') {
49
+ continue
50
+ }
51
+
52
+ for (const [tabId, rect] of computePaneRects(tree, bounds)) {
53
+ callback(tabId, rect)
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,323 @@
1
+ export type SplitDirection = 'horizontal' | 'vertical'
2
+
3
+ export type LayoutLeaf = { type: 'leaf'; tabId: string }
4
+ export type LayoutSplit = {
5
+ type: 'split'
6
+ direction: SplitDirection
7
+ ratio: number
8
+ first: LayoutNode
9
+ second: LayoutNode
10
+ }
11
+ export type LayoutNode = LayoutLeaf | LayoutSplit
12
+
13
+ export interface PaneRect {
14
+ x: number
15
+ y: number
16
+ cols: number
17
+ rows: number
18
+ }
19
+
20
+ /** Border thickness on each side of a split pane (used to compute content size from outer bounds). */
21
+ export const PANE_BORDER = 1
22
+
23
+ const MIN_RATIO = 0.15
24
+ const MAX_RATIO = 0.85
25
+ const RATIO_STEP = 0.05
26
+
27
+ let groupCounter = 0
28
+ export function createGroupId(): string {
29
+ return `group-${Date.now()}-${++groupCounter}`
30
+ }
31
+
32
+ export function getGroupIdForTab(
33
+ tabGroupMap: Record<string, string>,
34
+ tabId: string
35
+ ): string | null {
36
+ return tabGroupMap[tabId] ?? null
37
+ }
38
+
39
+ export function getTreeForTab(
40
+ layoutTrees: Record<string, LayoutNode>,
41
+ tabGroupMap: Record<string, string>,
42
+ tabId: string
43
+ ): LayoutNode | null {
44
+ const groupId = tabGroupMap[tabId]
45
+ if (!groupId) return null
46
+ return layoutTrees[groupId] ?? null
47
+ }
48
+
49
+ export function createLeaf(tabId: string): LayoutLeaf {
50
+ return { tabId, type: 'leaf' }
51
+ }
52
+
53
+ export function splitNode(
54
+ tree: LayoutNode,
55
+ targetTabId: string,
56
+ direction: SplitDirection,
57
+ newTabId: string
58
+ ): LayoutNode {
59
+ if (tree.type === 'leaf') {
60
+ if (tree.tabId === targetTabId) {
61
+ return {
62
+ direction,
63
+ first: tree,
64
+ ratio: 0.5,
65
+ second: createLeaf(newTabId),
66
+ type: 'split',
67
+ }
68
+ }
69
+ return tree
70
+ }
71
+
72
+ const newFirst = splitNode(tree.first, targetTabId, direction, newTabId)
73
+ if (newFirst !== tree.first) {
74
+ return { ...tree, first: newFirst }
75
+ }
76
+
77
+ const newSecond = splitNode(tree.second, targetTabId, direction, newTabId)
78
+ if (newSecond !== tree.second) {
79
+ return { ...tree, second: newSecond }
80
+ }
81
+
82
+ return tree
83
+ }
84
+
85
+ export function removeNode(tree: LayoutNode, tabId: string): LayoutNode | null {
86
+ if (tree.type === 'leaf') {
87
+ return tree.tabId === tabId ? null : tree
88
+ }
89
+
90
+ const newFirst = removeNode(tree.first, tabId)
91
+ const newSecond = removeNode(tree.second, tabId)
92
+
93
+ if (newFirst === null && newSecond === null) {
94
+ return null
95
+ }
96
+ if (newFirst === null) {
97
+ return newSecond
98
+ }
99
+ if (newSecond === null) {
100
+ return newFirst
101
+ }
102
+
103
+ if (newFirst !== tree.first || newSecond !== tree.second) {
104
+ return { ...tree, first: newFirst, second: newSecond }
105
+ }
106
+
107
+ return tree
108
+ }
109
+
110
+ export function findLeaf(tree: LayoutNode, tabId: string): boolean {
111
+ if (tree.type === 'leaf') {
112
+ return tree.tabId === tabId
113
+ }
114
+ return findLeaf(tree.first, tabId) || findLeaf(tree.second, tabId)
115
+ }
116
+
117
+ export function allLeafIds(tree: LayoutNode): string[] {
118
+ if (tree.type === 'leaf') {
119
+ return [tree.tabId]
120
+ }
121
+ return [...allLeafIds(tree.first), ...allLeafIds(tree.second)]
122
+ }
123
+
124
+ export function pruneLayoutTree(tree: LayoutNode, validTabIds: Set<string>): LayoutNode | null {
125
+ if (tree.type === 'leaf') {
126
+ return validTabIds.has(tree.tabId) ? tree : null
127
+ }
128
+ const first = pruneLayoutTree(tree.first, validTabIds)
129
+ const second = pruneLayoutTree(tree.second, validTabIds)
130
+ if (!first && !second) return null
131
+ if (!first) return second
132
+ if (!second) return first
133
+ if (first === tree.first && second === tree.second) return tree
134
+ return { ...tree, first, second }
135
+ }
136
+
137
+ export function resizeSplit(
138
+ tree: LayoutNode,
139
+ targetTabId: string,
140
+ delta: number,
141
+ axis?: SplitDirection
142
+ ): LayoutNode {
143
+ if (tree.type === 'leaf') {
144
+ return tree
145
+ }
146
+
147
+ const inFirst = findLeaf(tree.first, targetTabId)
148
+ const inSecond = findLeaf(tree.second, targetTabId)
149
+
150
+ if (!inFirst && !inSecond) {
151
+ return tree
152
+ }
153
+
154
+ // If axis is specified, only resize splits matching that axis
155
+ const canResizeThis = !axis || tree.direction === axis
156
+
157
+ if (inFirst || inSecond) {
158
+ // First try to recurse deeper
159
+ if (inFirst) {
160
+ const newFirst = resizeSplit(tree.first, targetTabId, delta, axis)
161
+ if (newFirst !== tree.first) return { ...tree, first: newFirst }
162
+ } else {
163
+ const newSecond = resizeSplit(tree.second, targetTabId, delta, axis)
164
+ if (newSecond !== tree.second) return { ...tree, second: newSecond }
165
+ }
166
+
167
+ // Couldn't resize deeper — resize this split if axis matches
168
+ if (canResizeThis) {
169
+ const newRatio = Math.min(MAX_RATIO, Math.max(MIN_RATIO, tree.ratio + delta * RATIO_STEP))
170
+ if (newRatio === tree.ratio) return tree
171
+ return { ...tree, ratio: newRatio }
172
+ }
173
+ }
174
+
175
+ return tree
176
+ }
177
+
178
+ export function setSplitRatio(
179
+ tree: LayoutNode,
180
+ targetTabId: string,
181
+ ratio: number,
182
+ axis?: SplitDirection
183
+ ): LayoutNode {
184
+ if (tree.type === 'leaf') {
185
+ return tree
186
+ }
187
+
188
+ const inFirst = findLeaf(tree.first, targetTabId)
189
+ const inSecond = findLeaf(tree.second, targetTabId)
190
+
191
+ if (!inFirst && !inSecond) {
192
+ return tree
193
+ }
194
+
195
+ const canResizeThis = !axis || tree.direction === axis
196
+
197
+ if (inFirst || inSecond) {
198
+ if (inFirst) {
199
+ const newFirst = setSplitRatio(tree.first, targetTabId, ratio, axis)
200
+ if (newFirst !== tree.first) return { ...tree, first: newFirst }
201
+ } else {
202
+ const newSecond = setSplitRatio(tree.second, targetTabId, ratio, axis)
203
+ if (newSecond !== tree.second) return { ...tree, second: newSecond }
204
+ }
205
+
206
+ if (canResizeThis) {
207
+ const newRatio = Math.min(MAX_RATIO, Math.max(MIN_RATIO, ratio))
208
+ if (newRatio === tree.ratio) return tree
209
+ return { ...tree, ratio: newRatio }
210
+ }
211
+ }
212
+
213
+ return tree
214
+ }
215
+
216
+ export function computePaneRects(tree: LayoutNode, bounds: PaneRect): Map<string, PaneRect> {
217
+ const result = new Map<string, PaneRect>()
218
+
219
+ if (tree.type === 'leaf') {
220
+ result.set(tree.tabId, bounds)
221
+ return result
222
+ }
223
+
224
+ if (tree.direction === 'vertical') {
225
+ // Split left/right
226
+ const firstCols = Math.max(1, Math.floor(bounds.cols * tree.ratio))
227
+ const secondCols = Math.max(1, bounds.cols - firstCols - 1) // -1 for separator
228
+
229
+ const firstBounds: PaneRect = { cols: firstCols, rows: bounds.rows, x: bounds.x, y: bounds.y }
230
+ const secondBounds: PaneRect = {
231
+ cols: secondCols,
232
+ rows: bounds.rows,
233
+ x: bounds.x + firstCols + 1,
234
+ y: bounds.y,
235
+ }
236
+
237
+ for (const [id, rect] of computePaneRects(tree.first, firstBounds)) {
238
+ result.set(id, rect)
239
+ }
240
+ for (const [id, rect] of computePaneRects(tree.second, secondBounds)) {
241
+ result.set(id, rect)
242
+ }
243
+ } else {
244
+ // Split top/bottom
245
+ const firstRows = Math.max(1, Math.floor(bounds.rows * tree.ratio))
246
+ const secondRows = Math.max(1, bounds.rows - firstRows - 1) // -1 for separator
247
+
248
+ const firstBounds: PaneRect = { cols: bounds.cols, rows: firstRows, x: bounds.x, y: bounds.y }
249
+ const secondBounds: PaneRect = {
250
+ cols: bounds.cols,
251
+ rows: secondRows,
252
+ x: bounds.x,
253
+ y: bounds.y + firstRows + 1,
254
+ }
255
+
256
+ for (const [id, rect] of computePaneRects(tree.first, firstBounds)) {
257
+ result.set(id, rect)
258
+ }
259
+ for (const [id, rect] of computePaneRects(tree.second, secondBounds)) {
260
+ result.set(id, rect)
261
+ }
262
+ }
263
+
264
+ return result
265
+ }
266
+
267
+ type NavDirection = 'left' | 'right' | 'up' | 'down'
268
+
269
+ function firstLeafId(node: LayoutNode): string {
270
+ if (node.type === 'leaf') return node.tabId
271
+ return firstLeafId(node.first)
272
+ }
273
+
274
+ function lastLeafId(node: LayoutNode): string {
275
+ if (node.type === 'leaf') return node.tabId
276
+ return lastLeafId(node.second)
277
+ }
278
+
279
+ export function getAdjacentLeaf(
280
+ tree: LayoutNode,
281
+ fromTabId: string,
282
+ direction: NavDirection
283
+ ): string | null {
284
+ if (tree.type === 'leaf') {
285
+ return null
286
+ }
287
+
288
+ const inFirst = findLeaf(tree.first, fromTabId)
289
+ const inSecond = findLeaf(tree.second, fromTabId)
290
+
291
+ if (!inFirst && !inSecond) {
292
+ return null
293
+ }
294
+
295
+ // Check if the split direction matches the navigation direction
296
+ const isMatchingDirection =
297
+ (tree.direction === 'vertical' && (direction === 'left' || direction === 'right')) ||
298
+ (tree.direction === 'horizontal' && (direction === 'up' || direction === 'down'))
299
+
300
+ if (isMatchingDirection) {
301
+ const goingToSecond = direction === 'right' || direction === 'down'
302
+
303
+ if (inFirst && goingToSecond) {
304
+ // Try to recurse in first child first
305
+ const deeper = getAdjacentLeaf(tree.first, fromTabId, direction)
306
+ if (deeper) return deeper
307
+ // Cross boundary: go to first leaf of second child
308
+ return firstLeafId(tree.second)
309
+ }
310
+
311
+ if (inSecond && !goingToSecond) {
312
+ const deeper = getAdjacentLeaf(tree.second, fromTabId, direction)
313
+ if (deeper) return deeper
314
+ return lastLeafId(tree.first)
315
+ }
316
+ }
317
+
318
+ // Direction doesn't match this split — recurse into the branch containing the tab
319
+ if (inFirst) {
320
+ return getAdjacentLeaf(tree.first, fromTabId, direction)
321
+ }
322
+ return getAdjacentLeaf(tree.second, fromTabId, direction)
323
+ }
@@ -0,0 +1,102 @@
1
+ import type { AppAction, AppState, GitFileEntry, GitPanelState } from '../types'
2
+
3
+ export const GIT_PANEL_MIN_RATIO = 0.2
4
+ export const GIT_PANEL_MAX_RATIO = 0.8
5
+
6
+ function clampRatio(value: number): number {
7
+ return Math.max(GIT_PANEL_MIN_RATIO, Math.min(GIT_PANEL_MAX_RATIO, value))
8
+ }
9
+
10
+ export function emptyGitPanel(): GitPanelState {
11
+ return {
12
+ ahead: 0,
13
+ behind: 0,
14
+ branch: null,
15
+ error: null,
16
+ files: [],
17
+ }
18
+ }
19
+
20
+ function sameFiles(a: GitFileEntry[], b: GitFileEntry[]): boolean {
21
+ if (a.length !== b.length) return false
22
+ for (let i = 0; i < a.length; i++) {
23
+ const x = a[i] as GitFileEntry
24
+ const y = b[i] as GitFileEntry
25
+ if (
26
+ x.path !== y.path ||
27
+ x.status !== y.status ||
28
+ x.section !== y.section ||
29
+ x.added !== y.added ||
30
+ x.removed !== y.removed ||
31
+ x.renamedFrom !== y.renamedFrom
32
+ ) {
33
+ return false
34
+ }
35
+ }
36
+ return true
37
+ }
38
+
39
+ export function reduceGitPanelState(state: AppState, action: AppAction): AppState | null {
40
+ switch (action.type) {
41
+ case 'toggle-git-panel': {
42
+ const nextGitVisible = !state.sidebar.gitPanelVisible
43
+ const nextVisible = state.sidebar.visible || nextGitVisible
44
+ return {
45
+ ...state,
46
+ sidebar: { ...state.sidebar, gitPanelVisible: nextGitVisible, visible: nextVisible },
47
+ }
48
+ }
49
+ case 'resize-git-panel': {
50
+ const nextRatio = clampRatio(state.sidebar.gitPanelRatio + action.delta)
51
+ if (nextRatio === state.sidebar.gitPanelRatio) return state
52
+ return { ...state, sidebar: { ...state.sidebar, gitPanelRatio: nextRatio } }
53
+ }
54
+ case 'git-refresh-success': {
55
+ const prev = state.gitPanel
56
+ const next = action.payload
57
+ if (
58
+ prev.branch === next.branch &&
59
+ prev.ahead === next.ahead &&
60
+ prev.behind === next.behind &&
61
+ prev.error === null &&
62
+ sameFiles(prev.files, next.files)
63
+ ) {
64
+ return state
65
+ }
66
+ return {
67
+ ...state,
68
+ gitPanel: {
69
+ ...prev,
70
+ ahead: next.ahead,
71
+ behind: next.behind,
72
+ branch: next.branch,
73
+ error: null,
74
+ files: next.files,
75
+ },
76
+ }
77
+ }
78
+ case 'git-refresh-error': {
79
+ const prev = state.gitPanel
80
+ if (prev.error === action.kind && prev.files.length === 0) return state
81
+ return {
82
+ ...state,
83
+ gitPanel: { ...prev, error: action.kind, files: [] },
84
+ }
85
+ }
86
+ case 'git-panel-reset': {
87
+ const prev = state.gitPanel
88
+ if (
89
+ prev.branch === null &&
90
+ prev.error === null &&
91
+ prev.files.length === 0 &&
92
+ prev.ahead === 0 &&
93
+ prev.behind === 0
94
+ ) {
95
+ return state
96
+ }
97
+ return { ...state, gitPanel: emptyGitPanel() }
98
+ }
99
+ default:
100
+ return null
101
+ }
102
+ }