@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,210 @@
1
+ import type { AppState, TabSession, TabStatus, WorkspaceSnapshotV1 } from './types'
2
+
3
+ import {
4
+ allLeafIds,
5
+ createGroupId,
6
+ createLeaf,
7
+ type LayoutNode,
8
+ pruneLayoutTree,
9
+ } from './layout-tree'
10
+
11
+ export function createEmptyWorkspaceSnapshot(): WorkspaceSnapshotV1 {
12
+ return {
13
+ activeTabId: null,
14
+ savedAt: new Date().toISOString(),
15
+ sidebar: {
16
+ visible: true,
17
+ width: 28,
18
+ },
19
+ tabs: [],
20
+ version: 1,
21
+ }
22
+ }
23
+
24
+ function getDisconnectedStatus(status: WorkspaceSnapshotV1['tabs'][number]['status']): TabStatus {
25
+ if (status === 'running' || status === 'starting') {
26
+ return 'disconnected'
27
+ }
28
+
29
+ return status
30
+ }
31
+
32
+ export function serializeWorkspace(state: AppState): WorkspaceSnapshotV1 {
33
+ return {
34
+ activeTabId: state.activeTabId,
35
+ layoutTree: Object.values(state.layoutTrees)[0] ?? undefined,
36
+ layoutTrees: Object.keys(state.layoutTrees).length > 0 ? state.layoutTrees : undefined,
37
+ savedAt: new Date().toISOString(),
38
+ sidebar: {
39
+ visible: state.sidebar.visible,
40
+ width: state.sidebar.width,
41
+ },
42
+ tabGroupMap: Object.keys(state.tabGroupMap).length > 0 ? state.tabGroupMap : undefined,
43
+ tabs: state.tabs.map((tab) => ({
44
+ assistant: tab.assistant,
45
+ buffer: tab.buffer,
46
+ command: tab.command,
47
+ errorMessage: tab.errorMessage,
48
+ exitCode: tab.exitCode,
49
+ id: tab.id,
50
+ status: tab.status === 'disconnected' ? 'running' : tab.status,
51
+ terminalModes: tab.terminalModes,
52
+ title: tab.title,
53
+ viewport: tab.viewport,
54
+ })),
55
+ version: 1,
56
+ }
57
+ }
58
+
59
+ export function restoreTabsFromWorkspace(snapshot: WorkspaceSnapshotV1 | undefined): TabSession[] {
60
+ if (!snapshot || snapshot.version !== 1) {
61
+ return []
62
+ }
63
+
64
+ return snapshot.tabs.map((tab) => ({
65
+ activity: 'idle',
66
+ assistant: tab.assistant,
67
+ buffer: tab.buffer,
68
+ command: tab.command,
69
+ errorMessage: tab.errorMessage,
70
+ exitCode: tab.exitCode,
71
+ id: tab.id,
72
+ status: getDisconnectedStatus(tab.status),
73
+ terminalModes: tab.terminalModes,
74
+ title: tab.title,
75
+ viewport: tab.viewport,
76
+ }))
77
+ }
78
+
79
+ export function restoreLayoutTree(
80
+ snapshot: WorkspaceSnapshotV1 | undefined,
81
+ tabs: TabSession[]
82
+ ): LayoutNode | null {
83
+ if (snapshot?.layoutTree) {
84
+ const validTabIds = new Set(tabs.map((t) => t.id))
85
+ const pruned = pruneLayoutTree(snapshot.layoutTree, validTabIds)
86
+ if (pruned) return pruned
87
+ }
88
+ // Fallback: single leaf for the first tab
89
+ return tabs[0] ? createLeaf(tabs[0].id) : null
90
+ }
91
+
92
+ export function restoreLayoutTrees(
93
+ snapshot: WorkspaceSnapshotV1 | undefined,
94
+ tabs: TabSession[]
95
+ ): { layoutTrees: Record<string, LayoutNode>; tabGroupMap: Record<string, string> } {
96
+ const validTabIds = new Set(tabs.map((t) => t.id))
97
+ const layoutTrees: Record<string, LayoutNode> = {}
98
+ const tabGroupMap: Record<string, string> = {}
99
+
100
+ if (snapshot?.layoutTrees && snapshot?.tabGroupMap) {
101
+ // New format
102
+ for (const [gId, tree] of Object.entries(snapshot.layoutTrees)) {
103
+ const pruned = pruneLayoutTree(tree, validTabIds)
104
+ if (pruned && pruned.type === 'split') {
105
+ layoutTrees[gId] = pruned
106
+ for (const leafId of allLeafIds(pruned)) {
107
+ tabGroupMap[leafId] = gId
108
+ }
109
+ }
110
+ }
111
+ } else if (snapshot?.layoutTree) {
112
+ // Legacy migration
113
+ const pruned = pruneLayoutTree(snapshot.layoutTree, validTabIds)
114
+ if (pruned && pruned.type === 'split') {
115
+ const gId = createGroupId()
116
+ layoutTrees[gId] = pruned
117
+ for (const leafId of allLeafIds(pruned)) {
118
+ tabGroupMap[leafId] = gId
119
+ }
120
+ }
121
+ }
122
+
123
+ return { layoutTrees, tabGroupMap }
124
+ }
125
+
126
+ export function normalizeGroupedTabOrder(
127
+ tabs: TabSession[],
128
+ layoutTrees: Record<string, LayoutNode>,
129
+ tabGroupMap: Record<string, string>
130
+ ): TabSession[] {
131
+ const groupedTabsByGroupId = new Map<string, TabSession[]>()
132
+ const emittedGroupIds = new Set<string>()
133
+ const orderedTabs: TabSession[] = []
134
+
135
+ for (const tab of tabs) {
136
+ const groupId = tabGroupMap[tab.id]
137
+ if (!groupId) {
138
+ continue
139
+ }
140
+
141
+ const groupTree = layoutTrees[groupId]
142
+ if (!groupTree || groupTree.type !== 'split') {
143
+ continue
144
+ }
145
+
146
+ const groupedTabs = groupedTabsByGroupId.get(groupId)
147
+ if (groupedTabs) {
148
+ groupedTabs.push(tab)
149
+ continue
150
+ }
151
+
152
+ groupedTabsByGroupId.set(groupId, [tab])
153
+ }
154
+
155
+ for (const tab of tabs) {
156
+ const groupId = tabGroupMap[tab.id]
157
+ const groupTree = groupId ? layoutTrees[groupId] : undefined
158
+
159
+ if (!groupId || !groupTree || groupTree.type !== 'split') {
160
+ orderedTabs.push(tab)
161
+ continue
162
+ }
163
+
164
+ if (emittedGroupIds.has(groupId)) {
165
+ continue
166
+ }
167
+
168
+ emittedGroupIds.add(groupId)
169
+ const groupedTabs = groupedTabsByGroupId.get(groupId)
170
+ if (!groupedTabs) {
171
+ continue
172
+ }
173
+
174
+ for (const groupTab of groupedTabs) {
175
+ orderedTabs.push(groupTab)
176
+ }
177
+ }
178
+
179
+ return orderedTabs
180
+ }
181
+
182
+ export function restoreWorkspaceState(
183
+ state: AppState,
184
+ workspaceSnapshot: WorkspaceSnapshotV1 | undefined
185
+ ): Pick<
186
+ AppState,
187
+ 'tabs' | 'activeTabId' | 'focusMode' | 'sidebar' | 'layoutTrees' | 'tabGroupMap'
188
+ > {
189
+ const tabs = restoreTabsFromWorkspace(workspaceSnapshot)
190
+ const activeTabId =
191
+ workspaceSnapshot?.activeTabId && tabs.some((tab) => tab.id === workspaceSnapshot.activeTabId)
192
+ ? workspaceSnapshot.activeTabId
193
+ : (tabs[0]?.id ?? null)
194
+
195
+ const { layoutTrees, tabGroupMap } = restoreLayoutTrees(workspaceSnapshot, tabs)
196
+ const orderedTabs = normalizeGroupedTabOrder(tabs, layoutTrees, tabGroupMap)
197
+
198
+ return {
199
+ activeTabId,
200
+ focusMode: 'navigation',
201
+ layoutTrees,
202
+ sidebar: {
203
+ ...state.sidebar,
204
+ visible: workspaceSnapshot?.sidebar.visible ?? state.sidebar.visible,
205
+ width: workspaceSnapshot?.sidebar.width ?? state.sidebar.width,
206
+ },
207
+ tabGroupMap,
208
+ tabs: orderedTabs,
209
+ }
210
+ }
@@ -0,0 +1,90 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { dirname, join } from 'node:path'
3
+
4
+ import { CONFIG_PATH } from '../config'
5
+ import { logDebug } from '../debug/input-log'
6
+ import { isSnippetRecord } from './validation'
7
+
8
+ export interface SnippetRecord {
9
+ id: string
10
+ name: string
11
+ content: string
12
+ }
13
+
14
+ const SNIPPETS_PATH = join(dirname(CONFIG_PATH), 'aimux-snippets.json')
15
+
16
+ const DEFAULT_SNIPPETS: SnippetRecord[] = [
17
+ {
18
+ content: 'Review this code for bugs, security issues, and suggest improvements.',
19
+ id: 'default-review',
20
+ name: 'Code review',
21
+ },
22
+ {
23
+ content: 'Explain how this code works step by step.',
24
+ id: 'default-explain',
25
+ name: 'Explain',
26
+ },
27
+ {
28
+ content: 'Write comprehensive unit tests for this code with edge cases.',
29
+ id: 'default-tests',
30
+ name: 'Write tests',
31
+ },
32
+ {
33
+ content:
34
+ 'Refactor this code to improve readability and maintainability without changing behavior.',
35
+ id: 'default-refactor',
36
+ name: 'Refactor',
37
+ },
38
+ {
39
+ content: 'Analyze this error and provide a fix with explanation.',
40
+ id: 'default-fix',
41
+ name: 'Fix error',
42
+ },
43
+ ]
44
+
45
+ export function loadSnippetCatalog(): SnippetRecord[] {
46
+ try {
47
+ if (!existsSync(SNIPPETS_PATH)) {
48
+ saveSnippetCatalog(DEFAULT_SNIPPETS)
49
+ return DEFAULT_SNIPPETS
50
+ }
51
+ const parsed = JSON.parse(readFileSync(SNIPPETS_PATH, 'utf8')) as {
52
+ version?: unknown
53
+ snippets?: unknown
54
+ }
55
+ if (parsed.version !== 1 || !Array.isArray(parsed.snippets)) {
56
+ logDebug('snippets.catalog.loadIssue', {
57
+ issue: 'invalid snippet catalog header',
58
+ path: SNIPPETS_PATH,
59
+ })
60
+ return []
61
+ }
62
+ if (!parsed.snippets.every(isSnippetRecord)) {
63
+ logDebug('snippets.catalog.loadIssue', {
64
+ issue: 'invalid snippet catalog entries',
65
+ path: SNIPPETS_PATH,
66
+ })
67
+ return []
68
+ }
69
+ return parsed.snippets
70
+ } catch (error) {
71
+ logDebug('snippets.catalog.loadIssue', {
72
+ issue: error instanceof Error ? error.message : String(error),
73
+ path: SNIPPETS_PATH,
74
+ })
75
+ return []
76
+ }
77
+ }
78
+
79
+ export function saveSnippetCatalog(snippets: SnippetRecord[]): void {
80
+ try {
81
+ mkdirSync(dirname(SNIPPETS_PATH), { recursive: true })
82
+ writeFileSync(SNIPPETS_PATH, `${JSON.stringify({ snippets, version: 1 }, null, 2)}\n`)
83
+ } catch (error) {
84
+ logDebug('snippets.catalog.saveError', {
85
+ error: error instanceof Error ? error.message : String(error),
86
+ path: SNIPPETS_PATH,
87
+ snippetCount: snippets.length,
88
+ })
89
+ }
90
+ }
@@ -0,0 +1,93 @@
1
+ import type { AppAction, AppState, SessionRecord, SnippetRecord } from './types'
2
+
3
+ import { emptyGitPanel, reduceGitPanelState } from './reducers/git-panel-state'
4
+ import { emptyModal, reduceModalState } from './reducers/modal-state'
5
+ import { reduceSessionState } from './reducers/session-state'
6
+ import { reduceTabState } from './reducers/tab-state'
7
+ import { reduceUIState } from './reducers/ui-state'
8
+ import { filterSnippets } from './selectors'
9
+
10
+ const DEFAULT_SIDEBAR_WIDTH = 28
11
+ const DEFAULT_SIDEBAR_MIN_WIDTH = 18
12
+ const DEFAULT_SIDEBAR_MAX_WIDTH = 42
13
+ const DEFAULT_TERMINAL_COLS = 80
14
+ const DEFAULT_TERMINAL_ROWS = 24
15
+
16
+ export interface InitialStateOverrides {
17
+ gitPanelVisible?: boolean
18
+ gitPanelRatio?: number
19
+ }
20
+
21
+ export function createInitialState(
22
+ customCommands: Record<string, string> = {},
23
+ sessions: SessionRecord[] = [],
24
+ snippets: SnippetRecord[] = [],
25
+ showSessionPicker = false,
26
+ overrides: InitialStateOverrides = {}
27
+ ): AppState {
28
+ return {
29
+ activeTabId: null,
30
+ currentSessionId: null,
31
+ customCommands,
32
+ focusMode: showSessionPicker ? 'modal' : 'navigation',
33
+ gitPanel: emptyGitPanel(),
34
+ layout: {
35
+ terminalCols: DEFAULT_TERMINAL_COLS,
36
+ terminalRows: DEFAULT_TERMINAL_ROWS,
37
+ },
38
+ layoutTrees: {},
39
+ modal: showSessionPicker
40
+ ? { editBuffer: null, selectedIndex: 0, sessionTargetId: null, type: 'session-picker' }
41
+ : emptyModal(),
42
+ pendingChords: null,
43
+ sessions,
44
+ sidebar: {
45
+ gitPanelRatio: overrides.gitPanelRatio ?? 0.5,
46
+ gitPanelVisible: overrides.gitPanelVisible ?? true,
47
+ maxWidth: DEFAULT_SIDEBAR_MAX_WIDTH,
48
+ minWidth: DEFAULT_SIDEBAR_MIN_WIDTH,
49
+ visible: true,
50
+ width: DEFAULT_SIDEBAR_WIDTH,
51
+ },
52
+ snippets,
53
+ tabGroupMap: {},
54
+ tabs: [],
55
+ }
56
+ }
57
+
58
+ export function appReducer(state: AppState, action: AppAction): AppState {
59
+ const sessionState = reduceSessionState(state, action)
60
+ if (sessionState) return sessionState
61
+
62
+ const tabState = reduceTabState(state, action)
63
+ if (tabState) return tabState
64
+
65
+ const modalState = reduceModalState(state, action)
66
+ if (modalState) return modalState
67
+
68
+ const uiState = reduceUIState(state, action)
69
+ if (uiState) return uiState
70
+
71
+ const gitPanelState = reduceGitPanelState(state, action)
72
+ if (gitPanelState) return gitPanelState
73
+
74
+ switch (action.type) {
75
+ case 'set-snippets':
76
+ return { ...state, snippets: action.snippets }
77
+ case 'delete-snippet': {
78
+ const newSnippets = state.snippets.filter((s) => s.id !== action.snippetId)
79
+ const filteredNew = filterSnippets(newSnippets, state.modal.editBuffer)
80
+ const maxIndex = Math.max(0, filteredNew.length - 1)
81
+ return {
82
+ ...state,
83
+ modal: {
84
+ ...state.modal,
85
+ selectedIndex: Math.min(state.modal.selectedIndex, maxIndex),
86
+ },
87
+ snippets: newSnippets,
88
+ }
89
+ }
90
+ default:
91
+ return state
92
+ }
93
+ }
@@ -0,0 +1,11 @@
1
+ import type { TerminalModeState } from './types'
2
+
3
+ export function createDefaultTerminalModes(): TerminalModeState {
4
+ return {
5
+ alternateScrollMode: false,
6
+ bracketedPasteMode: false,
7
+ isAlternateBuffer: false,
8
+ mouseTrackingMode: 'none',
9
+ sendFocusMode: false,
10
+ }
11
+ }