@brimveyn/aimux 1.3.0 → 1.3.1
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/package.json +2 -2
- package/src/app-runtime/backend-runtime-events.ts +7 -1
- package/src/app-runtime/pty-write.ts +7 -4
- package/src/app-runtime/side-effects.ts +54 -4
- package/src/app-runtime/snippet-actions.ts +3 -2
- package/src/app-runtime/use-backend-runtime.ts +7 -2
- package/src/app-runtime/use-renderer-bindings.ts +2 -2
- package/src/app-runtime/use-terminal-resize.ts +15 -6
- package/src/app.tsx +89 -30
- package/src/config.ts +11 -0
- package/src/daemon/daemon.ts +19 -2
- package/src/daemon/session-manager.ts +20 -5
- package/src/daemon/session-registry.ts +18 -11
- package/src/index.tsx +8 -1
- package/src/input/keymap/describe-bindings.ts +68 -0
- package/src/input/keymap/key-format.ts +67 -0
- package/src/input/modes/bridge.ts +1 -0
- package/src/input/modes/transitions.ts +2 -0
- package/src/input/modes/types.ts +2 -0
- package/src/ipc/manager-protocol.ts +51 -2
- package/src/ipc/protocol.ts +43 -2
- package/src/pty/pty-manager.ts +29 -3
- package/src/session-backend/local-session-backend.ts +12 -5
- package/src/session-backend/remote-session-backend.ts +18 -5
- package/src/session-backend/types.ts +4 -2
- package/src/state/reducers/modal-state.ts +18 -1
- package/src/state/reducers/tab-state.ts +20 -2
- package/src/state/session-persistence.ts +9 -2
- package/src/state/types.ts +23 -0
- package/src/state/validation.ts +8 -0
- package/src/terminal-manager/manager-client.ts +29 -5
- package/src/terminal-manager/terminal-manager.ts +19 -3
- package/src/ui/components/create-session-modal.tsx +3 -5
- package/src/ui/components/git-commit-modal.tsx +3 -5
- package/src/ui/components/help-modal.tsx +49 -68
- package/src/ui/components/list-item.tsx +24 -5
- package/src/ui/components/new-tab-modal.tsx +8 -9
- package/src/ui/components/pending-chord-overlay.tsx +28 -0
- package/src/ui/components/session-name-modal.tsx +3 -5
- package/src/ui/components/session-picker-modal.tsx +3 -1
- package/src/ui/components/snippet-editor-modal.tsx +3 -1
- package/src/ui/components/snippet-picker-modal.tsx +3 -1
- package/src/ui/components/status-bar.tsx +6 -2
- package/src/ui/components/theme-picker-modal.tsx +3 -6
- package/src/ui/components/update-available-modal.tsx +42 -0
- package/src/ui/keymap-context.ts +39 -0
- package/src/ui/root.tsx +12 -0
- package/src/ui/status-bar-model.ts +67 -39
- package/src/update/version-check.ts +67 -0
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { AppAction, AppState, TabSession } from '../types'
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
allLeafIds,
|
|
5
3
|
createGroupId,
|
|
@@ -15,6 +13,13 @@ import {
|
|
|
15
13
|
} from '../layout-tree'
|
|
16
14
|
import { normalizeGroupedTabOrder } from '../session-persistence'
|
|
17
15
|
import { createDefaultTerminalModes } from '../terminal-modes'
|
|
16
|
+
import {
|
|
17
|
+
type AppAction,
|
|
18
|
+
type AppState,
|
|
19
|
+
DEFAULT_SCROLL_INTENT,
|
|
20
|
+
deriveScrollIntent,
|
|
21
|
+
type TabSession,
|
|
22
|
+
} from '../types'
|
|
18
23
|
|
|
19
24
|
const MAX_BUFFER_LENGTH = 50_000
|
|
20
25
|
|
|
@@ -385,6 +390,7 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
385
390
|
buffer: '',
|
|
386
391
|
errorMessage: undefined,
|
|
387
392
|
exitCode: undefined,
|
|
393
|
+
scrollIntent: DEFAULT_SCROLL_INTENT,
|
|
388
394
|
status: 'starting',
|
|
389
395
|
terminalModes: createDefaultTerminalModes(),
|
|
390
396
|
viewport: undefined,
|
|
@@ -404,11 +410,23 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
404
410
|
...state,
|
|
405
411
|
tabs: updateTab(state.tabs, action.tabId, (tab) => ({
|
|
406
412
|
...tab,
|
|
413
|
+
scrollIntent:
|
|
414
|
+
action.source === 'resize' || action.source === 'switch'
|
|
415
|
+
? (tab.scrollIntent ?? DEFAULT_SCROLL_INTENT)
|
|
416
|
+
: deriveScrollIntent(action.viewport),
|
|
407
417
|
status: tab.status === 'starting' ? 'running' : tab.status,
|
|
408
418
|
terminalModes: action.terminalModes,
|
|
409
419
|
viewport: action.viewport,
|
|
410
420
|
})),
|
|
411
421
|
}
|
|
422
|
+
case 'set-scroll-intent':
|
|
423
|
+
return {
|
|
424
|
+
...state,
|
|
425
|
+
tabs: updateTab(state.tabs, action.tabId, (tab) => ({
|
|
426
|
+
...tab,
|
|
427
|
+
scrollIntent: action.intent,
|
|
428
|
+
})),
|
|
429
|
+
}
|
|
412
430
|
case 'set-tab-activity':
|
|
413
431
|
return {
|
|
414
432
|
...state,
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { AppState, TabSession, TabStatus, WorkspaceSnapshotV1 } from './types'
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
allLeafIds,
|
|
5
3
|
createGroupId,
|
|
@@ -7,6 +5,13 @@ import {
|
|
|
7
5
|
type LayoutNode,
|
|
8
6
|
pruneLayoutTree,
|
|
9
7
|
} from './layout-tree'
|
|
8
|
+
import {
|
|
9
|
+
type AppState,
|
|
10
|
+
DEFAULT_SCROLL_INTENT,
|
|
11
|
+
type TabSession,
|
|
12
|
+
type TabStatus,
|
|
13
|
+
type WorkspaceSnapshotV1,
|
|
14
|
+
} from './types'
|
|
10
15
|
|
|
11
16
|
export function createEmptyWorkspaceSnapshot(): WorkspaceSnapshotV1 {
|
|
12
17
|
return {
|
|
@@ -47,6 +52,7 @@ export function serializeWorkspace(state: AppState): WorkspaceSnapshotV1 {
|
|
|
47
52
|
errorMessage: tab.errorMessage,
|
|
48
53
|
exitCode: tab.exitCode,
|
|
49
54
|
id: tab.id,
|
|
55
|
+
scrollIntent: tab.scrollIntent,
|
|
50
56
|
status: tab.status === 'disconnected' ? 'running' : tab.status,
|
|
51
57
|
terminalModes: tab.terminalModes,
|
|
52
58
|
title: tab.title,
|
|
@@ -69,6 +75,7 @@ export function restoreTabsFromWorkspace(snapshot: WorkspaceSnapshotV1 | undefin
|
|
|
69
75
|
errorMessage: tab.errorMessage,
|
|
70
76
|
exitCode: tab.exitCode,
|
|
71
77
|
id: tab.id,
|
|
78
|
+
scrollIntent: tab.scrollIntent ?? DEFAULT_SCROLL_INTENT,
|
|
72
79
|
status: getDisconnectedStatus(tab.status),
|
|
73
80
|
terminalModes: tab.terminalModes,
|
|
74
81
|
title: tab.title,
|
package/src/state/types.ts
CHANGED
|
@@ -26,6 +26,7 @@ export type ModalType =
|
|
|
26
26
|
| 'help'
|
|
27
27
|
| 'split-picker'
|
|
28
28
|
| 'git-commit'
|
|
29
|
+
| 'update-available'
|
|
29
30
|
| null
|
|
30
31
|
|
|
31
32
|
export interface TerminalSpan {
|
|
@@ -49,6 +50,16 @@ export interface TerminalSnapshot {
|
|
|
49
50
|
cursorVisible: boolean
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
export type ScrollIntent = { kind: 'bottom' } | { absoluteLine: number; kind: 'anchor' }
|
|
54
|
+
|
|
55
|
+
export const DEFAULT_SCROLL_INTENT: ScrollIntent = { kind: 'bottom' }
|
|
56
|
+
|
|
57
|
+
export function deriveScrollIntent(viewport: TerminalSnapshot): ScrollIntent {
|
|
58
|
+
return viewport.viewportY >= viewport.baseY
|
|
59
|
+
? { kind: 'bottom' }
|
|
60
|
+
: { absoluteLine: viewport.viewportY, kind: 'anchor' }
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
export interface TerminalModeState {
|
|
53
64
|
mouseTrackingMode: 'none' | 'x10' | 'vt200' | 'drag' | 'any'
|
|
54
65
|
sendFocusMode: boolean
|
|
@@ -66,6 +77,7 @@ export interface PersistedTabSnapshot {
|
|
|
66
77
|
buffer: string
|
|
67
78
|
viewport?: TerminalSnapshot
|
|
68
79
|
terminalModes: TerminalModeState
|
|
80
|
+
scrollIntent?: ScrollIntent
|
|
69
81
|
errorMessage?: string
|
|
70
82
|
exitCode?: number
|
|
71
83
|
}
|
|
@@ -105,6 +117,7 @@ export interface TabSession {
|
|
|
105
117
|
buffer: string
|
|
106
118
|
viewport?: TerminalSnapshot
|
|
107
119
|
terminalModes: TerminalModeState
|
|
120
|
+
scrollIntent?: ScrollIntent
|
|
108
121
|
command: string
|
|
109
122
|
errorMessage?: string
|
|
110
123
|
exitCode?: number
|
|
@@ -228,6 +241,12 @@ export interface ModalSnippetEditor extends ModalBase {
|
|
|
228
241
|
contentBuffer: string
|
|
229
242
|
}
|
|
230
243
|
|
|
244
|
+
export interface ModalUpdateAvailable extends ModalBase {
|
|
245
|
+
type: 'update-available'
|
|
246
|
+
currentVersion: string
|
|
247
|
+
latestVersion: string
|
|
248
|
+
}
|
|
249
|
+
|
|
231
250
|
export type DirectoryResultType = 'git-repo' | 'worktree' | 'workspace'
|
|
232
251
|
|
|
233
252
|
export interface DirectoryResult {
|
|
@@ -248,6 +267,7 @@ export type ModalState =
|
|
|
248
267
|
| ModalCreateSession
|
|
249
268
|
| ModalSnippetEditor
|
|
250
269
|
| ModalGitCommit
|
|
270
|
+
| ModalUpdateAvailable
|
|
251
271
|
|
|
252
272
|
export interface LayoutState {
|
|
253
273
|
terminalCols: number
|
|
@@ -303,6 +323,7 @@ export type ModalAction =
|
|
|
303
323
|
| { type: 'open-snippet-editor'; snippetId?: string }
|
|
304
324
|
| { type: 'begin-snippet-filter' }
|
|
305
325
|
| { type: 'open-theme-picker' }
|
|
326
|
+
| { type: 'open-update-available-modal'; currentVersion: string; latestVersion: string }
|
|
306
327
|
|
|
307
328
|
// -- Session actions --
|
|
308
329
|
export type SessionAction =
|
|
@@ -336,7 +357,9 @@ export type TabAction =
|
|
|
336
357
|
tabId: string
|
|
337
358
|
viewport: TerminalSnapshot
|
|
338
359
|
terminalModes: TerminalModeState
|
|
360
|
+
source?: 'resize' | 'scroll' | 'data' | 'switch'
|
|
339
361
|
}
|
|
362
|
+
| { type: 'set-scroll-intent'; tabId: string; intent: ScrollIntent }
|
|
340
363
|
| { type: 'set-tab-activity'; tabId: string; activity?: TabActivity }
|
|
341
364
|
| { type: 'set-tab-status'; tabId: string; status: TabStatus; exitCode?: number }
|
|
342
365
|
| { type: 'set-tab-error'; tabId: string; message: string }
|
package/src/state/validation.ts
CHANGED
|
@@ -55,6 +55,13 @@ function isTerminalSnapshot(value: unknown): value is TerminalSnapshot {
|
|
|
55
55
|
)
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
function isScrollIntent(value: unknown): boolean {
|
|
59
|
+
if (!isObjectRecord(value)) return false
|
|
60
|
+
if (value.kind === 'bottom') return true
|
|
61
|
+
if (value.kind === 'anchor') return isFiniteNumber(value.absoluteLine)
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
|
|
58
65
|
function isTerminalModeState(value: unknown): value is TerminalModeState {
|
|
59
66
|
return (
|
|
60
67
|
isObjectRecord(value) &&
|
|
@@ -125,6 +132,7 @@ export function isWorkspaceSnapshotV1(value: unknown): value is WorkspaceSnapsho
|
|
|
125
132
|
isString(tab.buffer) &&
|
|
126
133
|
isTerminalModeState(tab.terminalModes) &&
|
|
127
134
|
(tab.viewport === undefined || isTerminalSnapshot(tab.viewport)) &&
|
|
135
|
+
(tab.scrollIntent === undefined || isScrollIntent(tab.scrollIntent)) &&
|
|
128
136
|
(tab.errorMessage === undefined || isString(tab.errorMessage)) &&
|
|
129
137
|
(tab.exitCode === undefined || isFiniteNumber(tab.exitCode))
|
|
130
138
|
) &&
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events'
|
|
2
2
|
import { connect, Socket } from 'node:net'
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
ScrollIntent,
|
|
6
|
+
TerminalModeState,
|
|
7
|
+
TerminalSnapshot,
|
|
8
|
+
WorkspaceSnapshotV1,
|
|
9
|
+
} from '../state/types'
|
|
5
10
|
|
|
6
11
|
import { getTerminalManagerSocketPath } from '../daemon/runtime-paths'
|
|
7
12
|
import { logDebug } from '../debug/input-log'
|
|
@@ -298,18 +303,29 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
|
|
|
298
303
|
})
|
|
299
304
|
}
|
|
300
305
|
|
|
301
|
-
resize(
|
|
306
|
+
resize(
|
|
307
|
+
sessionId: string,
|
|
308
|
+
cols: number,
|
|
309
|
+
rows: number,
|
|
310
|
+
intents?: Record<string, ScrollIntent>
|
|
311
|
+
): Promise<void> {
|
|
302
312
|
return this.sendExpectOk({
|
|
303
313
|
id: crypto.randomUUID(),
|
|
304
|
-
payload: { cols, rows, sessionId },
|
|
314
|
+
payload: { cols, intents, rows, sessionId },
|
|
305
315
|
type: 'resizeClient',
|
|
306
316
|
})
|
|
307
317
|
}
|
|
308
318
|
|
|
309
|
-
resizeTab(
|
|
319
|
+
resizeTab(
|
|
320
|
+
sessionId: string,
|
|
321
|
+
tabId: string,
|
|
322
|
+
cols: number,
|
|
323
|
+
rows: number,
|
|
324
|
+
intent?: ScrollIntent
|
|
325
|
+
): Promise<void> {
|
|
310
326
|
return this.sendExpectOk({
|
|
311
327
|
id: crypto.randomUUID(),
|
|
312
|
-
payload: { cols, rows, sessionId, tabId },
|
|
328
|
+
payload: { cols, intent, rows, sessionId, tabId },
|
|
313
329
|
type: 'resizeTab',
|
|
314
330
|
})
|
|
315
331
|
}
|
|
@@ -330,6 +346,14 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
|
|
|
330
346
|
})
|
|
331
347
|
}
|
|
332
348
|
|
|
349
|
+
reapplyScrollIntent(sessionId: string, tabId: string, intent: ScrollIntent): Promise<void> {
|
|
350
|
+
return this.sendExpectOk({
|
|
351
|
+
id: crypto.randomUUID(),
|
|
352
|
+
payload: { intent, sessionId, tabId },
|
|
353
|
+
type: 'reapplyScrollIntent',
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
|
|
333
357
|
setActiveTab(sessionId: string, tabId: string | null): Promise<void> {
|
|
334
358
|
return this.sendExpectOk({
|
|
335
359
|
id: crypto.randomUUID(),
|
|
@@ -166,22 +166,29 @@ export async function runTerminalManager(): Promise<void> {
|
|
|
166
166
|
)
|
|
167
167
|
sendOk(socket, message.id)
|
|
168
168
|
break
|
|
169
|
-
case 'resizeClient':
|
|
169
|
+
case 'resizeClient': {
|
|
170
170
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
171
|
+
const intentsRecord = message.payload.intents
|
|
172
|
+
const intentsMap = intentsRecord
|
|
173
|
+
? new Map(Object.entries(intentsRecord))
|
|
174
|
+
: undefined
|
|
171
175
|
sessionManager.resize(
|
|
172
176
|
message.payload.sessionId,
|
|
173
177
|
message.payload.cols,
|
|
174
|
-
message.payload.rows
|
|
178
|
+
message.payload.rows,
|
|
179
|
+
intentsMap
|
|
175
180
|
)
|
|
176
181
|
sendOk(socket, message.id)
|
|
177
182
|
break
|
|
183
|
+
}
|
|
178
184
|
case 'resizeTab':
|
|
179
185
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
180
186
|
sessionManager.resizeTab(
|
|
181
187
|
message.payload.sessionId,
|
|
182
188
|
message.payload.tabId,
|
|
183
189
|
message.payload.cols,
|
|
184
|
-
message.payload.rows
|
|
190
|
+
message.payload.rows,
|
|
191
|
+
message.payload.intent
|
|
185
192
|
)
|
|
186
193
|
sendOk(socket, message.id)
|
|
187
194
|
break
|
|
@@ -199,6 +206,15 @@ export async function runTerminalManager(): Promise<void> {
|
|
|
199
206
|
sessionManager.scrollToBottom(message.payload.sessionId, message.payload.tabId)
|
|
200
207
|
sendOk(socket, message.id)
|
|
201
208
|
break
|
|
209
|
+
case 'reapplyScrollIntent':
|
|
210
|
+
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
211
|
+
sessionManager.reapplyScrollIntent(
|
|
212
|
+
message.payload.sessionId,
|
|
213
|
+
message.payload.tabId,
|
|
214
|
+
message.payload.intent
|
|
215
|
+
)
|
|
216
|
+
sendOk(socket, message.id)
|
|
217
|
+
break
|
|
202
218
|
case 'setActiveTab':
|
|
203
219
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
204
220
|
sessionManager.setActiveTab(message.payload.sessionId, message.payload.tabId)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DirectoryResult } from '../../state/types'
|
|
2
2
|
|
|
3
|
+
import { useModalHelp } from '../keymap-context'
|
|
3
4
|
import { abbreviatePath } from '../path-format'
|
|
4
5
|
import { theme } from '../theme'
|
|
5
6
|
import { uiTokens } from '../ui-tokens'
|
|
@@ -50,13 +51,10 @@ export function CreateSessionModal({
|
|
|
50
51
|
}: CreateSessionModalProps) {
|
|
51
52
|
const dirActive = activeField === 'directory'
|
|
52
53
|
const nameActive = activeField === 'name'
|
|
54
|
+
const help = useModalHelp('modal.create-session')
|
|
53
55
|
|
|
54
56
|
return (
|
|
55
|
-
<ModalShell
|
|
56
|
-
title="Create session"
|
|
57
|
-
help="Tab switch field. Ctrl+n/p nav. Esc cancel."
|
|
58
|
-
width={uiTokens.modalWidth.xl}
|
|
59
|
-
>
|
|
57
|
+
<ModalShell title="Create session" help={help} width={uiTokens.modalWidth.xl}>
|
|
60
58
|
<box flexDirection="column">
|
|
61
59
|
<text fg={dirActive ? theme.text : theme.textMuted}>Search projects</text>
|
|
62
60
|
<InputField
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useModalHelp } from '../keymap-context'
|
|
1
2
|
import { theme } from '../theme'
|
|
2
3
|
import { uiTokens } from '../ui-tokens'
|
|
3
4
|
import { InputField } from './input-field'
|
|
@@ -13,13 +14,10 @@ interface GitCommitModalProps {
|
|
|
13
14
|
export function GitCommitModal({ activeField, body, cursorPos, title }: GitCommitModalProps) {
|
|
14
15
|
const titleActive = activeField === 'title'
|
|
15
16
|
const bodyActive = activeField === 'body'
|
|
17
|
+
const help = useModalHelp('modal.git-commit')
|
|
16
18
|
|
|
17
19
|
return (
|
|
18
|
-
<ModalShell
|
|
19
|
-
title="Commit"
|
|
20
|
-
help="Tab switch · ←→ move cursor · Enter newline (body) · Ctrl+Enter commit · Esc cancel"
|
|
21
|
-
width={uiTokens.modalWidth.xl}
|
|
22
|
-
>
|
|
20
|
+
<ModalShell title="Commit" help={help} width={uiTokens.modalWidth.xl}>
|
|
23
21
|
<box flexDirection="column">
|
|
24
22
|
<text fg={titleActive ? theme.text : theme.textMuted}>Title</text>
|
|
25
23
|
<InputField
|
|
@@ -1,79 +1,60 @@
|
|
|
1
|
+
import type { ModeId } from '@brimveyn/aimux-config'
|
|
2
|
+
|
|
3
|
+
import { describeBindings, groupDescribedBindings } from '../../input/keymap/describe-bindings'
|
|
4
|
+
import { useKeymap, useModalHelp } from '../keymap-context'
|
|
1
5
|
import { theme } from '../theme'
|
|
2
6
|
import { uiTokens } from '../ui-tokens'
|
|
3
7
|
import { ModalShell } from './modal-shell'
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
['Ctrl+n', 'New tab'],
|
|
19
|
-
['Ctrl+w', 'Close tab'],
|
|
20
|
-
['Ctrl+r', 'Restart tab'],
|
|
21
|
-
['Ctrl+g', 'Session picker'],
|
|
22
|
-
['Ctrl+s', 'Snippet picker'],
|
|
23
|
-
['Ctrl+t', 'Theme picker'],
|
|
24
|
-
],
|
|
25
|
-
title: 'Tabs & Sessions',
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
bindings: [
|
|
29
|
-
['Ctrl+b', 'Toggle sidebar'],
|
|
30
|
-
['Ctrl+h / l', 'Resize sidebar'],
|
|
31
|
-
['Shift+G', 'Toggle git panel'],
|
|
32
|
-
['Ctrl+j / k', 'Resize git panel'],
|
|
33
|
-
],
|
|
34
|
-
title: 'Sidebar',
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
bindings: [
|
|
38
|
-
['Ctrl+z', 'Back to navigation'],
|
|
39
|
-
['Ctrl+w', 'Enter layout mode'],
|
|
40
|
-
['Ctrl+b', 'Toggle sidebar'],
|
|
41
|
-
],
|
|
42
|
-
title: 'Terminal Input',
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
bindings: [
|
|
46
|
-
['|', 'Split vertical'],
|
|
47
|
-
['-', 'Split horizontal'],
|
|
48
|
-
['h / j / k / l', 'Focus pane'],
|
|
49
|
-
['Shift+H/J/K/L', 'Resize pane'],
|
|
50
|
-
['q', 'Close pane'],
|
|
51
|
-
['Esc', 'Cancel'],
|
|
52
|
-
],
|
|
53
|
-
title: 'Layout Mode (Ctrl+w)',
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
bindings: [['Ctrl+c', 'Quit']],
|
|
57
|
-
title: 'General',
|
|
58
|
-
},
|
|
59
|
-
] as const
|
|
9
|
+
interface ModeSection {
|
|
10
|
+
modeId: ModeId
|
|
11
|
+
title: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const SECTIONS: ModeSection[] = [
|
|
15
|
+
{ modeId: 'navigation', title: 'Navigation' },
|
|
16
|
+
{ modeId: 'terminal-input', title: 'Terminal input' },
|
|
17
|
+
{ modeId: 'layout', title: 'Layout' },
|
|
18
|
+
{ modeId: 'git-mode', title: 'Git mode' },
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
const KEYS_COLUMN_WIDTH = 22
|
|
60
22
|
|
|
61
23
|
export function HelpModal() {
|
|
24
|
+
const config = useKeymap()
|
|
25
|
+
const help = useModalHelp('modal.help', 1)
|
|
26
|
+
|
|
62
27
|
return (
|
|
63
|
-
<ModalShell title="Keybindings" help=
|
|
64
|
-
{SECTIONS.map((section) =>
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
28
|
+
<ModalShell title="Keybindings" help={help} width={uiTokens.modalWidth.lg}>
|
|
29
|
+
{SECTIONS.map((section) => {
|
|
30
|
+
const bindings = describeBindings(config, section.modeId, {
|
|
31
|
+
dedupeByDescription: true,
|
|
32
|
+
withDescriptionOnly: true,
|
|
33
|
+
})
|
|
34
|
+
if (bindings.length === 0) return null
|
|
35
|
+
const groups = groupDescribedBindings(bindings)
|
|
36
|
+
return (
|
|
37
|
+
<box key={section.modeId} flexDirection="column">
|
|
38
|
+
<text fg={theme.text}>{section.title}</text>
|
|
39
|
+
{groups.map((group, groupIdx) => (
|
|
40
|
+
<box
|
|
41
|
+
key={`${section.modeId}-${group.group ?? 'root'}-${groupIdx}`}
|
|
42
|
+
flexDirection="column"
|
|
43
|
+
>
|
|
44
|
+
{group.group ? <text fg={theme.textMuted}> {group.group}</text> : null}
|
|
45
|
+
{group.bindings.map((binding) => (
|
|
46
|
+
<box key={`${binding.keys}-${binding.description}`} flexDirection="row">
|
|
47
|
+
<box width={KEYS_COLUMN_WIDTH}>
|
|
48
|
+
<text fg={theme.accentAlt}> {binding.keysDisplay}</text>
|
|
49
|
+
</box>
|
|
50
|
+
<text fg={theme.textMuted}>{binding.description ?? ''}</text>
|
|
51
|
+
</box>
|
|
52
|
+
))}
|
|
71
53
|
</box>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
))}
|
|
54
|
+
))}
|
|
55
|
+
</box>
|
|
56
|
+
)
|
|
57
|
+
})}
|
|
77
58
|
</ModalShell>
|
|
78
59
|
)
|
|
79
60
|
}
|
|
@@ -3,24 +3,43 @@ import type { ReactNode } from 'react'
|
|
|
3
3
|
import { theme } from '../theme'
|
|
4
4
|
import { Surface } from './surface'
|
|
5
5
|
|
|
6
|
+
export type ListItemDirection = 'row' | 'column'
|
|
7
|
+
|
|
6
8
|
interface ListItemProps {
|
|
7
9
|
active: boolean
|
|
10
|
+
direction?: ListItemDirection
|
|
8
11
|
leading?: ReactNode
|
|
9
12
|
subtitle?: ReactNode
|
|
10
13
|
title: ReactNode
|
|
11
14
|
trailing?: ReactNode
|
|
12
15
|
}
|
|
13
16
|
|
|
14
|
-
export function ListItem({
|
|
17
|
+
export function ListItem({
|
|
18
|
+
active,
|
|
19
|
+
direction = 'column',
|
|
20
|
+
leading,
|
|
21
|
+
subtitle,
|
|
22
|
+
title,
|
|
23
|
+
trailing,
|
|
24
|
+
}: ListItemProps) {
|
|
25
|
+
const isRow = direction === 'row'
|
|
15
26
|
return (
|
|
16
|
-
<Surface
|
|
27
|
+
<Surface
|
|
28
|
+
tone={active ? 'selected' : 'elevated'}
|
|
29
|
+
paddingLeft={isRow ? 2 : 1}
|
|
30
|
+
paddingRight={isRow ? 2 : 1}
|
|
31
|
+
>
|
|
17
32
|
<box flexDirection="column">
|
|
18
33
|
<box flexDirection="row">
|
|
19
|
-
|
|
20
|
-
|
|
34
|
+
{isRow ? null : (
|
|
35
|
+
<>
|
|
36
|
+
<text fg={active ? theme.accent : theme.dim}>{active ? '›' : '·'}</text>
|
|
37
|
+
<text> </text>
|
|
38
|
+
</>
|
|
39
|
+
)}
|
|
21
40
|
{leading}
|
|
22
41
|
{leading ? <text> </text> : null}
|
|
23
|
-
<box flexGrow={1}>{title}</box>
|
|
42
|
+
<box flexGrow={isRow ? 0 : 1}>{title}</box>
|
|
24
43
|
{trailing ? <box>{trailing}</box> : null}
|
|
25
44
|
</box>
|
|
26
45
|
{subtitle ? <box paddingLeft={2}>{subtitle}</box> : null}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getAllAssistantOptions } from '../../pty/command-registry'
|
|
2
|
+
import { useModalHelp } from '../keymap-context'
|
|
2
3
|
import { theme } from '../theme'
|
|
3
4
|
import { uiTokens } from '../ui-tokens'
|
|
4
5
|
import { InputField } from './input-field'
|
|
@@ -14,17 +15,15 @@ interface NewTabModalProps {
|
|
|
14
15
|
export function NewTabModal({ customCommands, editBuffer, selectedIndex }: NewTabModalProps) {
|
|
15
16
|
const options = getAllAssistantOptions(customCommands)
|
|
16
17
|
const selectedOption = options[selectedIndex]
|
|
18
|
+
const editingHelp = useModalHelp('modal.new-tab.command-edit')
|
|
19
|
+
const browsingHelp = useModalHelp('modal.new-tab')
|
|
20
|
+
const help =
|
|
21
|
+
editBuffer !== null
|
|
22
|
+
? `Editing command for ${selectedOption?.label}. ${editingHelp}`
|
|
23
|
+
: browsingHelp
|
|
17
24
|
|
|
18
25
|
return (
|
|
19
|
-
<ModalShell
|
|
20
|
-
title="New assistant tab"
|
|
21
|
-
help={
|
|
22
|
-
editBuffer !== null
|
|
23
|
-
? `Editing command for ${selectedOption?.label}. Enter to confirm, Esc to cancel.`
|
|
24
|
-
: 'Use j/k or arrows, Enter to confirm, e to edit command, Esc to cancel.'
|
|
25
|
-
}
|
|
26
|
-
width={uiTokens.modalWidth.md}
|
|
27
|
-
>
|
|
26
|
+
<ModalShell title="New assistant tab" help={help} width={uiTokens.modalWidth.md}>
|
|
28
27
|
{editBuffer !== null ? (
|
|
29
28
|
<InputField active value={editBuffer} />
|
|
30
29
|
) : (
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { parseKeyNotation } from '../../input/keymap/key-chord'
|
|
2
|
+
import { formatChord } from '../../input/keymap/key-format'
|
|
3
|
+
import { useAppStore } from '../../state/app-store'
|
|
4
|
+
import { useKeymap } from '../keymap-context'
|
|
5
|
+
import { theme } from '../theme'
|
|
6
|
+
import { Surface } from './surface'
|
|
7
|
+
|
|
8
|
+
export function PendingChordOverlay() {
|
|
9
|
+
const pendingChords = useAppStore((s) => s.pendingChords)
|
|
10
|
+
const config = useKeymap()
|
|
11
|
+
|
|
12
|
+
if (!pendingChords || pendingChords.length === 0) return null
|
|
13
|
+
|
|
14
|
+
const leaderChord = parseKeyNotation(config.leader)[0]
|
|
15
|
+
const display = pendingChords.map((c) => formatChord(c, leaderChord)).join(' ')
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<box position="absolute" bottom={2} right={1}>
|
|
19
|
+
<Surface tone="elevated" paddingLeft={1} paddingRight={1}>
|
|
20
|
+
<box flexDirection="row">
|
|
21
|
+
<text fg={theme.textMuted}>pending: </text>
|
|
22
|
+
<text fg={theme.accent}>{display}</text>
|
|
23
|
+
<text fg={theme.textMuted}> …</text>
|
|
24
|
+
</box>
|
|
25
|
+
</Surface>
|
|
26
|
+
</box>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
+
import { useModalHelp } from '../keymap-context'
|
|
1
2
|
import { uiTokens } from '../ui-tokens'
|
|
2
3
|
import { InputField } from './input-field'
|
|
3
4
|
import { ModalShell } from './modal-shell'
|
|
4
5
|
|
|
5
6
|
export function SessionNameModal({ title, value }: { title: string; value: string }) {
|
|
7
|
+
const help = useModalHelp('modal.session-name')
|
|
6
8
|
return (
|
|
7
|
-
<ModalShell
|
|
8
|
-
title={title}
|
|
9
|
-
help="Type a session name. Enter confirm, Esc cancel."
|
|
10
|
-
width={uiTokens.modalWidth.md}
|
|
11
|
-
>
|
|
9
|
+
<ModalShell title={title} help={help} width={uiTokens.modalWidth.md}>
|
|
12
10
|
<InputField active value={value} />
|
|
13
11
|
</ModalShell>
|
|
14
12
|
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SessionRecord } from '../../state/types'
|
|
2
2
|
|
|
3
3
|
import { filterSessions } from '../../state/selectors'
|
|
4
|
+
import { useModalHelp } from '../keymap-context'
|
|
4
5
|
import { abbreviatePath } from '../path-format'
|
|
5
6
|
import { theme } from '../theme'
|
|
6
7
|
import { uiTokens } from '../ui-tokens'
|
|
@@ -47,11 +48,12 @@ export function SessionPickerModal({
|
|
|
47
48
|
const hasFilter = !!filter
|
|
48
49
|
const showFilteredEmptyState = filtered.length === 0 && sessions.length > 0
|
|
49
50
|
const showInitialEmptyState = filtered.length === 0 && sessions.length === 0
|
|
51
|
+
const help = useModalHelp('modal.session-picker')
|
|
50
52
|
|
|
51
53
|
return (
|
|
52
54
|
<ModalShell
|
|
53
55
|
title="Sessions"
|
|
54
|
-
help=
|
|
56
|
+
help={help}
|
|
55
57
|
width={uiTokens.modalWidth.lg}
|
|
56
58
|
footer={<ModalFilterBar filter={filter} />}
|
|
57
59
|
>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useModalHelp } from '../keymap-context'
|
|
1
2
|
import { theme } from '../theme'
|
|
2
3
|
import { uiTokens } from '../ui-tokens'
|
|
3
4
|
import { InputField } from './input-field'
|
|
@@ -18,11 +19,12 @@ export function SnippetEditorModal({
|
|
|
18
19
|
}: SnippetEditorModalProps) {
|
|
19
20
|
const nameActive = activeField === 'name'
|
|
20
21
|
const contentActive = activeField === 'content'
|
|
22
|
+
const help = useModalHelp('modal.snippet-editor')
|
|
21
23
|
|
|
22
24
|
return (
|
|
23
25
|
<ModalShell
|
|
24
26
|
title={isEditing ? 'Edit snippet' : 'Create snippet'}
|
|
25
|
-
help=
|
|
27
|
+
help={help}
|
|
26
28
|
width={uiTokens.modalWidth.xl}
|
|
27
29
|
>
|
|
28
30
|
<box flexDirection="column">
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SnippetRecord } from '../../state/types'
|
|
2
2
|
|
|
3
3
|
import { filterSnippets } from '../../state/selectors'
|
|
4
|
+
import { useModalHelp } from '../keymap-context'
|
|
4
5
|
import { theme } from '../theme'
|
|
5
6
|
import { uiTokens } from '../ui-tokens'
|
|
6
7
|
import { ListItem } from './list-item'
|
|
@@ -23,11 +24,12 @@ function truncateContent(content: string): string {
|
|
|
23
24
|
|
|
24
25
|
export function SnippetPickerModal({ filter, selectedIndex, snippets }: SnippetPickerModalProps) {
|
|
25
26
|
const filtered = filterSnippets(snippets, filter)
|
|
27
|
+
const help = useModalHelp('modal.snippet-picker')
|
|
26
28
|
|
|
27
29
|
return (
|
|
28
30
|
<ModalShell
|
|
29
31
|
title="Snippets"
|
|
30
|
-
help=
|
|
32
|
+
help={help}
|
|
31
33
|
width={uiTokens.modalWidth.xl}
|
|
32
34
|
footer={<ModalFilterBar filter={filter} />}
|
|
33
35
|
>
|