@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,358 @@
1
+ import { basename } from 'node:path'
2
+
3
+ import type { AppAction, AppState } from '../types'
4
+
5
+ import { getAllAssistantOptions } from '../../pty/command-registry'
6
+ import { THEME_IDS } from '../../ui/themes'
7
+ import { filterSessions, filterSnippets } from '../selectors'
8
+
9
+ const THEME_COUNT = THEME_IDS.length
10
+
11
+ function emptyModal() {
12
+ return {
13
+ editBuffer: null,
14
+ selectedIndex: 0,
15
+ sessionTargetId: null,
16
+ type: null,
17
+ } as const
18
+ }
19
+
20
+ export { emptyModal }
21
+
22
+ export function reduceModalState(state: AppState, action: AppAction): AppState | null {
23
+ switch (action.type) {
24
+ case 'open-new-tab-modal':
25
+ return {
26
+ ...state,
27
+ focusMode: 'modal',
28
+ modal: { editBuffer: null, selectedIndex: 0, sessionTargetId: null, type: 'new-tab' },
29
+ }
30
+ case 'open-help-modal':
31
+ return {
32
+ ...state,
33
+ focusMode: 'modal',
34
+ modal: { editBuffer: null, selectedIndex: 0, sessionTargetId: null, type: 'help' },
35
+ }
36
+ case 'open-split-picker':
37
+ return {
38
+ ...state,
39
+ focusMode: 'modal',
40
+ modal: {
41
+ editBuffer: null,
42
+ selectedIndex: 0,
43
+ sessionTargetId: null,
44
+ splitDirection: action.direction,
45
+ type: 'split-picker',
46
+ },
47
+ }
48
+ case 'open-session-picker':
49
+ return {
50
+ ...state,
51
+ focusMode: 'modal',
52
+ modal: {
53
+ editBuffer: null,
54
+ selectedIndex: 0,
55
+ sessionTargetId: null,
56
+ type: 'session-picker',
57
+ },
58
+ }
59
+ case 'open-session-name-modal':
60
+ return {
61
+ ...state,
62
+ focusMode: 'command-edit',
63
+ modal: {
64
+ editBuffer: action.initialName ?? '',
65
+ selectedIndex: 0,
66
+ sessionTargetId: action.sessionTargetId ?? null,
67
+ type: 'session-name',
68
+ },
69
+ }
70
+ case 'open-create-session-modal':
71
+ return {
72
+ ...state,
73
+ focusMode: 'command-edit',
74
+ modal: {
75
+ activeField: 'directory',
76
+ directoryResults: [],
77
+ editBuffer: '',
78
+ nameBuffer: '',
79
+ pendingProjectPath: null,
80
+ selectedIndex: 0,
81
+ sessionTargetId: null,
82
+ type: 'create-session',
83
+ },
84
+ }
85
+ case 'set-directory-results': {
86
+ if (state.modal.type !== 'create-session') {
87
+ return state
88
+ }
89
+ return {
90
+ ...state,
91
+ modal: {
92
+ ...state.modal,
93
+ directoryResults: action.results,
94
+ selectedIndex: 0,
95
+ },
96
+ }
97
+ }
98
+ case 'open-snippet-picker':
99
+ return {
100
+ ...state,
101
+ focusMode: 'modal',
102
+ modal: {
103
+ editBuffer: null,
104
+ selectedIndex: 0,
105
+ sessionTargetId: null,
106
+ type: 'snippet-picker',
107
+ },
108
+ }
109
+ case 'open-snippet-editor': {
110
+ const snippet = action.snippetId
111
+ ? state.snippets.find((s) => s.id === action.snippetId)
112
+ : undefined
113
+ return {
114
+ ...state,
115
+ focusMode: 'command-edit',
116
+ modal: {
117
+ activeField: 'name',
118
+ contentBuffer: snippet?.content ?? '',
119
+ editBuffer: snippet?.name ?? '',
120
+ selectedIndex: 0,
121
+ sessionTargetId: snippet?.id ?? null,
122
+ type: 'snippet-editor',
123
+ },
124
+ }
125
+ }
126
+ case 'open-theme-picker':
127
+ return {
128
+ ...state,
129
+ focusMode: 'modal',
130
+ modal: { editBuffer: null, selectedIndex: 0, sessionTargetId: null, type: 'theme-picker' },
131
+ }
132
+ case 'begin-snippet-filter': {
133
+ if (state.modal.type !== 'snippet-picker') {
134
+ return state
135
+ }
136
+ return {
137
+ ...state,
138
+ focusMode: 'command-edit',
139
+ modal: { ...state.modal, editBuffer: state.modal.editBuffer ?? '' },
140
+ }
141
+ }
142
+ case 'close-modal':
143
+ return { ...state, focusMode: 'navigation', modal: emptyModal() }
144
+ case 'move-modal-selection': {
145
+ if (
146
+ state.modal.type !== 'new-tab' &&
147
+ state.modal.type !== 'session-picker' &&
148
+ state.modal.type !== 'snippet-picker' &&
149
+ state.modal.type !== 'theme-picker' &&
150
+ state.modal.type !== 'create-session' &&
151
+ state.modal.type !== 'split-picker'
152
+ ) {
153
+ return state
154
+ }
155
+ if (state.modal.type === 'create-session' && state.modal.activeField !== 'directory') {
156
+ return state
157
+ }
158
+ let optionCount: number
159
+ if (state.modal.type === 'new-tab' || state.modal.type === 'split-picker') {
160
+ optionCount = getAllAssistantOptions(state.customCommands).length
161
+ } else if (state.modal.type === 'create-session') {
162
+ optionCount = state.modal.directoryResults.length
163
+ } else if (state.modal.type === 'snippet-picker') {
164
+ const filtered = filterSnippets(state.snippets, state.modal.editBuffer)
165
+ optionCount = filtered.length
166
+ } else if (state.modal.type === 'theme-picker') {
167
+ optionCount = THEME_COUNT
168
+ } else {
169
+ const filtered = filterSessions(state.sessions, state.modal.editBuffer)
170
+ optionCount = Math.max(1, filtered.length + 1)
171
+ }
172
+ if (optionCount === 0) {
173
+ return state
174
+ }
175
+ return {
176
+ ...state,
177
+ modal: {
178
+ ...state.modal,
179
+ selectedIndex: (state.modal.selectedIndex + action.delta + optionCount) % optionCount,
180
+ },
181
+ }
182
+ }
183
+ case 'begin-command-edit': {
184
+ if (state.modal.type !== 'new-tab' && state.modal.type !== 'session-name') {
185
+ return state
186
+ }
187
+ if (state.modal.type === 'session-name') {
188
+ return { ...state, focusMode: 'command-edit' }
189
+ }
190
+ const allOptions = getAllAssistantOptions(state.customCommands)
191
+ const option = allOptions[state.modal.selectedIndex]
192
+ const assistantId = option?.id
193
+ const currentCmd = (assistantId && state.customCommands[assistantId]) ?? option?.command ?? ''
194
+ return {
195
+ ...state,
196
+ focusMode: 'command-edit',
197
+ modal: { ...state.modal, editBuffer: currentCmd },
198
+ }
199
+ }
200
+ case 'update-command-edit': {
201
+ if (state.modal.editBuffer === null) {
202
+ return state
203
+ }
204
+ const buf =
205
+ action.char === '\b'
206
+ ? state.modal.editBuffer.slice(0, -1)
207
+ : state.modal.editBuffer + action.char
208
+ const resetIndex =
209
+ state.modal.type === 'session-picker' || state.modal.type === 'snippet-picker'
210
+ ? 0
211
+ : state.modal.selectedIndex
212
+ return { ...state, modal: { ...state.modal, editBuffer: buf, selectedIndex: resetIndex } }
213
+ }
214
+ case 'commit-command-edit': {
215
+ if (state.modal.editBuffer === null) {
216
+ return state
217
+ }
218
+ if (state.modal.type === 'create-session') {
219
+ return state
220
+ }
221
+ if (state.modal.type === 'snippet-editor') {
222
+ return state
223
+ }
224
+ if (state.modal.type === 'rename-tab') {
225
+ return state
226
+ }
227
+ if (state.modal.type === 'session-name') {
228
+ return state
229
+ }
230
+ if (state.modal.type === 'session-picker' || state.modal.type === 'snippet-picker') {
231
+ return {
232
+ ...state,
233
+ focusMode: 'modal',
234
+ modal: { ...state.modal, selectedIndex: 0 },
235
+ }
236
+ }
237
+ if (state.modal.type !== 'new-tab') {
238
+ return state
239
+ }
240
+ const allOpts = getAllAssistantOptions(state.customCommands)
241
+ const option = allOpts[state.modal.selectedIndex]
242
+ const assistantId = option?.id
243
+ if (!assistantId) {
244
+ return { ...state, focusMode: 'modal', modal: { ...state.modal, editBuffer: null } }
245
+ }
246
+ const trimmed = state.modal.editBuffer.trim()
247
+ const newCustomCommands = { ...state.customCommands }
248
+ if (trimmed) {
249
+ newCustomCommands[assistantId] = trimmed
250
+ } else {
251
+ delete newCustomCommands[assistantId]
252
+ }
253
+ return {
254
+ ...state,
255
+ customCommands: newCustomCommands,
256
+ focusMode: 'modal',
257
+ modal: { ...state.modal, editBuffer: null },
258
+ }
259
+ }
260
+ case 'cancel-command-edit': {
261
+ if (state.modal.type === 'create-session' || state.modal.type === 'snippet-editor') {
262
+ return { ...state, focusMode: 'navigation', modal: emptyModal() }
263
+ }
264
+ if (state.modal.type === 'session-picker' || state.modal.type === 'snippet-picker') {
265
+ return {
266
+ ...state,
267
+ focusMode: 'modal',
268
+ modal: { ...state.modal, editBuffer: null, selectedIndex: 0 },
269
+ }
270
+ }
271
+ return { ...state, focusMode: 'modal', modal: { ...state.modal, editBuffer: null } }
272
+ }
273
+ case 'switch-create-session-field': {
274
+ if (state.modal.type === 'create-session') {
275
+ const nextField = state.modal.activeField === 'directory' ? 'name' : 'directory'
276
+ return {
277
+ ...state,
278
+ modal: {
279
+ ...state.modal,
280
+ activeField: nextField,
281
+ editBuffer: state.modal.nameBuffer,
282
+ nameBuffer: state.modal.editBuffer ?? '',
283
+ },
284
+ }
285
+ }
286
+ if (state.modal.type === 'snippet-editor') {
287
+ const nextField = state.modal.activeField === 'name' ? 'content' : 'name'
288
+ return {
289
+ ...state,
290
+ modal: {
291
+ ...state.modal,
292
+ activeField: nextField,
293
+ contentBuffer: state.modal.editBuffer ?? '',
294
+ editBuffer: state.modal.contentBuffer,
295
+ },
296
+ }
297
+ }
298
+ return state
299
+ }
300
+ case 'select-directory': {
301
+ if (state.modal.type !== 'create-session') {
302
+ return state
303
+ }
304
+ const selected = state.modal.directoryResults[state.modal.selectedIndex]
305
+ if (!selected) {
306
+ return state
307
+ }
308
+ const nameValue =
309
+ state.modal.activeField === 'directory'
310
+ ? state.modal.nameBuffer
311
+ : (state.modal.editBuffer ?? '')
312
+ const autoName = nameValue || basename(selected.path)
313
+ return {
314
+ ...state,
315
+ modal: {
316
+ ...state.modal,
317
+ activeField: 'name',
318
+ editBuffer: autoName,
319
+ nameBuffer:
320
+ state.modal.activeField === 'directory'
321
+ ? (state.modal.editBuffer ?? '')
322
+ : state.modal.nameBuffer,
323
+ pendingProjectPath: selected.path,
324
+ },
325
+ }
326
+ }
327
+ case 'begin-session-filter': {
328
+ if (state.modal.type !== 'session-picker') {
329
+ return state
330
+ }
331
+ return {
332
+ ...state,
333
+ focusMode: 'command-edit',
334
+ modal: { ...state.modal, editBuffer: state.modal.editBuffer ?? '' },
335
+ }
336
+ }
337
+ case 'open-rename-tab-modal': {
338
+ const activeTab = state.activeTabId
339
+ ? state.tabs.find((tab) => tab.id === state.activeTabId)
340
+ : undefined
341
+ if (!activeTab) {
342
+ return state
343
+ }
344
+ return {
345
+ ...state,
346
+ focusMode: 'command-edit',
347
+ modal: {
348
+ editBuffer: activeTab.title,
349
+ selectedIndex: 0,
350
+ sessionTargetId: activeTab.id,
351
+ type: 'rename-tab',
352
+ },
353
+ }
354
+ }
355
+ default:
356
+ return null
357
+ }
358
+ }
@@ -0,0 +1,86 @@
1
+ import type { AppAction, AppState } from '../types'
2
+
3
+ import { filterSessions } from '../selectors'
4
+ import { restoreWorkspaceState } from '../session-persistence'
5
+
6
+ const CLOSED_MODAL = {
7
+ editBuffer: null,
8
+ selectedIndex: 0,
9
+ sessionTargetId: null,
10
+ type: null,
11
+ } as const
12
+
13
+ export function reduceSessionState(state: AppState, action: AppAction): AppState | null {
14
+ switch (action.type) {
15
+ case 'load-session': {
16
+ const snapshot =
17
+ action.workspaceSnapshot ??
18
+ state.sessions.find((entry) => entry.id === action.sessionId)?.workspaceSnapshot
19
+ return {
20
+ ...state,
21
+ ...restoreWorkspaceState(state, snapshot),
22
+ currentSessionId: action.sessionId,
23
+ focusMode: 'navigation',
24
+ modal: CLOSED_MODAL,
25
+ sessions: state.sessions.map((entry) =>
26
+ entry.id === action.sessionId
27
+ ? {
28
+ ...entry,
29
+ lastOpenedAt: new Date().toISOString(),
30
+ updatedAt: new Date().toISOString(),
31
+ }
32
+ : entry
33
+ ),
34
+ }
35
+ }
36
+ case 'set-sessions':
37
+ return { ...state, sessions: action.sessions }
38
+ case 'create-session-record':
39
+ return {
40
+ ...state,
41
+ currentSessionId: action.session.id,
42
+ focusMode: 'navigation',
43
+ modal: CLOSED_MODAL,
44
+ sessions: [...state.sessions, action.session],
45
+ }
46
+ case 'rename-session-record':
47
+ return {
48
+ ...state,
49
+ focusMode: 'modal',
50
+ modal: {
51
+ editBuffer: null,
52
+ selectedIndex: state.modal.selectedIndex,
53
+ sessionTargetId: null,
54
+ type: 'session-picker',
55
+ },
56
+ sessions: state.sessions.map((session) =>
57
+ session.id === action.sessionId
58
+ ? { ...session, name: action.name, updatedAt: new Date().toISOString() }
59
+ : session
60
+ ),
61
+ }
62
+ case 'delete-session-record': {
63
+ const newSessions = state.sessions.filter((session) => session.id !== action.sessionId)
64
+ const filteredNew = filterSessions(newSessions, state.modal.editBuffer)
65
+ const maxIndex = filteredNew.length
66
+ const clampedIndex = Math.min(state.modal.selectedIndex, maxIndex)
67
+ return {
68
+ ...state,
69
+ activeTabId: action.sessionId === state.currentSessionId ? null : state.activeTabId,
70
+ currentSessionId:
71
+ action.sessionId === state.currentSessionId ? null : state.currentSessionId,
72
+ focusMode: 'modal',
73
+ modal: {
74
+ editBuffer: null,
75
+ selectedIndex: clampedIndex,
76
+ sessionTargetId: null,
77
+ type: 'session-picker',
78
+ },
79
+ sessions: newSessions,
80
+ tabs: action.sessionId === state.currentSessionId ? [] : state.tabs,
81
+ }
82
+ }
83
+ default:
84
+ return null
85
+ }
86
+ }