@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.
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/package.json +79 -0
- package/src/app-runtime/backend-attach-runtime.ts +135 -0
- package/src/app-runtime/backend-runtime-events.ts +78 -0
- package/src/app-runtime/click-selection-resolver.ts +130 -0
- package/src/app-runtime/pty-write.ts +32 -0
- package/src/app-runtime/render-invalidation.ts +20 -0
- package/src/app-runtime/selection-clipboard.ts +69 -0
- package/src/app-runtime/selection-scroll.ts +143 -0
- package/src/app-runtime/session-actions.ts +170 -0
- package/src/app-runtime/side-effects.ts +434 -0
- package/src/app-runtime/snippet-actions.ts +90 -0
- package/src/app-runtime/split-drag-controller.ts +15 -0
- package/src/app-runtime/tab-runtime-timeouts.ts +86 -0
- package/src/app-runtime/terminal-mouse-adapter.ts +31 -0
- package/src/app-runtime/use-backend-runtime.ts +99 -0
- package/src/app-runtime/use-directory-search.ts +52 -0
- package/src/app-runtime/use-mouse-handlers.ts +183 -0
- package/src/app-runtime/use-renderer-bindings.ts +163 -0
- package/src/app-runtime/use-terminal-resize.ts +141 -0
- package/src/app-runtime/use-workspace-autosave.ts +39 -0
- package/src/app.tsx +247 -0
- package/src/config/loader.ts +36 -0
- package/src/config.ts +146 -0
- package/src/daemon/daemon.ts +236 -0
- package/src/daemon/runtime-paths.ts +69 -0
- package/src/daemon/session-manager.ts +109 -0
- package/src/daemon/session-registry.ts +207 -0
- package/src/debug/input-log.ts +29 -0
- package/src/doctor.ts +106 -0
- package/src/git/git-poller.ts +49 -0
- package/src/git/git-status.ts +183 -0
- package/src/index.tsx +56 -0
- package/src/input/keymap/build-handlers.ts +34 -0
- package/src/input/keymap/key-chord.ts +201 -0
- package/src/input/keymap/keymap-mode-handler.ts +88 -0
- package/src/input/keymap/sequence-resolver.ts +117 -0
- package/src/input/keymap/trie.ts +103 -0
- package/src/input/modes/bridge.ts +58 -0
- package/src/input/modes/handlers/index.ts +18 -0
- package/src/input/modes/handlers/shared.ts +70 -0
- package/src/input/modes/registry.ts +34 -0
- package/src/input/modes/transitions.ts +37 -0
- package/src/input/modes/types.ts +63 -0
- package/src/input/mouse-forwarding.ts +98 -0
- package/src/input/multi-click-detector.ts +55 -0
- package/src/input/paste.ts +12 -0
- package/src/input/raw-input-handler.ts +191 -0
- package/src/input/terminal-text-extraction.ts +78 -0
- package/src/ipc/protocol.ts +341 -0
- package/src/platform/clipboard.ts +13 -0
- package/src/platform/daemon-control.ts +62 -0
- package/src/platform/id.ts +7 -0
- package/src/platform/project-search.ts +75 -0
- package/src/pty/command-registry.ts +69 -0
- package/src/pty/pty-manager.ts +268 -0
- package/src/pty/terminal-snapshot.ts +210 -0
- package/src/restart-daemon.ts +28 -0
- package/src/session-backend/bootstrap.ts +107 -0
- package/src/session-backend/local-session-backend.ts +164 -0
- package/src/session-backend/remote-session-backend.ts +373 -0
- package/src/session-backend/types.ts +47 -0
- package/src/state/app-store.ts +19 -0
- package/src/state/layout-resize.ts +56 -0
- package/src/state/layout-tree.ts +323 -0
- package/src/state/reducers/git-panel-state.ts +102 -0
- package/src/state/reducers/modal-state.ts +358 -0
- package/src/state/reducers/session-state.ts +86 -0
- package/src/state/reducers/tab-state.ts +531 -0
- package/src/state/reducers/ui-state.ts +29 -0
- package/src/state/selectors.ts +30 -0
- package/src/state/session-catalog.ts +96 -0
- package/src/state/session-persistence.ts +210 -0
- package/src/state/snippet-catalog.ts +90 -0
- package/src/state/store.ts +93 -0
- package/src/state/terminal-modes.ts +11 -0
- package/src/state/types.ts +367 -0
- package/src/state/validation.ts +154 -0
- package/src/state/workspace-save.ts +33 -0
- package/src/ui/components/create-session-modal.tsx +100 -0
- package/src/ui/components/git-panel.tsx +200 -0
- package/src/ui/components/help-modal.tsx +79 -0
- package/src/ui/components/input-field.tsx +18 -0
- package/src/ui/components/list-item.tsx +30 -0
- package/src/ui/components/modal-filter-bar.tsx +18 -0
- package/src/ui/components/modal-shell.tsx +47 -0
- package/src/ui/components/new-tab-modal.tsx +52 -0
- package/src/ui/components/pending-chord-indicator.tsx +41 -0
- package/src/ui/components/session-name-modal.tsx +15 -0
- package/src/ui/components/session-picker-modal.tsx +93 -0
- package/src/ui/components/sidebar-group-metadata.ts +44 -0
- package/src/ui/components/sidebar-scroll.ts +33 -0
- package/src/ui/components/sidebar.tsx +225 -0
- package/src/ui/components/snippet-editor-modal.tsx +39 -0
- package/src/ui/components/snippet-picker-modal.tsx +56 -0
- package/src/ui/components/split-layout.tsx +201 -0
- package/src/ui/components/status-bar.tsx +60 -0
- package/src/ui/components/surface.tsx +63 -0
- package/src/ui/components/tab-item.tsx +118 -0
- package/src/ui/components/terminal-pane.tsx +215 -0
- package/src/ui/components/theme-picker-modal.tsx +35 -0
- package/src/ui/components/use-sidebar-auto-scroll.ts +63 -0
- package/src/ui/components/use-sidebar-branch.ts +36 -0
- package/src/ui/directory-search.ts +1 -0
- package/src/ui/git-branch.ts +11 -0
- package/src/ui/path-format.ts +6 -0
- package/src/ui/root.tsx +261 -0
- package/src/ui/status-bar-model.ts +87 -0
- package/src/ui/theme.ts +9 -0
- package/src/ui/themes.ts +255 -0
- package/src/ui/ui-tokens.ts +11 -0
- package/src/update.ts +62 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
export type BuiltinAssistantId = 'claude' | 'codex' | 'opencode' | 'terminal'
|
|
2
|
+
|
|
3
|
+
export type AssistantId = BuiltinAssistantId | (string & {})
|
|
4
|
+
|
|
5
|
+
export type TabStatus = 'starting' | 'running' | 'disconnected' | 'exited' | 'error'
|
|
6
|
+
|
|
7
|
+
export type TabActivity = 'busy' | 'idle'
|
|
8
|
+
|
|
9
|
+
export type FocusMode = 'navigation' | 'terminal-input' | 'modal' | 'command-edit' | 'layout'
|
|
10
|
+
|
|
11
|
+
export type ModalType =
|
|
12
|
+
| 'new-tab'
|
|
13
|
+
| 'session-picker'
|
|
14
|
+
| 'session-name'
|
|
15
|
+
| 'create-session'
|
|
16
|
+
| 'rename-tab'
|
|
17
|
+
| 'snippet-picker'
|
|
18
|
+
| 'snippet-editor'
|
|
19
|
+
| 'theme-picker'
|
|
20
|
+
| 'help'
|
|
21
|
+
| 'split-picker'
|
|
22
|
+
| null
|
|
23
|
+
|
|
24
|
+
export interface TerminalSpan {
|
|
25
|
+
text: string
|
|
26
|
+
fg?: string
|
|
27
|
+
bg?: string
|
|
28
|
+
bold?: boolean
|
|
29
|
+
italic?: boolean
|
|
30
|
+
underline?: boolean
|
|
31
|
+
cursor?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TerminalLine {
|
|
35
|
+
spans: TerminalSpan[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TerminalSnapshot {
|
|
39
|
+
lines: TerminalLine[]
|
|
40
|
+
viewportY: number
|
|
41
|
+
baseY: number
|
|
42
|
+
cursorVisible: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface TerminalModeState {
|
|
46
|
+
mouseTrackingMode: 'none' | 'x10' | 'vt200' | 'drag' | 'any'
|
|
47
|
+
sendFocusMode: boolean
|
|
48
|
+
alternateScrollMode: boolean
|
|
49
|
+
isAlternateBuffer: boolean
|
|
50
|
+
bracketedPasteMode: boolean
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface PersistedTabSnapshot {
|
|
54
|
+
id: string
|
|
55
|
+
assistant: AssistantId
|
|
56
|
+
title: string
|
|
57
|
+
command: string
|
|
58
|
+
status: Exclude<TabStatus, 'disconnected'>
|
|
59
|
+
buffer: string
|
|
60
|
+
viewport?: TerminalSnapshot
|
|
61
|
+
terminalModes: TerminalModeState
|
|
62
|
+
errorMessage?: string
|
|
63
|
+
exitCode?: number
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface WorkspaceSnapshotV1 {
|
|
67
|
+
version: 1
|
|
68
|
+
savedAt: string
|
|
69
|
+
activeTabId: string | null
|
|
70
|
+
sidebar: {
|
|
71
|
+
visible: boolean
|
|
72
|
+
width: number
|
|
73
|
+
gitPanelVisible?: boolean
|
|
74
|
+
gitPanelRatio?: number
|
|
75
|
+
}
|
|
76
|
+
tabs: PersistedTabSnapshot[]
|
|
77
|
+
layoutTree?: import('./layout-tree').LayoutNode
|
|
78
|
+
layoutTrees?: Record<string, import('./layout-tree').LayoutNode>
|
|
79
|
+
tabGroupMap?: Record<string, string>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface SessionRecord {
|
|
83
|
+
id: string
|
|
84
|
+
name: string
|
|
85
|
+
projectPath?: string
|
|
86
|
+
createdAt: string
|
|
87
|
+
updatedAt: string
|
|
88
|
+
lastOpenedAt: string
|
|
89
|
+
workspaceSnapshot?: WorkspaceSnapshotV1
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface TabSession {
|
|
93
|
+
id: string
|
|
94
|
+
assistant: AssistantId
|
|
95
|
+
title: string
|
|
96
|
+
status: TabStatus
|
|
97
|
+
activity?: TabActivity
|
|
98
|
+
buffer: string
|
|
99
|
+
viewport?: TerminalSnapshot
|
|
100
|
+
terminalModes: TerminalModeState
|
|
101
|
+
command: string
|
|
102
|
+
errorMessage?: string
|
|
103
|
+
exitCode?: number
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface SidebarState {
|
|
107
|
+
visible: boolean
|
|
108
|
+
width: number
|
|
109
|
+
minWidth: number
|
|
110
|
+
maxWidth: number
|
|
111
|
+
gitPanelVisible: boolean
|
|
112
|
+
gitPanelRatio: number
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type GitFileStatus = 'M' | 'A' | 'D' | 'R' | 'C' | 'U' | '?'
|
|
116
|
+
|
|
117
|
+
export type GitFileSection = 'staged' | 'unstaged' | 'untracked'
|
|
118
|
+
|
|
119
|
+
export interface GitFileEntry {
|
|
120
|
+
path: string
|
|
121
|
+
renamedFrom?: string
|
|
122
|
+
section: GitFileSection
|
|
123
|
+
status: GitFileStatus
|
|
124
|
+
added: number | null
|
|
125
|
+
removed: number | null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export type GitPanelError = 'not-a-repo' | 'unknown'
|
|
129
|
+
|
|
130
|
+
export interface GitPanelState {
|
|
131
|
+
branch: string | null
|
|
132
|
+
ahead: number
|
|
133
|
+
behind: number
|
|
134
|
+
files: GitFileEntry[]
|
|
135
|
+
error: GitPanelError | null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface ModalBase {
|
|
139
|
+
selectedIndex: number
|
|
140
|
+
editBuffer: string | null
|
|
141
|
+
sessionTargetId: string | null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface ModalClosed extends ModalBase {
|
|
145
|
+
type: null
|
|
146
|
+
editBuffer: null
|
|
147
|
+
sessionTargetId: null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface ModalNewTab extends ModalBase {
|
|
151
|
+
type: 'new-tab'
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface ModalSessionPicker extends ModalBase {
|
|
155
|
+
type: 'session-picker'
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface ModalSessionName extends ModalBase {
|
|
159
|
+
type: 'session-name'
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface ModalRenameTab extends ModalBase {
|
|
163
|
+
type: 'rename-tab'
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface ModalSnippetPicker extends ModalBase {
|
|
167
|
+
type: 'snippet-picker'
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface ModalThemePicker extends ModalBase {
|
|
171
|
+
type: 'theme-picker'
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface ModalHelp extends ModalBase {
|
|
175
|
+
type: 'help'
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface ModalSplitPicker extends ModalBase {
|
|
179
|
+
type: 'split-picker'
|
|
180
|
+
splitDirection: import('./layout-tree').SplitDirection
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface ModalCreateSession extends ModalBase {
|
|
184
|
+
type: 'create-session'
|
|
185
|
+
directoryResults: DirectoryResult[]
|
|
186
|
+
pendingProjectPath: string | null
|
|
187
|
+
activeField: 'directory' | 'name'
|
|
188
|
+
nameBuffer: string
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface ModalSnippetEditor extends ModalBase {
|
|
192
|
+
type: 'snippet-editor'
|
|
193
|
+
activeField: 'name' | 'content'
|
|
194
|
+
contentBuffer: string
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export type DirectoryResultType = 'git-repo' | 'worktree' | 'workspace'
|
|
198
|
+
|
|
199
|
+
export interface DirectoryResult {
|
|
200
|
+
path: string
|
|
201
|
+
type: DirectoryResultType
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export type ModalState =
|
|
205
|
+
| ModalClosed
|
|
206
|
+
| ModalNewTab
|
|
207
|
+
| ModalSessionPicker
|
|
208
|
+
| ModalSessionName
|
|
209
|
+
| ModalRenameTab
|
|
210
|
+
| ModalSnippetPicker
|
|
211
|
+
| ModalThemePicker
|
|
212
|
+
| ModalHelp
|
|
213
|
+
| ModalSplitPicker
|
|
214
|
+
| ModalCreateSession
|
|
215
|
+
| ModalSnippetEditor
|
|
216
|
+
|
|
217
|
+
export interface LayoutState {
|
|
218
|
+
terminalCols: number
|
|
219
|
+
terminalRows: number
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export interface SnippetRecord {
|
|
223
|
+
id: string
|
|
224
|
+
name: string
|
|
225
|
+
content: string
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export interface AppState {
|
|
229
|
+
tabs: TabSession[]
|
|
230
|
+
activeTabId: string | null
|
|
231
|
+
layoutTrees: Record<string, import('./layout-tree').LayoutNode>
|
|
232
|
+
tabGroupMap: Record<string, string>
|
|
233
|
+
sessions: SessionRecord[]
|
|
234
|
+
currentSessionId: string | null
|
|
235
|
+
snippets: SnippetRecord[]
|
|
236
|
+
focusMode: FocusMode
|
|
237
|
+
sidebar: SidebarState
|
|
238
|
+
modal: ModalState
|
|
239
|
+
layout: LayoutState
|
|
240
|
+
customCommands: Record<AssistantId, string>
|
|
241
|
+
gitPanel: GitPanelState
|
|
242
|
+
/** Chord prefix the sequence resolver is currently waiting on, or null when idle. */
|
|
243
|
+
pendingChords: string[] | null
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// -- Modal actions --
|
|
247
|
+
export type ModalAction =
|
|
248
|
+
| { type: 'open-new-tab-modal' }
|
|
249
|
+
| { type: 'open-help-modal' }
|
|
250
|
+
| { type: 'open-split-picker'; direction: import('./layout-tree').SplitDirection }
|
|
251
|
+
| { type: 'open-session-picker' }
|
|
252
|
+
| { type: 'open-session-name-modal'; sessionTargetId?: string; initialName?: string }
|
|
253
|
+
| { type: 'close-modal' }
|
|
254
|
+
| { type: 'move-modal-selection'; delta: number }
|
|
255
|
+
| { type: 'begin-command-edit' }
|
|
256
|
+
| { type: 'update-command-edit'; char: string }
|
|
257
|
+
| { type: 'commit-command-edit' }
|
|
258
|
+
| { type: 'cancel-command-edit' }
|
|
259
|
+
| { type: 'open-create-session-modal' }
|
|
260
|
+
| { type: 'set-directory-results'; results: DirectoryResult[] }
|
|
261
|
+
| { type: 'switch-create-session-field' }
|
|
262
|
+
| { type: 'select-directory' }
|
|
263
|
+
| { type: 'begin-session-filter' }
|
|
264
|
+
| { type: 'open-rename-tab-modal' }
|
|
265
|
+
| { type: 'open-snippet-picker' }
|
|
266
|
+
| { type: 'open-snippet-editor'; snippetId?: string }
|
|
267
|
+
| { type: 'begin-snippet-filter' }
|
|
268
|
+
| { type: 'open-theme-picker' }
|
|
269
|
+
|
|
270
|
+
// -- Session actions --
|
|
271
|
+
export type SessionAction =
|
|
272
|
+
| { type: 'load-session'; sessionId: string; workspaceSnapshot?: WorkspaceSnapshotV1 }
|
|
273
|
+
| { type: 'set-sessions'; sessions: SessionRecord[] }
|
|
274
|
+
| { type: 'create-session-record'; session: SessionRecord }
|
|
275
|
+
| { type: 'rename-session-record'; sessionId: string; name: string }
|
|
276
|
+
| { type: 'delete-session-record'; sessionId: string }
|
|
277
|
+
|
|
278
|
+
// -- Tab actions --
|
|
279
|
+
export type TabAction =
|
|
280
|
+
| { type: 'add-tab'; tab: TabSession }
|
|
281
|
+
| {
|
|
282
|
+
type: 'hydrate-workspace'
|
|
283
|
+
tabs: TabSession[]
|
|
284
|
+
activeTabId: string | null
|
|
285
|
+
layoutTree?: import('./layout-tree').LayoutNode | null
|
|
286
|
+
layoutTrees?: Record<string, import('./layout-tree').LayoutNode>
|
|
287
|
+
tabGroupMap?: Record<string, string>
|
|
288
|
+
}
|
|
289
|
+
| { type: 'close-tab'; tabId: string }
|
|
290
|
+
| { type: 'close-active-tab' }
|
|
291
|
+
| { type: 'set-active-tab'; tabId: string }
|
|
292
|
+
| { type: 'move-active-tab'; delta: number }
|
|
293
|
+
| { type: 'reorder-active-tab'; delta: number }
|
|
294
|
+
| { type: 'reset-tab-session'; tabId: string }
|
|
295
|
+
| { type: 'rename-tab'; tabId: string; title: string }
|
|
296
|
+
| { type: 'append-tab-buffer'; tabId: string; chunk: string }
|
|
297
|
+
| {
|
|
298
|
+
type: 'replace-tab-viewport'
|
|
299
|
+
tabId: string
|
|
300
|
+
viewport: TerminalSnapshot
|
|
301
|
+
terminalModes: TerminalModeState
|
|
302
|
+
}
|
|
303
|
+
| { type: 'set-tab-activity'; tabId: string; activity?: TabActivity }
|
|
304
|
+
| { type: 'set-tab-status'; tabId: string; status: TabStatus; exitCode?: number }
|
|
305
|
+
| { type: 'set-tab-error'; tabId: string; message: string }
|
|
306
|
+
|
|
307
|
+
// -- Layout actions --
|
|
308
|
+
export type LayoutAction =
|
|
309
|
+
| {
|
|
310
|
+
type: 'split-pane'
|
|
311
|
+
direction: import('./layout-tree').SplitDirection
|
|
312
|
+
newTab: TabSession
|
|
313
|
+
}
|
|
314
|
+
| { type: 'close-pane'; tabId: string }
|
|
315
|
+
| {
|
|
316
|
+
type: 'focus-pane-direction'
|
|
317
|
+
direction: 'left' | 'right' | 'up' | 'down'
|
|
318
|
+
}
|
|
319
|
+
| {
|
|
320
|
+
type: 'resize-pane'
|
|
321
|
+
tabId: string
|
|
322
|
+
delta: number
|
|
323
|
+
axis?: import('./layout-tree').SplitDirection
|
|
324
|
+
}
|
|
325
|
+
| {
|
|
326
|
+
type: 'set-split-ratio'
|
|
327
|
+
tabId: string
|
|
328
|
+
ratio: number
|
|
329
|
+
axis?: import('./layout-tree').SplitDirection
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// -- UI actions --
|
|
333
|
+
export type UIAction =
|
|
334
|
+
| { type: 'toggle-sidebar' }
|
|
335
|
+
| { type: 'resize-sidebar'; delta: number }
|
|
336
|
+
| { type: 'set-focus-mode'; focusMode: FocusMode }
|
|
337
|
+
| { type: 'set-terminal-size'; cols: number; rows: number }
|
|
338
|
+
| { type: 'toggle-git-panel' }
|
|
339
|
+
| { type: 'resize-git-panel'; delta: number }
|
|
340
|
+
| { type: 'set-pending-chords'; chords: string[] | null }
|
|
341
|
+
|
|
342
|
+
// -- Git panel actions --
|
|
343
|
+
export interface GitRefreshPayload {
|
|
344
|
+
branch: string | null
|
|
345
|
+
ahead: number
|
|
346
|
+
behind: number
|
|
347
|
+
files: GitFileEntry[]
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export type GitPanelAction =
|
|
351
|
+
| { type: 'git-refresh-success'; payload: GitRefreshPayload }
|
|
352
|
+
| { type: 'git-refresh-error'; kind: GitPanelError }
|
|
353
|
+
| { type: 'git-panel-reset' }
|
|
354
|
+
|
|
355
|
+
// -- Data actions --
|
|
356
|
+
export type DataAction =
|
|
357
|
+
| { type: 'set-snippets'; snippets: SnippetRecord[] }
|
|
358
|
+
| { type: 'delete-snippet'; snippetId: string }
|
|
359
|
+
|
|
360
|
+
export type AppAction =
|
|
361
|
+
| ModalAction
|
|
362
|
+
| SessionAction
|
|
363
|
+
| TabAction
|
|
364
|
+
| LayoutAction
|
|
365
|
+
| UIAction
|
|
366
|
+
| DataAction
|
|
367
|
+
| GitPanelAction
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SessionRecord,
|
|
3
|
+
SnippetRecord,
|
|
4
|
+
TerminalLine,
|
|
5
|
+
TerminalModeState,
|
|
6
|
+
TerminalSnapshot,
|
|
7
|
+
WorkspaceSnapshotV1,
|
|
8
|
+
} from './types'
|
|
9
|
+
|
|
10
|
+
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
11
|
+
return typeof value === 'object' && value !== null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isString(value: unknown): value is string {
|
|
15
|
+
return typeof value === 'string'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isBoolean(value: unknown): value is boolean {
|
|
19
|
+
return typeof value === 'boolean'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isFiniteNumber(value: unknown): value is number {
|
|
23
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isTerminalLine(value: unknown): value is TerminalLine {
|
|
27
|
+
if (!isObjectRecord(value) || !Array.isArray(value.spans)) {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return value.spans.every((span) => {
|
|
32
|
+
if (!isObjectRecord(span) || !isString(span.text)) {
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
(span.fg === undefined || isString(span.fg)) &&
|
|
38
|
+
(span.bg === undefined || isString(span.bg)) &&
|
|
39
|
+
(span.bold === undefined || isBoolean(span.bold)) &&
|
|
40
|
+
(span.italic === undefined || isBoolean(span.italic)) &&
|
|
41
|
+
(span.underline === undefined || isBoolean(span.underline)) &&
|
|
42
|
+
(span.cursor === undefined || isBoolean(span.cursor))
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isTerminalSnapshot(value: unknown): value is TerminalSnapshot {
|
|
48
|
+
return (
|
|
49
|
+
isObjectRecord(value) &&
|
|
50
|
+
Array.isArray(value.lines) &&
|
|
51
|
+
value.lines.every(isTerminalLine) &&
|
|
52
|
+
isFiniteNumber(value.viewportY) &&
|
|
53
|
+
isFiniteNumber(value.baseY) &&
|
|
54
|
+
isBoolean(value.cursorVisible)
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isTerminalModeState(value: unknown): value is TerminalModeState {
|
|
59
|
+
return (
|
|
60
|
+
isObjectRecord(value) &&
|
|
61
|
+
(value.mouseTrackingMode === 'none' ||
|
|
62
|
+
value.mouseTrackingMode === 'x10' ||
|
|
63
|
+
value.mouseTrackingMode === 'vt200' ||
|
|
64
|
+
value.mouseTrackingMode === 'drag' ||
|
|
65
|
+
value.mouseTrackingMode === 'any') &&
|
|
66
|
+
isBoolean(value.sendFocusMode) &&
|
|
67
|
+
isBoolean(value.alternateScrollMode) &&
|
|
68
|
+
isBoolean(value.isAlternateBuffer) &&
|
|
69
|
+
isBoolean(value.bracketedPasteMode)
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isLayoutNode(value: unknown): boolean {
|
|
74
|
+
if (!isObjectRecord(value)) return false
|
|
75
|
+
if (value.type === 'leaf') {
|
|
76
|
+
return isString(value.tabId) && value.tabId.length > 0
|
|
77
|
+
}
|
|
78
|
+
if (value.type === 'split') {
|
|
79
|
+
return (
|
|
80
|
+
(value.direction === 'horizontal' || value.direction === 'vertical') &&
|
|
81
|
+
isFiniteNumber(value.ratio) &&
|
|
82
|
+
value.ratio > 0 &&
|
|
83
|
+
value.ratio < 1 &&
|
|
84
|
+
isLayoutNode(value.first) &&
|
|
85
|
+
isLayoutNode(value.second)
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isLayoutTreesMap(value: unknown): boolean {
|
|
92
|
+
if (!isObjectRecord(value)) return false
|
|
93
|
+
return Object.values(value).every(isLayoutNode)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isStringRecord(value: unknown): boolean {
|
|
97
|
+
if (!isObjectRecord(value)) return false
|
|
98
|
+
return Object.values(value).every(isString)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function isWorkspaceSnapshotV1(value: unknown): value is WorkspaceSnapshotV1 {
|
|
102
|
+
return (
|
|
103
|
+
isObjectRecord(value) &&
|
|
104
|
+
value.version === 1 &&
|
|
105
|
+
isString(value.savedAt) &&
|
|
106
|
+
(value.activeTabId === null || isString(value.activeTabId)) &&
|
|
107
|
+
isObjectRecord(value.sidebar) &&
|
|
108
|
+
isBoolean(value.sidebar.visible) &&
|
|
109
|
+
isFiniteNumber(value.sidebar.width) &&
|
|
110
|
+
(value.sidebar.gitPanelVisible === undefined || isBoolean(value.sidebar.gitPanelVisible)) &&
|
|
111
|
+
(value.sidebar.gitPanelRatio === undefined || isFiniteNumber(value.sidebar.gitPanelRatio)) &&
|
|
112
|
+
Array.isArray(value.tabs) &&
|
|
113
|
+
value.tabs.every(
|
|
114
|
+
(tab) =>
|
|
115
|
+
isObjectRecord(tab) &&
|
|
116
|
+
isString(tab.id) &&
|
|
117
|
+
isString(tab.assistant) &&
|
|
118
|
+
tab.assistant.length > 0 &&
|
|
119
|
+
isString(tab.title) &&
|
|
120
|
+
isString(tab.command) &&
|
|
121
|
+
(tab.status === 'starting' ||
|
|
122
|
+
tab.status === 'running' ||
|
|
123
|
+
tab.status === 'exited' ||
|
|
124
|
+
tab.status === 'error') &&
|
|
125
|
+
isString(tab.buffer) &&
|
|
126
|
+
isTerminalModeState(tab.terminalModes) &&
|
|
127
|
+
(tab.viewport === undefined || isTerminalSnapshot(tab.viewport)) &&
|
|
128
|
+
(tab.errorMessage === undefined || isString(tab.errorMessage)) &&
|
|
129
|
+
(tab.exitCode === undefined || isFiniteNumber(tab.exitCode))
|
|
130
|
+
) &&
|
|
131
|
+
(value.layoutTree === undefined || isLayoutNode(value.layoutTree)) &&
|
|
132
|
+
(value.layoutTrees === undefined || isLayoutTreesMap(value.layoutTrees)) &&
|
|
133
|
+
(value.tabGroupMap === undefined || isStringRecord(value.tabGroupMap))
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function isSessionRecord(value: unknown): value is SessionRecord {
|
|
138
|
+
return (
|
|
139
|
+
isObjectRecord(value) &&
|
|
140
|
+
isString(value.id) &&
|
|
141
|
+
isString(value.name) &&
|
|
142
|
+
(value.projectPath === undefined || isString(value.projectPath)) &&
|
|
143
|
+
isString(value.createdAt) &&
|
|
144
|
+
isString(value.updatedAt) &&
|
|
145
|
+
isString(value.lastOpenedAt) &&
|
|
146
|
+
(value.workspaceSnapshot === undefined || isWorkspaceSnapshotV1(value.workspaceSnapshot))
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function isSnippetRecord(value: unknown): value is SnippetRecord {
|
|
151
|
+
return (
|
|
152
|
+
isObjectRecord(value) && isString(value.id) && isString(value.name) && isString(value.content)
|
|
153
|
+
)
|
|
154
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AppState, SessionRecord } from './types'
|
|
2
|
+
|
|
3
|
+
import { loadConfig, saveConfig } from '../config'
|
|
4
|
+
import { saveSessionCatalog } from './session-catalog'
|
|
5
|
+
import { serializeWorkspace } from './session-persistence'
|
|
6
|
+
|
|
7
|
+
export function buildSessionsWithCurrentSnapshot(
|
|
8
|
+
sessions: SessionRecord[],
|
|
9
|
+
currentSessionId: string | null,
|
|
10
|
+
state: AppState
|
|
11
|
+
): SessionRecord[] {
|
|
12
|
+
return sessions.map((session) =>
|
|
13
|
+
session.id === currentSessionId
|
|
14
|
+
? {
|
|
15
|
+
...session,
|
|
16
|
+
updatedAt: new Date().toISOString(),
|
|
17
|
+
workspaceSnapshot: serializeWorkspace(state),
|
|
18
|
+
}
|
|
19
|
+
: session
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function saveCurrentWorkspace(state: AppState): void {
|
|
24
|
+
saveConfig({
|
|
25
|
+
...loadConfig(),
|
|
26
|
+
customCommands: state.customCommands,
|
|
27
|
+
gitPanelRatio: state.sidebar.gitPanelRatio,
|
|
28
|
+
gitPanelVisible: state.sidebar.gitPanelVisible,
|
|
29
|
+
})
|
|
30
|
+
saveSessionCatalog(
|
|
31
|
+
buildSessionsWithCurrentSnapshot(state.sessions, state.currentSessionId, state)
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { DirectoryResult } from '../../state/types'
|
|
2
|
+
|
|
3
|
+
import { abbreviatePath } from '../path-format'
|
|
4
|
+
import { theme } from '../theme'
|
|
5
|
+
import { uiTokens } from '../ui-tokens'
|
|
6
|
+
import { InputField } from './input-field'
|
|
7
|
+
import { ListItem } from './list-item'
|
|
8
|
+
import { ModalShell } from './modal-shell'
|
|
9
|
+
|
|
10
|
+
function getDirectoryResultIcon(result: DirectoryResult): string {
|
|
11
|
+
if (result.type === 'worktree') {
|
|
12
|
+
return '\u{e728}'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (result.type === 'workspace') {
|
|
16
|
+
return '\u{f07c}'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return '\u{e702}'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getDirectoryResultColor(result: DirectoryResult): string {
|
|
23
|
+
if (result.type === 'worktree') {
|
|
24
|
+
return theme.warning
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (result.type === 'workspace') {
|
|
28
|
+
return theme.accentAlt
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return theme.accent
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface CreateSessionModalProps {
|
|
35
|
+
activeField: 'directory' | 'name'
|
|
36
|
+
directoryQuery: string
|
|
37
|
+
sessionName: string
|
|
38
|
+
results: DirectoryResult[]
|
|
39
|
+
selectedIndex: number
|
|
40
|
+
pendingProjectPath: string | null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function CreateSessionModal({
|
|
44
|
+
activeField,
|
|
45
|
+
directoryQuery,
|
|
46
|
+
pendingProjectPath,
|
|
47
|
+
results,
|
|
48
|
+
selectedIndex,
|
|
49
|
+
sessionName,
|
|
50
|
+
}: CreateSessionModalProps) {
|
|
51
|
+
const dirActive = activeField === 'directory'
|
|
52
|
+
const nameActive = activeField === 'name'
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<ModalShell
|
|
56
|
+
title="Create session"
|
|
57
|
+
help="Tab switch field. Ctrl+n/p nav. Esc cancel."
|
|
58
|
+
width={uiTokens.modalWidth.xl}
|
|
59
|
+
>
|
|
60
|
+
<box flexDirection="column">
|
|
61
|
+
<text fg={dirActive ? theme.text : theme.textMuted}>Search projects</text>
|
|
62
|
+
<InputField
|
|
63
|
+
active={dirActive}
|
|
64
|
+
value={
|
|
65
|
+
pendingProjectPath && !dirActive ? abbreviatePath(pendingProjectPath) : directoryQuery
|
|
66
|
+
}
|
|
67
|
+
/>
|
|
68
|
+
</box>
|
|
69
|
+
|
|
70
|
+
{dirActive && results.length === 0 && directoryQuery.length > 0 ? (
|
|
71
|
+
<text fg={theme.textMuted}>No matches</text>
|
|
72
|
+
) : null}
|
|
73
|
+
|
|
74
|
+
{dirActive
|
|
75
|
+
? results.map((result, index) => {
|
|
76
|
+
const active = index === selectedIndex
|
|
77
|
+
return (
|
|
78
|
+
<ListItem
|
|
79
|
+
key={result.path}
|
|
80
|
+
active={active}
|
|
81
|
+
leading={
|
|
82
|
+
<text fg={getDirectoryResultColor(result)}>{getDirectoryResultIcon(result)}</text>
|
|
83
|
+
}
|
|
84
|
+
title={
|
|
85
|
+
<text fg={active ? theme.text : theme.textMuted}>
|
|
86
|
+
{abbreviatePath(result.path)}
|
|
87
|
+
</text>
|
|
88
|
+
}
|
|
89
|
+
/>
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
: null}
|
|
93
|
+
|
|
94
|
+
<box flexDirection="column">
|
|
95
|
+
<text fg={nameActive ? theme.text : theme.textMuted}>Session name</text>
|
|
96
|
+
<InputField active={nameActive} value={sessionName} />
|
|
97
|
+
</box>
|
|
98
|
+
</ModalShell>
|
|
99
|
+
)
|
|
100
|
+
}
|