@brimveyn/aimux 1.4.1 → 1.5.4
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/README.md +142 -176
- package/package.json +6 -3
- package/src/app-runtime/use-renderer-bindings.ts +1 -1
- package/src/app-runtime/use-terminal-resize.ts +12 -2
- package/src/app.tsx +20 -3
- package/src/config.ts +53 -17
- package/src/index.tsx +2 -2
- package/src/input/keymap/build-handlers.ts +1 -0
- package/src/input/keymap/help-entries.ts +44 -0
- package/src/input/keymap/keymap-ref.ts +11 -0
- package/src/input/keymap/sequence-resolver.ts +35 -2
- package/src/input/keymap/trie.ts +1 -0
- package/src/input/modes/bridge.ts +1 -1
- package/src/input/modes/handlers/shared.ts +0 -16
- package/src/input/modes/transitions.ts +4 -4
- package/src/input/modes/types.ts +1 -1
- package/src/platform/daemon-control.ts +0 -8
- package/src/state/reducers/git-panel-state.ts +35 -8
- package/src/state/reducers/modal-state.ts +48 -3
- package/src/state/selectors.ts +1 -5
- package/src/state/session-persistence.ts +1 -20
- package/src/state/store.ts +38 -5
- package/src/state/types.ts +27 -13
- package/src/state/validation.ts +0 -2
- package/src/state/workspace-save.ts +6 -2
- package/src/ui/components/create-session-modal.tsx +5 -3
- package/src/ui/components/git-commit-modal.tsx +1 -3
- package/src/ui/components/git-pane-widget.tsx +46 -0
- package/src/ui/components/git-panel.tsx +72 -25
- package/src/ui/components/help-modal.tsx +156 -42
- package/src/ui/components/modal-keybinds-overlay.tsx +39 -0
- package/src/ui/components/modal-shell.tsx +15 -3
- package/src/ui/components/new-tab-modal.tsx +9 -10
- package/src/ui/components/pending-chord-overlay.tsx +2 -1
- package/src/ui/components/session-name-modal.tsx +1 -3
- package/src/ui/components/session-picker-modal.tsx +1 -3
- package/src/ui/components/sidebar.tsx +24 -50
- package/src/ui/components/snippet-editor-modal.tsx +1 -3
- package/src/ui/components/snippet-picker-modal.tsx +1 -3
- package/src/ui/components/status-bar.tsx +0 -4
- package/src/ui/components/theme-picker-modal.tsx +6 -3
- package/src/ui/components/update-available-modal.tsx +7 -4
- package/src/ui/keymap-context.ts +1 -6
- package/src/ui/root.tsx +29 -1
- package/src/ui/status-bar-model.ts +0 -5
- package/src/ui/directory-search.ts +0 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ModeId, ResolvedKeymapConfig } from '@brimveyn/aimux-config'
|
|
2
|
+
|
|
3
|
+
import { describeBindings, type DescribedBinding } from './describe-bindings'
|
|
4
|
+
|
|
5
|
+
export interface HelpEntry extends DescribedBinding {
|
|
6
|
+
mode: ModeId
|
|
7
|
+
modeLabel: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const HELP_MODE_LABELS: { modeId: ModeId; label: string }[] = [
|
|
11
|
+
{ label: 'Navigation', modeId: 'navigation' },
|
|
12
|
+
{ label: 'Terminal input', modeId: 'terminal-input' },
|
|
13
|
+
{ label: 'Git mode', modeId: 'git-mode' },
|
|
14
|
+
{ label: 'Git commit', modeId: 'modal.git-commit' },
|
|
15
|
+
{ label: 'New tab', modeId: 'modal.new-tab' },
|
|
16
|
+
{ label: 'New tab — command', modeId: 'modal.new-tab.command-edit' },
|
|
17
|
+
{ label: 'Session picker', modeId: 'modal.session-picker' },
|
|
18
|
+
{ label: 'Session picker — filter', modeId: 'modal.session-picker.filtering' },
|
|
19
|
+
{ label: 'Session name', modeId: 'modal.session-name' },
|
|
20
|
+
{ label: 'Create session', modeId: 'modal.create-session' },
|
|
21
|
+
{ label: 'Rename tab', modeId: 'modal.rename-tab' },
|
|
22
|
+
{ label: 'Snippet picker', modeId: 'modal.snippet-picker' },
|
|
23
|
+
{ label: 'Snippet picker — filter', modeId: 'modal.snippet-picker.filtering' },
|
|
24
|
+
{ label: 'Snippet editor', modeId: 'modal.snippet-editor' },
|
|
25
|
+
{ label: 'Theme picker', modeId: 'modal.theme-picker' },
|
|
26
|
+
{ label: 'Split picker', modeId: 'modal.split-picker' },
|
|
27
|
+
{ label: 'Help', modeId: 'modal.help' },
|
|
28
|
+
{ label: 'Help — filter', modeId: 'modal.help.filtering' },
|
|
29
|
+
{ label: 'Update available', modeId: 'modal.update-available' },
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
export function collectHelpEntries(config: ResolvedKeymapConfig): HelpEntry[] {
|
|
33
|
+
const entries: HelpEntry[] = []
|
|
34
|
+
for (const { label, modeId } of HELP_MODE_LABELS) {
|
|
35
|
+
const bindings = describeBindings(config, modeId, {
|
|
36
|
+
dedupeByDescription: true,
|
|
37
|
+
withDescriptionOnly: true,
|
|
38
|
+
})
|
|
39
|
+
for (const binding of bindings) {
|
|
40
|
+
entries.push({ ...binding, mode: modeId, modeLabel: label })
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return entries
|
|
44
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ResolvedKeymapConfig } from '@brimveyn/aimux-config'
|
|
2
|
+
|
|
3
|
+
let activeKeymap: ResolvedKeymapConfig | null = null
|
|
4
|
+
|
|
5
|
+
export function setActiveKeymap(config: ResolvedKeymapConfig | null): void {
|
|
6
|
+
activeKeymap = config
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getActiveKeymap(): ResolvedKeymapConfig | null {
|
|
10
|
+
return activeKeymap
|
|
11
|
+
}
|
|
@@ -17,6 +17,8 @@ export class SequenceResolver {
|
|
|
17
17
|
private timeoutHandle: ReturnType<typeof setTimeout> | null = null
|
|
18
18
|
private onTimeout: ((binding: TrieBinding) => void) | null = null
|
|
19
19
|
private onPendingChange: ((chords: KeyChord[] | null) => void) | null = null
|
|
20
|
+
private repeatTerminal: KeyChord | null = null
|
|
21
|
+
private repeatBinding: TrieBinding | null = null
|
|
20
22
|
|
|
21
23
|
constructor(
|
|
22
24
|
private readonly trie: KeyTrie,
|
|
@@ -37,16 +39,29 @@ export class SequenceResolver {
|
|
|
37
39
|
feed(chord: KeyChord): ResolveResult {
|
|
38
40
|
this.clearTimeout()
|
|
39
41
|
|
|
42
|
+
// Repeat: when not mid-sequence and the chord matches the last repeatable
|
|
43
|
+
// sequence's terminal key, re-fire it.
|
|
44
|
+
if (
|
|
45
|
+
this.currentNode === null &&
|
|
46
|
+
this.repeatTerminal !== null &&
|
|
47
|
+
this.repeatBinding !== null &&
|
|
48
|
+
chord === this.repeatTerminal
|
|
49
|
+
) {
|
|
50
|
+
return { binding: this.repeatBinding, type: 'resolved' }
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
const fromNode = this.currentNode ?? this.trie.root
|
|
41
54
|
const match = this.trie.lookup(chord, fromNode)
|
|
42
55
|
|
|
43
56
|
switch (match.type) {
|
|
44
57
|
case 'exact': {
|
|
45
58
|
this.resetState()
|
|
59
|
+
this.updateRepeatOnResolve(chord, match.binding)
|
|
46
60
|
return { binding: match.binding, type: 'resolved' }
|
|
47
61
|
}
|
|
48
62
|
|
|
49
63
|
case 'prefix': {
|
|
64
|
+
this.clearRepeat()
|
|
50
65
|
this.currentNode = match.node
|
|
51
66
|
this.pendingBinding = null
|
|
52
67
|
this.pendingChords.push(chord)
|
|
@@ -55,11 +70,12 @@ export class SequenceResolver {
|
|
|
55
70
|
}
|
|
56
71
|
|
|
57
72
|
case 'exact+prefix': {
|
|
73
|
+
this.clearRepeat()
|
|
58
74
|
this.currentNode = match.node
|
|
59
75
|
this.pendingBinding = match.binding
|
|
60
76
|
this.pendingChords.push(chord)
|
|
61
77
|
this.emitPendingChange()
|
|
62
|
-
this.startTimeout(match.binding)
|
|
78
|
+
this.startTimeout(chord, match.binding)
|
|
63
79
|
return { type: 'pending' }
|
|
64
80
|
}
|
|
65
81
|
|
|
@@ -69,6 +85,7 @@ export class SequenceResolver {
|
|
|
69
85
|
this.resetState()
|
|
70
86
|
return this.feed(chord)
|
|
71
87
|
}
|
|
88
|
+
this.clearRepeat()
|
|
72
89
|
return { type: 'passthrough' }
|
|
73
90
|
}
|
|
74
91
|
}
|
|
@@ -77,6 +94,7 @@ export class SequenceResolver {
|
|
|
77
94
|
reset(): void {
|
|
78
95
|
this.clearTimeout()
|
|
79
96
|
this.resetState()
|
|
97
|
+
this.clearRepeat()
|
|
80
98
|
}
|
|
81
99
|
|
|
82
100
|
setTimeoutCallback(cb: (binding: TrieBinding) => void): void {
|
|
@@ -107,11 +125,26 @@ export class SequenceResolver {
|
|
|
107
125
|
}
|
|
108
126
|
}
|
|
109
127
|
|
|
110
|
-
private startTimeout(binding: TrieBinding): void {
|
|
128
|
+
private startTimeout(chord: KeyChord, binding: TrieBinding): void {
|
|
111
129
|
this.timeoutHandle = setTimeout(() => {
|
|
112
130
|
this.timeoutHandle = null
|
|
113
131
|
this.resetState()
|
|
132
|
+
this.updateRepeatOnResolve(chord, binding)
|
|
114
133
|
this.onTimeout?.(binding)
|
|
115
134
|
}, this.config.timeoutMs)
|
|
116
135
|
}
|
|
136
|
+
|
|
137
|
+
private updateRepeatOnResolve(chord: KeyChord, binding: TrieBinding): void {
|
|
138
|
+
if (binding.repeatable) {
|
|
139
|
+
this.repeatTerminal = chord
|
|
140
|
+
this.repeatBinding = binding
|
|
141
|
+
} else {
|
|
142
|
+
this.clearRepeat()
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private clearRepeat(): void {
|
|
147
|
+
this.repeatTerminal = null
|
|
148
|
+
this.repeatBinding = null
|
|
149
|
+
}
|
|
117
150
|
}
|
package/src/input/keymap/trie.ts
CHANGED
|
@@ -5,7 +5,6 @@ type SupportedModalType = Exclude<ModalType, null>
|
|
|
5
5
|
|
|
6
6
|
const DIRECT_FOCUS_MODE_IDS: Partial<Record<FocusMode, ModeId>> = {
|
|
7
7
|
'git': 'git-mode',
|
|
8
|
-
'layout': 'layout',
|
|
9
8
|
'navigation': 'navigation',
|
|
10
9
|
'terminal-input': 'terminal-input',
|
|
11
10
|
}
|
|
@@ -13,6 +12,7 @@ const DIRECT_FOCUS_MODE_IDS: Partial<Record<FocusMode, ModeId>> = {
|
|
|
13
12
|
const COMMAND_EDIT_MODE_IDS: Partial<Record<SupportedModalType, ModeId>> = {
|
|
14
13
|
'create-session': 'modal.create-session',
|
|
15
14
|
'git-commit': 'modal.git-commit',
|
|
15
|
+
'help': 'modal.help.filtering',
|
|
16
16
|
'new-tab': 'modal.new-tab.command-edit',
|
|
17
17
|
'rename-tab': 'modal.rename-tab',
|
|
18
18
|
'session-name': 'modal.session-name',
|
|
@@ -68,19 +68,3 @@ export function handleCtrlNavigation(key: KeyInput): KeyResult | null {
|
|
|
68
68
|
|
|
69
69
|
return null
|
|
70
70
|
}
|
|
71
|
-
|
|
72
|
-
export function handleCursorNavigation(key: KeyInput): KeyResult | null {
|
|
73
|
-
if (key.name === 'left' && !key.ctrl && !key.meta) {
|
|
74
|
-
return result([{ delta: -1, type: 'move-modal-cursor' }])
|
|
75
|
-
}
|
|
76
|
-
if (key.name === 'right' && !key.ctrl && !key.meta) {
|
|
77
|
-
return result([{ delta: 1, type: 'move-modal-cursor' }])
|
|
78
|
-
}
|
|
79
|
-
if (key.name === 'home' || (key.ctrl && key.name === 'a')) {
|
|
80
|
-
return result([{ to: 'home', type: 'move-modal-cursor' }])
|
|
81
|
-
}
|
|
82
|
-
if (key.name === 'end' || (key.ctrl && key.name === 'e')) {
|
|
83
|
-
return result([{ to: 'end', type: 'move-modal-cursor' }])
|
|
84
|
-
}
|
|
85
|
-
return null
|
|
86
|
-
}
|
|
@@ -2,10 +2,10 @@ import type { ModeId } from './types'
|
|
|
2
2
|
|
|
3
3
|
const TRANSITIONS: Record<ModeId, readonly ModeId[]> = {
|
|
4
4
|
'git-mode': ['navigation', 'modal.git-commit'],
|
|
5
|
-
'layout': ['terminal-input', 'navigation', 'modal.split-picker'],
|
|
6
5
|
'modal.create-session': ['navigation', 'modal.session-picker'],
|
|
7
6
|
'modal.git-commit': ['git-mode'],
|
|
8
|
-
'modal.help': ['navigation'],
|
|
7
|
+
'modal.help': ['navigation', 'modal.help.filtering'],
|
|
8
|
+
'modal.help.filtering': ['modal.help'],
|
|
9
9
|
'modal.new-tab': ['navigation', 'modal.new-tab.command-edit'],
|
|
10
10
|
'modal.new-tab.command-edit': ['modal.new-tab'],
|
|
11
11
|
'modal.rename-tab': ['navigation'],
|
|
@@ -20,7 +20,7 @@ const TRANSITIONS: Record<ModeId, readonly ModeId[]> = {
|
|
|
20
20
|
'modal.snippet-editor': ['navigation', 'modal.snippet-picker'],
|
|
21
21
|
'modal.snippet-picker': ['navigation', 'modal.snippet-picker.filtering', 'modal.snippet-editor'],
|
|
22
22
|
'modal.snippet-picker.filtering': ['modal.snippet-picker'],
|
|
23
|
-
'modal.split-picker': ['navigation'],
|
|
23
|
+
'modal.split-picker': ['navigation', 'terminal-input'],
|
|
24
24
|
'modal.theme-picker': ['navigation'],
|
|
25
25
|
'modal.update-available': ['navigation'],
|
|
26
26
|
'navigation': [
|
|
@@ -34,7 +34,7 @@ const TRANSITIONS: Record<ModeId, readonly ModeId[]> = {
|
|
|
34
34
|
'modal.update-available',
|
|
35
35
|
'git-mode',
|
|
36
36
|
],
|
|
37
|
-
'terminal-input': ['navigation', '
|
|
37
|
+
'terminal-input': ['navigation', 'modal.split-picker'],
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export function isValidTransition(from: ModeId, to: ModeId): boolean {
|
package/src/input/modes/types.ts
CHANGED
|
@@ -5,7 +5,6 @@ import type { AppAction, AppState, TabSession } from '../../state/types'
|
|
|
5
5
|
export type ModeId =
|
|
6
6
|
| 'navigation'
|
|
7
7
|
| 'terminal-input'
|
|
8
|
-
| 'layout'
|
|
9
8
|
| 'git-mode'
|
|
10
9
|
| 'modal.new-tab'
|
|
11
10
|
| 'modal.new-tab.command-edit'
|
|
@@ -19,6 +18,7 @@ export type ModeId =
|
|
|
19
18
|
| 'modal.snippet-editor'
|
|
20
19
|
| 'modal.theme-picker'
|
|
21
20
|
| 'modal.help'
|
|
21
|
+
| 'modal.help.filtering'
|
|
22
22
|
| 'modal.split-picker'
|
|
23
23
|
| 'modal.git-commit'
|
|
24
24
|
| 'modal.update-available'
|
|
@@ -21,10 +21,6 @@ export async function findSocketProcessPid(socketPath: string): Promise<number |
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export async function findDaemonPid(socketPath: string): Promise<number | null> {
|
|
25
|
-
return findSocketProcessPid(socketPath)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
24
|
export async function findIpcDaemonPid(): Promise<number | null> {
|
|
29
25
|
return findSocketProcessPid(getIpcDaemonSocketPath())
|
|
30
26
|
}
|
|
@@ -53,10 +49,6 @@ export async function killProcess(pid: number): Promise<void> {
|
|
|
53
49
|
}
|
|
54
50
|
}
|
|
55
51
|
|
|
56
|
-
export async function killDaemon(pid: number): Promise<void> {
|
|
57
|
-
await killProcess(pid)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
52
|
async function waitForSocket(socketPath: string): Promise<boolean> {
|
|
61
53
|
const deadline = Date.now() + 2_000
|
|
62
54
|
while (Date.now() < deadline) {
|
|
@@ -49,18 +49,45 @@ function sameFiles(a: GitFileEntry[], b: GitFileEntry[]): boolean {
|
|
|
49
49
|
|
|
50
50
|
export function reduceGitPanelState(state: AppState, action: AppAction): AppState | null {
|
|
51
51
|
switch (action.type) {
|
|
52
|
-
case 'toggle-git-
|
|
53
|
-
const
|
|
54
|
-
const
|
|
52
|
+
case 'toggle-git-pane': {
|
|
53
|
+
const nextVisible = !state.gitPane.visible
|
|
54
|
+
const sidebarMustShow = state.gitPane.mode === 'embedded' && nextVisible
|
|
55
55
|
return {
|
|
56
56
|
...state,
|
|
57
|
-
|
|
57
|
+
gitPane: { ...state.gitPane, visible: nextVisible },
|
|
58
|
+
sidebar: sidebarMustShow ? { ...state.sidebar, visible: true } : state.sidebar,
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
|
-
case 'resize-git-
|
|
61
|
-
const nextRatio = clampRatio(state.
|
|
62
|
-
if (nextRatio === state.
|
|
63
|
-
return { ...state,
|
|
61
|
+
case 'resize-git-pane': {
|
|
62
|
+
const nextRatio = clampRatio(state.gitPane.ratio + action.delta)
|
|
63
|
+
if (nextRatio === state.gitPane.ratio) return state
|
|
64
|
+
return { ...state, gitPane: { ...state.gitPane, ratio: nextRatio } }
|
|
65
|
+
}
|
|
66
|
+
case 'set-git-pane-mode': {
|
|
67
|
+
if (state.gitPane.mode === action.mode) return state
|
|
68
|
+
const isEmbedded = action.mode === 'embedded'
|
|
69
|
+
const isValidEmbedded =
|
|
70
|
+
state.gitPane.position === 'top' || state.gitPane.position === 'bottom'
|
|
71
|
+
const isValidPane = state.gitPane.position === 'left' || state.gitPane.position === 'right'
|
|
72
|
+
let nextPosition: typeof state.gitPane.position
|
|
73
|
+
if (isEmbedded) {
|
|
74
|
+
nextPosition = isValidEmbedded ? state.gitPane.position : 'bottom'
|
|
75
|
+
} else {
|
|
76
|
+
nextPosition = isValidPane ? state.gitPane.position : 'left'
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
...state,
|
|
80
|
+
gitPane: { ...state.gitPane, mode: action.mode, position: nextPosition },
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
case 'set-git-pane-position': {
|
|
84
|
+
const validForMode =
|
|
85
|
+
state.gitPane.mode === 'embedded'
|
|
86
|
+
? action.position === 'top' || action.position === 'bottom'
|
|
87
|
+
: action.position === 'left' || action.position === 'right'
|
|
88
|
+
if (!validForMode) return state
|
|
89
|
+
if (state.gitPane.position === action.position) return state
|
|
90
|
+
return { ...state, gitPane: { ...state.gitPane, position: action.position } }
|
|
64
91
|
}
|
|
65
92
|
case 'git-refresh-success': {
|
|
66
93
|
const prev = state.gitPanel
|
|
@@ -2,6 +2,8 @@ import { basename } from 'node:path'
|
|
|
2
2
|
|
|
3
3
|
import type { AppAction, AppState } from '../types'
|
|
4
4
|
|
|
5
|
+
import { collectHelpEntries } from '../../input/keymap/help-entries'
|
|
6
|
+
import { getActiveKeymap } from '../../input/keymap/keymap-ref'
|
|
5
7
|
import { getAllAssistantOptions } from '../../pty/command-registry'
|
|
6
8
|
import { THEME_IDS } from '../../ui/themes'
|
|
7
9
|
import { filterSessions, filterSnippets } from '../selectors'
|
|
@@ -38,18 +40,22 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
38
40
|
type: 'new-tab',
|
|
39
41
|
},
|
|
40
42
|
}
|
|
41
|
-
case 'open-help-modal':
|
|
43
|
+
case 'open-help-modal': {
|
|
44
|
+
const keymap = getActiveKeymap()
|
|
45
|
+
const entryCount = keymap ? collectHelpEntries(keymap).length : 0
|
|
42
46
|
return {
|
|
43
47
|
...state,
|
|
44
48
|
focusMode: 'modal',
|
|
45
49
|
modal: {
|
|
46
50
|
cursorPos: 0,
|
|
47
51
|
editBuffer: null,
|
|
52
|
+
entryCount,
|
|
48
53
|
selectedIndex: 0,
|
|
49
54
|
sessionTargetId: null,
|
|
50
55
|
type: 'help',
|
|
51
56
|
},
|
|
52
57
|
}
|
|
58
|
+
}
|
|
53
59
|
case 'open-split-picker':
|
|
54
60
|
return {
|
|
55
61
|
...state,
|
|
@@ -200,12 +206,45 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
200
206
|
modal: { ...state.modal, cursorPos: buf.length, editBuffer: buf },
|
|
201
207
|
}
|
|
202
208
|
}
|
|
209
|
+
case 'set-help-entry-count': {
|
|
210
|
+
if (state.modal.type !== 'help') return state
|
|
211
|
+
if (state.modal.entryCount === action.count) {
|
|
212
|
+
// Still clamp selectedIndex in case the count shrank below it.
|
|
213
|
+
const clamped = Math.min(state.modal.selectedIndex, Math.max(0, action.count - 1))
|
|
214
|
+
if (clamped === state.modal.selectedIndex) return state
|
|
215
|
+
return { ...state, modal: { ...state.modal, selectedIndex: clamped } }
|
|
216
|
+
}
|
|
217
|
+
const clamped = Math.min(state.modal.selectedIndex, Math.max(0, action.count - 1))
|
|
218
|
+
return {
|
|
219
|
+
...state,
|
|
220
|
+
modal: { ...state.modal, entryCount: action.count, selectedIndex: clamped },
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
case 'begin-help-filter': {
|
|
224
|
+
if (state.modal.type !== 'help') {
|
|
225
|
+
return state
|
|
226
|
+
}
|
|
227
|
+
const buf = state.modal.editBuffer ?? ''
|
|
228
|
+
return {
|
|
229
|
+
...state,
|
|
230
|
+
focusMode: 'command-edit',
|
|
231
|
+
modal: { ...state.modal, cursorPos: buf.length, editBuffer: buf },
|
|
232
|
+
}
|
|
233
|
+
}
|
|
203
234
|
case 'close-modal': {
|
|
204
235
|
const nextFocus: AppState['focusMode'] =
|
|
205
236
|
state.modal.type === 'git-commit' ? 'git' : 'navigation'
|
|
206
237
|
return { ...state, focusMode: nextFocus, modal: emptyModal() }
|
|
207
238
|
}
|
|
208
239
|
case 'move-modal-selection': {
|
|
240
|
+
if (state.modal.type === 'help') {
|
|
241
|
+
const count = state.modal.entryCount
|
|
242
|
+
if (count <= 0) return state
|
|
243
|
+
const raw = state.modal.selectedIndex + action.delta
|
|
244
|
+
const nextIndex = ((raw % count) + count) % count
|
|
245
|
+
if (nextIndex === state.modal.selectedIndex) return state
|
|
246
|
+
return { ...state, modal: { ...state.modal, selectedIndex: nextIndex } }
|
|
247
|
+
}
|
|
209
248
|
if (
|
|
210
249
|
state.modal.type !== 'new-tab' &&
|
|
211
250
|
state.modal.type !== 'session-picker' &&
|
|
@@ -293,7 +332,9 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
293
332
|
nextCursor = cursor + action.char.length
|
|
294
333
|
}
|
|
295
334
|
const resetIndex =
|
|
296
|
-
state.modal.type === 'session-picker' ||
|
|
335
|
+
state.modal.type === 'session-picker' ||
|
|
336
|
+
state.modal.type === 'snippet-picker' ||
|
|
337
|
+
state.modal.type === 'help'
|
|
297
338
|
? 0
|
|
298
339
|
: state.modal.selectedIndex
|
|
299
340
|
return {
|
|
@@ -360,7 +401,11 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
360
401
|
if (state.modal.type === 'create-session' || state.modal.type === 'snippet-editor') {
|
|
361
402
|
return { ...state, focusMode: 'navigation', modal: emptyModal() }
|
|
362
403
|
}
|
|
363
|
-
if (
|
|
404
|
+
if (
|
|
405
|
+
state.modal.type === 'session-picker' ||
|
|
406
|
+
state.modal.type === 'snippet-picker' ||
|
|
407
|
+
state.modal.type === 'help'
|
|
408
|
+
) {
|
|
364
409
|
return {
|
|
365
410
|
...state,
|
|
366
411
|
focusMode: 'modal',
|
package/src/state/selectors.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SessionRecord, SnippetRecord } from './types'
|
|
2
2
|
|
|
3
3
|
export function filterSessions(sessions: SessionRecord[], filter: string | null): SessionRecord[] {
|
|
4
4
|
if (!filter) {
|
|
@@ -24,7 +24,3 @@ export function filterSnippets(snippets: SnippetRecord[], filter: string | null)
|
|
|
24
24
|
snippet.name.toLowerCase().includes(lower) || snippet.content.toLowerCase().includes(lower)
|
|
25
25
|
)
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
export function getActiveTab(state: AppState): TabSession | undefined {
|
|
29
|
-
return state.activeTabId ? state.tabs.find((tab) => tab.id === state.activeTabId) : undefined
|
|
30
|
-
}
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
allLeafIds,
|
|
3
|
-
createGroupId,
|
|
4
|
-
createLeaf,
|
|
5
|
-
type LayoutNode,
|
|
6
|
-
pruneLayoutTree,
|
|
7
|
-
} from './layout-tree'
|
|
1
|
+
import { allLeafIds, createGroupId, type LayoutNode, pruneLayoutTree } from './layout-tree'
|
|
8
2
|
import {
|
|
9
3
|
type AppState,
|
|
10
4
|
DEFAULT_SCROLL_INTENT,
|
|
@@ -83,19 +77,6 @@ export function restoreTabsFromWorkspace(snapshot: WorkspaceSnapshotV1 | undefin
|
|
|
83
77
|
}))
|
|
84
78
|
}
|
|
85
79
|
|
|
86
|
-
export function restoreLayoutTree(
|
|
87
|
-
snapshot: WorkspaceSnapshotV1 | undefined,
|
|
88
|
-
tabs: TabSession[]
|
|
89
|
-
): LayoutNode | null {
|
|
90
|
-
if (snapshot?.layoutTree) {
|
|
91
|
-
const validTabIds = new Set(tabs.map((t) => t.id))
|
|
92
|
-
const pruned = pruneLayoutTree(snapshot.layoutTree, validTabIds)
|
|
93
|
-
if (pruned) return pruned
|
|
94
|
-
}
|
|
95
|
-
// Fallback: single leaf for the first tab
|
|
96
|
-
return tabs[0] ? createLeaf(tabs[0].id) : null
|
|
97
|
-
}
|
|
98
|
-
|
|
99
80
|
export function restoreLayoutTrees(
|
|
100
81
|
snapshot: WorkspaceSnapshotV1 | undefined,
|
|
101
82
|
tabs: TabSession[]
|
package/src/state/store.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AppAction,
|
|
3
|
+
AppState,
|
|
4
|
+
GitPaneMode,
|
|
5
|
+
GitPanePosition,
|
|
6
|
+
GitPaneState,
|
|
7
|
+
SessionBarPosition,
|
|
8
|
+
SessionRecord,
|
|
9
|
+
SnippetRecord,
|
|
10
|
+
} from './types'
|
|
2
11
|
|
|
3
12
|
import { emptyGitMode, reduceGitModeState } from './reducers/git-mode-state'
|
|
4
13
|
import { emptyGitPanel, reduceGitPanelState } from './reducers/git-panel-state'
|
|
@@ -15,12 +24,27 @@ const DEFAULT_TERMINAL_COLS = 80
|
|
|
15
24
|
const DEFAULT_TERMINAL_ROWS = 24
|
|
16
25
|
|
|
17
26
|
export interface InitialStateOverrides {
|
|
18
|
-
|
|
19
|
-
gitPanelRatio?: number
|
|
27
|
+
gitPane?: Partial<GitPaneState>
|
|
20
28
|
sessionBarVisible?: boolean
|
|
21
29
|
sessionBarPosition?: SessionBarPosition
|
|
22
30
|
}
|
|
23
31
|
|
|
32
|
+
const DEFAULT_GIT_PANE: GitPaneState = {
|
|
33
|
+
diffCount: { enabled: true },
|
|
34
|
+
mode: 'embedded',
|
|
35
|
+
path: { enabled: true },
|
|
36
|
+
position: 'bottom',
|
|
37
|
+
ratio: 0.5,
|
|
38
|
+
visible: true,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveGitPanePosition(mode: GitPaneMode, position: GitPanePosition): GitPanePosition {
|
|
42
|
+
if (mode === 'embedded') {
|
|
43
|
+
return position === 'top' || position === 'bottom' ? position : 'bottom'
|
|
44
|
+
}
|
|
45
|
+
return position === 'left' || position === 'right' ? position : 'left'
|
|
46
|
+
}
|
|
47
|
+
|
|
24
48
|
export function createInitialState(
|
|
25
49
|
customCommands: Record<string, string> = {},
|
|
26
50
|
sessions: SessionRecord[] = [],
|
|
@@ -28,12 +52,23 @@ export function createInitialState(
|
|
|
28
52
|
showSessionPicker = false,
|
|
29
53
|
overrides: InitialStateOverrides = {}
|
|
30
54
|
): AppState {
|
|
55
|
+
const gitPaneMode = overrides.gitPane?.mode ?? DEFAULT_GIT_PANE.mode
|
|
56
|
+
const gitPanePosition = resolveGitPanePosition(
|
|
57
|
+
gitPaneMode,
|
|
58
|
+
overrides.gitPane?.position ?? DEFAULT_GIT_PANE.position
|
|
59
|
+
)
|
|
31
60
|
return {
|
|
32
61
|
activeTabId: null,
|
|
33
62
|
currentSessionId: null,
|
|
34
63
|
customCommands,
|
|
35
64
|
focusMode: showSessionPicker ? 'modal' : 'navigation',
|
|
36
65
|
gitMode: emptyGitMode(),
|
|
66
|
+
gitPane: {
|
|
67
|
+
...DEFAULT_GIT_PANE,
|
|
68
|
+
...overrides.gitPane,
|
|
69
|
+
mode: gitPaneMode,
|
|
70
|
+
position: gitPanePosition,
|
|
71
|
+
},
|
|
37
72
|
gitPanel: emptyGitPanel(),
|
|
38
73
|
layout: {
|
|
39
74
|
terminalCols: DEFAULT_TERMINAL_COLS,
|
|
@@ -51,8 +86,6 @@ export function createInitialState(
|
|
|
51
86
|
sessions,
|
|
52
87
|
sessionsBusy: {},
|
|
53
88
|
sidebar: {
|
|
54
|
-
gitPanelRatio: overrides.gitPanelRatio ?? 0.5,
|
|
55
|
-
gitPanelVisible: overrides.gitPanelVisible ?? true,
|
|
56
89
|
maxWidth: DEFAULT_SIDEBAR_MAX_WIDTH,
|
|
57
90
|
minWidth: DEFAULT_SIDEBAR_MIN_WIDTH,
|
|
58
91
|
visible: true,
|
package/src/state/types.ts
CHANGED
|
@@ -6,13 +6,7 @@ export type TabStatus = 'starting' | 'running' | 'disconnected' | 'exited' | 'er
|
|
|
6
6
|
|
|
7
7
|
export type TabActivity = 'busy' | 'idle'
|
|
8
8
|
|
|
9
|
-
export type FocusMode =
|
|
10
|
-
| 'navigation'
|
|
11
|
-
| 'terminal-input'
|
|
12
|
-
| 'modal'
|
|
13
|
-
| 'command-edit'
|
|
14
|
-
| 'layout'
|
|
15
|
-
| 'git'
|
|
9
|
+
export type FocusMode = 'navigation' | 'terminal-input' | 'modal' | 'command-edit' | 'git'
|
|
16
10
|
|
|
17
11
|
export type ModalType =
|
|
18
12
|
| 'new-tab'
|
|
@@ -89,8 +83,6 @@ export interface WorkspaceSnapshotV1 {
|
|
|
89
83
|
sidebar: {
|
|
90
84
|
visible: boolean
|
|
91
85
|
width: number
|
|
92
|
-
gitPanelVisible?: boolean
|
|
93
|
-
gitPanelRatio?: number
|
|
94
86
|
}
|
|
95
87
|
tabs: PersistedTabSnapshot[]
|
|
96
88
|
layoutTree?: import('./layout-tree').LayoutNode
|
|
@@ -136,8 +128,24 @@ export interface SidebarState {
|
|
|
136
128
|
width: number
|
|
137
129
|
minWidth: number
|
|
138
130
|
maxWidth: number
|
|
139
|
-
|
|
140
|
-
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type GitPaneMode = 'embedded' | 'pane'
|
|
134
|
+
export type GitPanePosition = 'top' | 'bottom' | 'left' | 'right'
|
|
135
|
+
|
|
136
|
+
export type GitPanePathConfig =
|
|
137
|
+
| { enabled: false }
|
|
138
|
+
| { enabled: true; pathFn?: (path: string) => string }
|
|
139
|
+
|
|
140
|
+
export type GitPaneDiffCountConfig = { enabled: boolean }
|
|
141
|
+
|
|
142
|
+
export interface GitPaneState {
|
|
143
|
+
visible: boolean
|
|
144
|
+
mode: GitPaneMode
|
|
145
|
+
position: GitPanePosition
|
|
146
|
+
ratio: number
|
|
147
|
+
path: GitPanePathConfig
|
|
148
|
+
diffCount: GitPaneDiffCountConfig
|
|
141
149
|
}
|
|
142
150
|
|
|
143
151
|
export type GitFileStatus = 'M' | 'A' | 'D' | 'R' | 'C' | 'U' | '?'
|
|
@@ -222,6 +230,7 @@ export interface ModalThemePicker extends ModalBase {
|
|
|
222
230
|
|
|
223
231
|
export interface ModalHelp extends ModalBase {
|
|
224
232
|
type: 'help'
|
|
233
|
+
entryCount: number
|
|
225
234
|
}
|
|
226
235
|
|
|
227
236
|
export interface ModalSplitPicker extends ModalBase {
|
|
@@ -300,6 +309,7 @@ export interface AppState {
|
|
|
300
309
|
snippets: SnippetRecord[]
|
|
301
310
|
focusMode: FocusMode
|
|
302
311
|
sidebar: SidebarState
|
|
312
|
+
gitPane: GitPaneState
|
|
303
313
|
modal: ModalState
|
|
304
314
|
layout: LayoutState
|
|
305
315
|
customCommands: Record<AssistantId, string>
|
|
@@ -332,6 +342,8 @@ export type ModalAction =
|
|
|
332
342
|
| { type: 'open-snippet-picker' }
|
|
333
343
|
| { type: 'open-snippet-editor'; snippetId?: string }
|
|
334
344
|
| { type: 'begin-snippet-filter' }
|
|
345
|
+
| { type: 'begin-help-filter' }
|
|
346
|
+
| { type: 'set-help-entry-count'; count: number }
|
|
335
347
|
| { type: 'open-theme-picker' }
|
|
336
348
|
| { type: 'open-update-available-modal'; currentVersion: string; latestVersion: string }
|
|
337
349
|
|
|
@@ -407,8 +419,10 @@ export type UIAction =
|
|
|
407
419
|
| { type: 'resize-sidebar'; delta: number }
|
|
408
420
|
| { type: 'set-focus-mode'; focusMode: FocusMode }
|
|
409
421
|
| { type: 'set-terminal-size'; cols: number; rows: number }
|
|
410
|
-
| { type: 'toggle-git-
|
|
411
|
-
| { type: 'resize-git-
|
|
422
|
+
| { type: 'toggle-git-pane' }
|
|
423
|
+
| { type: 'resize-git-pane'; delta: number }
|
|
424
|
+
| { type: 'set-git-pane-mode'; mode: GitPaneMode }
|
|
425
|
+
| { type: 'set-git-pane-position'; position: GitPanePosition }
|
|
412
426
|
| { type: 'set-pending-chords'; chords: string[] | null }
|
|
413
427
|
| { type: 'toggle-session-bar' }
|
|
414
428
|
| { type: 'set-session-bar-position'; position: SessionBarPosition }
|
package/src/state/validation.ts
CHANGED
|
@@ -114,8 +114,6 @@ export function isWorkspaceSnapshotV1(value: unknown): value is WorkspaceSnapsho
|
|
|
114
114
|
isObjectRecord(value.sidebar) &&
|
|
115
115
|
isBoolean(value.sidebar.visible) &&
|
|
116
116
|
isFiniteNumber(value.sidebar.width) &&
|
|
117
|
-
(value.sidebar.gitPanelVisible === undefined || isBoolean(value.sidebar.gitPanelVisible)) &&
|
|
118
|
-
(value.sidebar.gitPanelRatio === undefined || isFiniteNumber(value.sidebar.gitPanelRatio)) &&
|
|
119
117
|
Array.isArray(value.tabs) &&
|
|
120
118
|
value.tabs.every(
|
|
121
119
|
(tab) =>
|
|
@@ -24,8 +24,12 @@ export function saveCurrentWorkspace(state: AppState): void {
|
|
|
24
24
|
saveConfig({
|
|
25
25
|
...loadConfig(),
|
|
26
26
|
customCommands: state.customCommands,
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
gitPane: {
|
|
28
|
+
mode: state.gitPane.mode,
|
|
29
|
+
position: state.gitPane.position,
|
|
30
|
+
ratio: state.gitPane.ratio,
|
|
31
|
+
visible: state.gitPane.visible,
|
|
32
|
+
},
|
|
29
33
|
sessionBarPosition: state.sessionBar.position,
|
|
30
34
|
sessionBarVisible: state.sessionBar.visible,
|
|
31
35
|
})
|