@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,69 @@
1
+ import type { TabSession } from '../state/types'
2
+
3
+ import { extractStreamText } from '../input/terminal-text-extraction'
4
+
5
+ interface PositionedNode {
6
+ parent?: unknown
7
+ x: number
8
+ y: number
9
+ }
10
+
11
+ export interface OtuiSelection {
12
+ isDragging?: boolean
13
+ anchor: { x: number; y: number }
14
+ focus: { x: number; y: number }
15
+ touchedRenderables?: unknown[]
16
+ getSelectedText(): string
17
+ }
18
+
19
+ function isPositionedNode(value: unknown): value is PositionedNode {
20
+ return (
21
+ typeof value === 'object' &&
22
+ value !== null &&
23
+ typeof Reflect.get(value, 'x') === 'number' &&
24
+ typeof Reflect.get(value, 'y') === 'number'
25
+ )
26
+ }
27
+
28
+ export function computeStreamSelectedText(
29
+ selection: OtuiSelection,
30
+ lines: { spans: { text: string }[] }[]
31
+ ): string | null {
32
+ const touched = selection.touchedRenderables
33
+ if (!touched || touched.length === 0) return null
34
+
35
+ const viewportText = touched[0]
36
+ if (!isPositionedNode(viewportText)) return null
37
+
38
+ const originX = viewportText.x
39
+ const originY = viewportText.y
40
+
41
+ const anchorRow = selection.anchor.y - originY
42
+ const anchorCol = selection.anchor.x - originX
43
+ const focusRow = selection.focus.y - originY
44
+ const focusCol = selection.focus.x - originX
45
+
46
+ return extractStreamText(
47
+ lines as Parameters<typeof extractStreamText>[0],
48
+ anchorRow,
49
+ anchorCol,
50
+ focusRow,
51
+ focusCol
52
+ )
53
+ }
54
+
55
+ export function resolveSelectionClipboardText(
56
+ selection: OtuiSelection,
57
+ tab: TabSession | undefined
58
+ ): { selectedText: string; streamLength: number | null; fallbackLength: number } {
59
+ const fallback = selection.getSelectedText()
60
+ const streamText =
61
+ tab?.viewport && tab.viewport.lines.length > 0
62
+ ? computeStreamSelectedText(selection, tab.viewport.lines)
63
+ : null
64
+ return {
65
+ fallbackLength: fallback.length,
66
+ selectedText: streamText ?? fallback,
67
+ streamLength: streamText?.length ?? null,
68
+ }
69
+ }
@@ -0,0 +1,143 @@
1
+ interface SelectableLike {
2
+ selectable?: boolean
3
+ isDestroyed?: boolean
4
+ }
5
+
6
+ function isUsableTarget(target: SelectableLike | null | undefined): target is SelectableLike {
7
+ return Boolean(target?.selectable) && !target?.isDestroyed
8
+ }
9
+
10
+ interface SelectionLike {
11
+ isActive: boolean
12
+ isDragging: boolean
13
+ anchor: { x: number; y: number }
14
+ focus: { x: number; y: number }
15
+ selectedRenderables: SelectableLike[]
16
+ touchedRenderables: SelectableLike[]
17
+ }
18
+
19
+ export interface RendererSelectionApi {
20
+ getSelection(): SelectionLike | null
21
+ startSelection(target: unknown, x: number, y: number): void
22
+ updateSelection(target: unknown, x: number, y: number, opts: { finishDragging: boolean }): void
23
+ clearSelection(): void
24
+ }
25
+
26
+ interface ShiftState {
27
+ target: SelectableLike | null
28
+ anchor: { x: number; y: number } | null
29
+ focus: { x: number; y: number } | null
30
+ }
31
+
32
+ const states = new WeakMap<object, ShiftState>()
33
+
34
+ function getState(renderer: object): ShiftState {
35
+ let s = states.get(renderer)
36
+ if (!s) {
37
+ s = { anchor: null, focus: null, target: null }
38
+ states.set(renderer, s)
39
+ }
40
+ return s
41
+ }
42
+
43
+ export function resetSelectionShiftState(renderer: object): void {
44
+ states.set(renderer, { anchor: null, focus: null, target: null })
45
+ }
46
+
47
+ function pickSelectionTarget(selection: SelectionLike): SelectableLike | null {
48
+ for (const r of selection.selectedRenderables) {
49
+ if (isUsableTarget(r)) return r
50
+ }
51
+ for (const r of selection.touchedRenderables) {
52
+ if (isUsableTarget(r)) return r
53
+ }
54
+ return null
55
+ }
56
+
57
+ function liveDiffersFromCache(live: SelectionLike, state: ShiftState): boolean {
58
+ if (!state.anchor || !state.focus) return true
59
+ return (
60
+ live.anchor.x !== state.anchor.x ||
61
+ live.anchor.y !== state.anchor.y ||
62
+ live.focus.x !== state.focus.x ||
63
+ live.focus.y !== state.focus.y
64
+ )
65
+ }
66
+
67
+ export function shiftSelectionByScroll(renderer: RendererSelectionApi, deltaLines: number): void {
68
+ if (deltaLines === 0) return
69
+
70
+ const live = renderer.getSelection()
71
+
72
+ if (!live?.isActive) {
73
+ resetSelectionShiftState(renderer)
74
+ return
75
+ }
76
+
77
+ if (live.isDragging) {
78
+ // Mid-drag (e.g. user holds the mouse and scrolls past the viewport edge to
79
+ // extend the selection): only the anchor needs to follow the content. The
80
+ // focus is owned by opentui's live pointer tracking, which keeps it pinned
81
+ // to the cursor in screen-space — leave it alone.
82
+ const target = pickSelectionTarget(live)
83
+ if (!isUsableTarget(target)) return
84
+ const anchorY = live.anchor.y - deltaLines
85
+ renderer.startSelection(target, live.anchor.x, anchorY)
86
+ renderer.updateSelection(target, live.focus.x, live.focus.y, { finishDragging: false })
87
+ resetSelectionShiftState(renderer)
88
+ return
89
+ }
90
+
91
+ const state = getState(renderer)
92
+
93
+ if (liveDiffersFromCache(live, state)) {
94
+ const target = pickSelectionTarget(live) ?? (isUsableTarget(state.target) ? state.target : null)
95
+ if (target) {
96
+ state.target = target
97
+ state.anchor = { x: live.anchor.x, y: live.anchor.y }
98
+ state.focus = { x: live.focus.x, y: live.focus.y }
99
+ }
100
+ }
101
+
102
+ if (!isUsableTarget(state.target) || !state.anchor || !state.focus) {
103
+ if (!isUsableTarget(state.target)) {
104
+ resetSelectionShiftState(renderer)
105
+ }
106
+ return
107
+ }
108
+
109
+ const anchor = { x: state.anchor.x, y: state.anchor.y - deltaLines }
110
+ const focus = { x: state.focus.x, y: state.focus.y - deltaLines }
111
+ state.anchor = anchor
112
+ state.focus = focus
113
+
114
+ renderer.startSelection(state.target, anchor.x, anchor.y)
115
+ renderer.updateSelection(state.target, focus.x, focus.y, { finishDragging: true })
116
+ }
117
+
118
+ export interface ViewportObservation {
119
+ tabId: string
120
+ y: number
121
+ }
122
+
123
+ export function applyViewportObservation(
124
+ renderer: RendererSelectionApi,
125
+ prior: ViewportObservation | null,
126
+ next: ViewportObservation | null
127
+ ): ViewportObservation | null {
128
+ if (!next) {
129
+ if (prior !== null) resetSelectionShiftState(renderer)
130
+ return null
131
+ }
132
+
133
+ if (!prior || prior.tabId !== next.tabId) {
134
+ resetSelectionShiftState(renderer)
135
+ return next
136
+ }
137
+
138
+ const delta = next.y - prior.y
139
+ if (delta !== 0) {
140
+ shiftSelectionByScroll(renderer, delta)
141
+ }
142
+ return next
143
+ }
@@ -0,0 +1,170 @@
1
+ import type { SessionBackend } from '../session-backend/types'
2
+ import type { AppAction, AppState, SessionRecord, TabSession } from '../state/types'
3
+
4
+ import { logInputDebug } from '../debug/input-log'
5
+ import { createPrefixedId } from '../platform/id'
6
+ import { saveSessionCatalog } from '../state/session-catalog'
7
+ import { createEmptyWorkspaceSnapshot, serializeWorkspace } from '../state/session-persistence'
8
+ import { buildSessionsWithCurrentSnapshot } from '../state/workspace-save'
9
+
10
+ export function createSessionFromCurrentState(
11
+ state: AppState,
12
+ name: string,
13
+ projectPath?: string
14
+ ): { session: SessionRecord; sessions: SessionRecord[] } {
15
+ const now = new Date().toISOString()
16
+ const workspaceSnapshot =
17
+ state.currentSessionId || state.tabs.length === 0
18
+ ? createEmptyWorkspaceSnapshot()
19
+ : serializeWorkspace(state)
20
+ const session: SessionRecord = {
21
+ createdAt: now,
22
+ id: createPrefixedId('session'),
23
+ lastOpenedAt: now,
24
+ name,
25
+ projectPath,
26
+ updatedAt: now,
27
+ workspaceSnapshot,
28
+ }
29
+
30
+ let updatedSessions = state.sessions
31
+ if (state.currentSessionId) {
32
+ const currentSnapshot = serializeWorkspace(state)
33
+ updatedSessions = state.sessions.map((entry) =>
34
+ entry.id === state.currentSessionId
35
+ ? { ...entry, updatedAt: now, workspaceSnapshot: currentSnapshot }
36
+ : entry
37
+ )
38
+ }
39
+
40
+ return {
41
+ session,
42
+ sessions: [...updatedSessions, session],
43
+ }
44
+ }
45
+
46
+ export function renameSessionRecords(
47
+ sessions: SessionRecord[],
48
+ sessionId: string,
49
+ name: string
50
+ ): SessionRecord[] {
51
+ return sessions.map((session) =>
52
+ session.id === sessionId ? { ...session, name, updatedAt: new Date().toISOString() } : session
53
+ )
54
+ }
55
+
56
+ export function switchSessionRecords(state: AppState, session: SessionRecord): SessionRecord[] {
57
+ const sessionsWithSnapshot = buildSessionsWithCurrentSnapshot(
58
+ state.sessions,
59
+ state.currentSessionId,
60
+ state
61
+ )
62
+ return sessionsWithSnapshot.map((entry) =>
63
+ entry.id === session.id ? { ...entry, lastOpenedAt: new Date().toISOString() } : entry
64
+ )
65
+ }
66
+
67
+ export function deleteSessionRecords(
68
+ sessions: SessionRecord[],
69
+ sessionId: string
70
+ ): SessionRecord[] {
71
+ return sessions.filter((session) => session.id !== sessionId)
72
+ }
73
+
74
+ export function handleCreateSessionEffect(
75
+ state: AppState,
76
+ dispatch: (action: AppAction) => void,
77
+ name: string,
78
+ projectPath?: string
79
+ ): void {
80
+ const { session, sessions } = createSessionFromCurrentState(state, name, projectPath)
81
+ logInputDebug('app.session.create', {
82
+ fromCurrentWorkspace: !state.currentSessionId && state.tabs.length > 0,
83
+ name,
84
+ sessionId: session.id,
85
+ tabCount: session.workspaceSnapshot?.tabs.length ?? 0,
86
+ })
87
+ saveSessionCatalog(sessions)
88
+ dispatch({ sessions, type: 'set-sessions' })
89
+ dispatch({
90
+ sessionId: session.id,
91
+ type: 'load-session',
92
+ workspaceSnapshot: session.workspaceSnapshot,
93
+ })
94
+ }
95
+
96
+ export function handleRenameSessionEffect(
97
+ sessions: SessionRecord[],
98
+ dispatch: (action: AppAction) => void,
99
+ sessionId: string,
100
+ name: string
101
+ ): void {
102
+ logInputDebug('app.session.rename', { name, sessionId })
103
+ const renamed = renameSessionRecords(sessions, sessionId, name)
104
+ saveSessionCatalog(renamed)
105
+ dispatch({ name, sessionId, type: 'rename-session-record' })
106
+ }
107
+
108
+ export function handleSwitchSessionEffect(
109
+ state: AppState,
110
+ backend: SessionBackend,
111
+ dispatch: (action: AppAction) => void,
112
+ session: SessionRecord
113
+ ): void {
114
+ logInputDebug('app.session.switch.start', {
115
+ currentTabCount: state.tabs.length,
116
+ fromSessionId: state.currentSessionId,
117
+ restoredTabCount: session.workspaceSnapshot?.tabs.length ?? 0,
118
+ toName: session.name,
119
+ toSessionId: session.id,
120
+ })
121
+ const sessions = switchSessionRecords(state, session)
122
+ saveSessionCatalog(sessions)
123
+ void backend.destroy(true)
124
+ dispatch({ sessions, type: 'set-sessions' })
125
+ dispatch({
126
+ sessionId: session.id,
127
+ type: 'load-session',
128
+ workspaceSnapshot: session.workspaceSnapshot,
129
+ })
130
+ logInputDebug('app.session.switch.dispatched', { toSessionId: session.id })
131
+ }
132
+
133
+ export function handleDeleteSessionEffect(
134
+ state: AppState,
135
+ backend: SessionBackend,
136
+ dispatch: (action: AppAction) => void,
137
+ sessionId: string
138
+ ): void {
139
+ const remaining = deleteSessionRecords(state.sessions, sessionId)
140
+ logInputDebug('app.session.delete', {
141
+ remainingCount: remaining.length,
142
+ sessionId,
143
+ wasCurrent: sessionId === state.currentSessionId,
144
+ })
145
+ saveSessionCatalog(remaining)
146
+ if (sessionId === state.currentSessionId) {
147
+ void backend.destroy(true)
148
+ }
149
+ dispatch({ sessionId, type: 'delete-session-record' })
150
+ }
151
+
152
+ export function restartTabSession(
153
+ backend: SessionBackend,
154
+ dispatch: (action: AppAction) => void,
155
+ clearIdleTimer: (tabId: string) => void,
156
+ clearStartupGrace: (tabId: string) => void,
157
+ startTabSession: (tab: TabSession) => void,
158
+ tab: TabSession
159
+ ): void {
160
+ logInputDebug('app.restartTab', {
161
+ command: tab.command,
162
+ status: tab.status,
163
+ tabId: tab.id,
164
+ })
165
+ clearIdleTimer(tab.id)
166
+ clearStartupGrace(tab.id)
167
+ backend.disposeSession(tab.id)
168
+ dispatch({ tabId: tab.id, type: 'reset-tab-session' })
169
+ startTabSession(tab)
170
+ }