@brimveyn/aimux 1.3.0 → 1.4.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/README.md +3 -0
- package/package.json +2 -2
- package/src/app-runtime/backend-runtime-events.ts +13 -1
- package/src/app-runtime/pty-write.ts +7 -4
- package/src/app-runtime/side-effects.ts +72 -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 +113 -37
- package/src/config.ts +32 -1
- 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 +3 -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 +39 -5
- package/src/session-backend/remote-session-backend.ts +18 -5
- package/src/session-backend/types.ts +5 -2
- package/src/state/dispatch-ref.ts +11 -0
- package/src/state/reducers/modal-state.ts +18 -1
- package/src/state/reducers/session-state.ts +28 -0
- package/src/state/reducers/tab-state.ts +20 -2
- package/src/state/reducers/ui-state.ts +8 -0
- package/src/state/session-catalog.ts +22 -1
- package/src/state/session-persistence.ts +9 -2
- package/src/state/store.ts +8 -1
- package/src/state/types.ts +37 -0
- package/src/state/validation.ts +10 -0
- package/src/state/workspace-save.ts +2 -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-bar.tsx +208 -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/tab-item.tsx +3 -15
- package/src/ui/components/theme-picker-modal.tsx +3 -6
- package/src/ui/components/update-available-modal.tsx +42 -0
- package/src/ui/hooks/use-busy-spinner.ts +18 -0
- package/src/ui/keymap-context.ts +39 -0
- package/src/ui/root.tsx +16 -0
- package/src/ui/session-ordering.ts +34 -0
- package/src/ui/status-bar-model.ts +67 -39
- package/src/update/version-check.ts +67 -0
|
@@ -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
|
>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { AppState } from '../../state/types'
|
|
2
2
|
|
|
3
|
+
import { version as APP_VERSION } from '../../../package.json'
|
|
3
4
|
import { useAppStore } from '../../state/app-store'
|
|
5
|
+
import { useKeymap } from '../keymap-context'
|
|
4
6
|
import { getStatusBarModel } from '../status-bar-model'
|
|
5
7
|
import { theme } from '../theme'
|
|
6
8
|
|
|
@@ -43,7 +45,8 @@ function getModeLabel(focusMode: AppState['focusMode']): string {
|
|
|
43
45
|
export function StatusBar() {
|
|
44
46
|
const state = useAppStore((s) => s)
|
|
45
47
|
const activeTab = state.tabs.find((tab) => tab.id === state.activeTabId)
|
|
46
|
-
const
|
|
48
|
+
const config = useKeymap()
|
|
49
|
+
const model = getStatusBarModel(state, activeTab, config)
|
|
47
50
|
|
|
48
51
|
return (
|
|
49
52
|
<box
|
|
@@ -60,8 +63,9 @@ export function StatusBar() {
|
|
|
60
63
|
<text> </text>
|
|
61
64
|
<text fg={theme.text}>{model.left}</text>
|
|
62
65
|
</box>
|
|
63
|
-
<box width="100%">
|
|
66
|
+
<box width="100%" flexDirection="row" justifyContent="space-between">
|
|
64
67
|
<text fg={theme.textMuted}>{model.right}</text>
|
|
68
|
+
<text fg={theme.dim}>v{APP_VERSION}</text>
|
|
65
69
|
</box>
|
|
66
70
|
</box>
|
|
67
71
|
)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
|
|
3
1
|
import type { TabSession } from '../../state/types'
|
|
4
2
|
|
|
3
|
+
import { useBusySpinner } from '../hooks/use-busy-spinner'
|
|
5
4
|
import { theme } from '../theme'
|
|
6
5
|
|
|
7
6
|
interface TabItemProps {
|
|
@@ -28,9 +27,6 @@ function getStatusColor(status: TabSession['status']): string {
|
|
|
28
27
|
}
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
const BUSY_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
32
|
-
const BUSY_FRAME_INTERVAL_MS = 80
|
|
33
|
-
|
|
34
30
|
function getIndicator(active: boolean, focused: boolean, inLayout: boolean): string {
|
|
35
31
|
if (active) {
|
|
36
32
|
return focused ? '›' : '•'
|
|
@@ -48,16 +44,8 @@ function getIndicatorColor(active: boolean, focused: boolean, inLayout: boolean)
|
|
|
48
44
|
}
|
|
49
45
|
|
|
50
46
|
function BusyIndicator() {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
useEffect(() => {
|
|
54
|
-
const interval = setInterval(() => {
|
|
55
|
-
setFrame((prev) => (prev + 1) % BUSY_FRAMES.length)
|
|
56
|
-
}, BUSY_FRAME_INTERVAL_MS)
|
|
57
|
-
return () => clearInterval(interval)
|
|
58
|
-
}, [])
|
|
59
|
-
|
|
60
|
-
return <text fg={theme.accent}>{BUSY_FRAMES[frame]} busy</text>
|
|
47
|
+
const frame = useBusySpinner()
|
|
48
|
+
return <text fg={theme.accent}>{frame} busy</text>
|
|
61
49
|
}
|
|
62
50
|
|
|
63
51
|
function ActivityIndicator({ isFocusedInput, tab }: { tab: TabSession; isFocusedInput: boolean }) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useModalHelp } from '../keymap-context'
|
|
1
2
|
import { theme } from '../theme'
|
|
2
3
|
import { THEME_IDS, type ThemeId, THEMES } from '../themes'
|
|
3
4
|
import { uiTokens } from '../ui-tokens'
|
|
@@ -10,13 +11,9 @@ interface ThemePickerModalProps {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export function ThemePickerModal({ currentThemeId, selectedIndex }: ThemePickerModalProps) {
|
|
14
|
+
const help = useModalHelp('modal.theme-picker')
|
|
13
15
|
return (
|
|
14
|
-
<ModalShell
|
|
15
|
-
title="Select theme"
|
|
16
|
-
help="j/k move, Enter confirm, Esc cancel."
|
|
17
|
-
width={uiTokens.modalWidth.md}
|
|
18
|
-
listGap={0}
|
|
19
|
-
>
|
|
16
|
+
<ModalShell title="Select theme" help={help} width={uiTokens.modalWidth.md} listGap={0}>
|
|
20
17
|
{THEME_IDS.map((id, index) => {
|
|
21
18
|
const entry = THEMES[id]
|
|
22
19
|
const active = index === selectedIndex
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useModalHelp } from '../keymap-context'
|
|
2
|
+
import { theme } from '../theme'
|
|
3
|
+
import { uiTokens } from '../ui-tokens'
|
|
4
|
+
import { ListItem } from './list-item'
|
|
5
|
+
import { ModalShell } from './modal-shell'
|
|
6
|
+
|
|
7
|
+
interface UpdateAvailableModalProps {
|
|
8
|
+
currentVersion: string
|
|
9
|
+
latestVersion: string
|
|
10
|
+
selectedIndex: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const OPTIONS: { label: string }[] = [
|
|
14
|
+
{ label: 'Yes, update now' },
|
|
15
|
+
{ label: 'No, skip this version' },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
export function UpdateAvailableModal({
|
|
19
|
+
currentVersion,
|
|
20
|
+
latestVersion,
|
|
21
|
+
selectedIndex,
|
|
22
|
+
}: UpdateAvailableModalProps) {
|
|
23
|
+
const modalHelp = useModalHelp('modal.update-available')
|
|
24
|
+
const help = `${currentVersion} → ${latestVersion} ${modalHelp}`
|
|
25
|
+
return (
|
|
26
|
+
<ModalShell title="Update available" help={help} width={uiTokens.modalWidth.md} listGap={1}>
|
|
27
|
+
<box flexDirection="row" gap={1} marginTop={1}>
|
|
28
|
+
{OPTIONS.map((option, index) => {
|
|
29
|
+
const active = index === selectedIndex
|
|
30
|
+
return (
|
|
31
|
+
<ListItem
|
|
32
|
+
key={option.label}
|
|
33
|
+
active={active}
|
|
34
|
+
direction="row"
|
|
35
|
+
title={<text fg={active ? theme.text : theme.textMuted}>{option.label}</text>}
|
|
36
|
+
/>
|
|
37
|
+
)
|
|
38
|
+
})}
|
|
39
|
+
</box>
|
|
40
|
+
</ModalShell>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
export const BUSY_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
4
|
+
export const BUSY_FRAME_INTERVAL_MS = 80
|
|
5
|
+
|
|
6
|
+
export function useBusySpinner(enabled = true): string {
|
|
7
|
+
const [frame, setFrame] = useState(0)
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!enabled) return
|
|
11
|
+
const interval = setInterval(() => {
|
|
12
|
+
setFrame((prev) => (prev + 1) % BUSY_FRAMES.length)
|
|
13
|
+
}, BUSY_FRAME_INTERVAL_MS)
|
|
14
|
+
return () => clearInterval(interval)
|
|
15
|
+
}, [enabled])
|
|
16
|
+
|
|
17
|
+
return BUSY_FRAMES[frame] ?? BUSY_FRAMES[0] ?? ''
|
|
18
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getDefaultKeymapConfig,
|
|
3
|
+
type ModeId,
|
|
4
|
+
type ResolvedKeymapConfig,
|
|
5
|
+
} from '@brimveyn/aimux-config'
|
|
6
|
+
import { createContext, useContext, useMemo } from 'react'
|
|
7
|
+
|
|
8
|
+
import { describeBindings } from '../input/keymap/describe-bindings'
|
|
9
|
+
|
|
10
|
+
const FALLBACK_CONFIG = getDefaultKeymapConfig()
|
|
11
|
+
|
|
12
|
+
export const KeymapContext = createContext<ResolvedKeymapConfig>(FALLBACK_CONFIG)
|
|
13
|
+
|
|
14
|
+
export function useKeymap(): ResolvedKeymapConfig {
|
|
15
|
+
return useContext(KeymapContext)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const HELP_JOINER = ' · '
|
|
19
|
+
const MAX_HELP_ENTRIES = 5
|
|
20
|
+
|
|
21
|
+
/** Build a single-line help string for a mode (used in modal headers and status bar). */
|
|
22
|
+
export function buildHintText(
|
|
23
|
+
config: ResolvedKeymapConfig,
|
|
24
|
+
modeId: ModeId,
|
|
25
|
+
limit: number = MAX_HELP_ENTRIES
|
|
26
|
+
): string {
|
|
27
|
+
const bindings = describeBindings(config, modeId, {
|
|
28
|
+
dedupeByDescription: true,
|
|
29
|
+
withDescriptionOnly: true,
|
|
30
|
+
})
|
|
31
|
+
if (bindings.length === 0) return ''
|
|
32
|
+
const trimmed = bindings.slice(0, limit)
|
|
33
|
+
return trimmed.map((b) => `${b.keysDisplay} ${b.description ?? ''}`.trim()).join(HELP_JOINER)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function useModalHelp(modeId: ModeId, limit?: number): string {
|
|
37
|
+
const config = useKeymap()
|
|
38
|
+
return useMemo(() => buildHintText(config, modeId, limit), [config, modeId, limit])
|
|
39
|
+
}
|
package/src/ui/root.tsx
CHANGED
|
@@ -11,6 +11,8 @@ import { GitCommitModal } from './components/git-commit-modal'
|
|
|
11
11
|
import { GitView } from './components/git-view'
|
|
12
12
|
import { HelpModal } from './components/help-modal'
|
|
13
13
|
import { NewTabModal } from './components/new-tab-modal'
|
|
14
|
+
import { PendingChordOverlay } from './components/pending-chord-overlay'
|
|
15
|
+
import { SessionBar } from './components/session-bar'
|
|
14
16
|
import { SessionNameModal } from './components/session-name-modal'
|
|
15
17
|
import { SessionPickerModal } from './components/session-picker-modal'
|
|
16
18
|
import { Sidebar } from './components/sidebar'
|
|
@@ -20,6 +22,7 @@ import { SplitLayout } from './components/split-layout'
|
|
|
20
22
|
import { StatusBar } from './components/status-bar'
|
|
21
23
|
import { TerminalPane } from './components/terminal-pane'
|
|
22
24
|
import { ThemePickerModal } from './components/theme-picker-modal'
|
|
25
|
+
import { UpdateAvailableModal } from './components/update-available-modal'
|
|
23
26
|
import { theme } from './theme'
|
|
24
27
|
|
|
25
28
|
function getCreateSessionFields(modal: ModalState) {
|
|
@@ -132,6 +135,14 @@ function renderModal(
|
|
|
132
135
|
return (
|
|
133
136
|
<ThemePickerModal selectedIndex={modal.selectedIndex} currentThemeId={options.themeId} />
|
|
134
137
|
)
|
|
138
|
+
case 'update-available':
|
|
139
|
+
return (
|
|
140
|
+
<UpdateAvailableModal
|
|
141
|
+
selectedIndex={modal.selectedIndex}
|
|
142
|
+
currentVersion={modal.currentVersion}
|
|
143
|
+
latestVersion={modal.latestVersion}
|
|
144
|
+
/>
|
|
145
|
+
)
|
|
135
146
|
case 'help':
|
|
136
147
|
return <HelpModal />
|
|
137
148
|
case 'git-commit': {
|
|
@@ -203,6 +214,7 @@ export function RootView({
|
|
|
203
214
|
const customCommands = useAppStore((s) => s.customCommands)
|
|
204
215
|
const sessions = useAppStore((s) => s.sessions)
|
|
205
216
|
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
217
|
+
const sessionBarPosition = useAppStore((s) => s.sessionBar.position)
|
|
206
218
|
|
|
207
219
|
const activeTab = tabs.find((tab) => tab.id === activeTabId)
|
|
208
220
|
const activeTree = activeTabId ? getTreeForTab(layoutTrees, tabGroupMap, activeTabId) : null
|
|
@@ -216,6 +228,7 @@ export function RootView({
|
|
|
216
228
|
<box flexDirection="column" width="100%" height="100%" backgroundColor={theme.background}>
|
|
217
229
|
<GitView />
|
|
218
230
|
<StatusBar />
|
|
231
|
+
<PendingChordOverlay />
|
|
219
232
|
{renderModal(modal, {
|
|
220
233
|
createSessionFields,
|
|
221
234
|
currentSessionId,
|
|
@@ -232,6 +245,7 @@ export function RootView({
|
|
|
232
245
|
|
|
233
246
|
return (
|
|
234
247
|
<box flexDirection="column" width="100%" height="100%" backgroundColor={theme.background}>
|
|
248
|
+
{sessionBarPosition === 'top' && <SessionBar />}
|
|
235
249
|
<box flexDirection="row" gap={0} padding={0} flexGrow={1}>
|
|
236
250
|
<Sidebar onTabActivate={onPaneActivate} />
|
|
237
251
|
{activeTree && activeTree.type === 'split' ? (
|
|
@@ -279,7 +293,9 @@ export function RootView({
|
|
|
279
293
|
/>
|
|
280
294
|
)}
|
|
281
295
|
</box>
|
|
296
|
+
{sessionBarPosition === 'bottom' && <SessionBar />}
|
|
282
297
|
<StatusBar />
|
|
298
|
+
<PendingChordOverlay />
|
|
283
299
|
{renderModal(modal, {
|
|
284
300
|
createSessionFields,
|
|
285
301
|
currentSessionId,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SessionRecord } from '../state/types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Return sessions in user-facing display order: persisted `order` ascending,
|
|
5
|
+
* with any missing `order` falling back to `createdAt` ascending.
|
|
6
|
+
*/
|
|
7
|
+
export function orderSessionsForDisplay(sessions: SessionRecord[]): SessionRecord[] {
|
|
8
|
+
return sessions.slice().sort((a, b) => {
|
|
9
|
+
const ao = a.order ?? Number.MAX_SAFE_INTEGER
|
|
10
|
+
const bo = b.order ?? Number.MAX_SAFE_INTEGER
|
|
11
|
+
if (ao !== bo) return ao - bo
|
|
12
|
+
return a.createdAt.localeCompare(b.createdAt)
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Move `moveId` to the slot currently held by `intoPositionOfId`, shifting the
|
|
18
|
+
* displaced id in the opposite direction. Pure; returns a new array. Returns
|
|
19
|
+
* the input unchanged if either id is missing or both refer to the same slot.
|
|
20
|
+
*/
|
|
21
|
+
export function moveIdToIdPosition(
|
|
22
|
+
ids: string[],
|
|
23
|
+
moveId: string,
|
|
24
|
+
intoPositionOfId: string
|
|
25
|
+
): string[] {
|
|
26
|
+
if (moveId === intoPositionOfId) return ids
|
|
27
|
+
const from = ids.indexOf(moveId)
|
|
28
|
+
const to = ids.indexOf(intoPositionOfId)
|
|
29
|
+
if (from < 0 || to < 0) return ids
|
|
30
|
+
const next = ids.slice()
|
|
31
|
+
next.splice(from, 1)
|
|
32
|
+
next.splice(to, 0, moveId)
|
|
33
|
+
return next
|
|
34
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import type { ModeId, ResolvedKeymapConfig } from '@brimveyn/aimux-config'
|
|
2
|
+
|
|
1
3
|
import type { AppState, TabSession } from '../state/types'
|
|
2
4
|
|
|
5
|
+
import { buildHintText } from './keymap-context'
|
|
3
6
|
import { abbreviatePath } from './path-format'
|
|
4
7
|
|
|
5
8
|
export interface StatusBarModel {
|
|
@@ -8,6 +11,7 @@ export interface StatusBarModel {
|
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
const MAX_TAB_LABEL_LENGTH = 24
|
|
14
|
+
const HINT_LIMIT = 6
|
|
11
15
|
|
|
12
16
|
function truncateLabel(label: string): string {
|
|
13
17
|
if (label.length <= MAX_TAB_LABEL_LENGTH) {
|
|
@@ -25,39 +29,15 @@ function getActiveTabLabel(tab?: TabSession): string {
|
|
|
25
29
|
return `${truncateLabel(tab.title)} (${tab.status})`
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
return 'Ctrl+g sessions Ctrl+n new Ctrl+b toggle Ctrl+h/l resize ? help'
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (activeTab.status === 'disconnected') {
|
|
34
|
-
return 'Ctrl+r restart restored tab Ctrl+w close i focus'
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return 'Ctrl+g sessions j/k move Shift+J/K reorder Ctrl+r restart Ctrl+w close i focus ? help'
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function getInputHint(activeTab?: TabSession): string {
|
|
41
|
-
if (!activeTab) {
|
|
42
|
-
return 'Ctrl+n new no active tab to focus'
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (activeTab.status === 'disconnected') {
|
|
46
|
-
return 'Ctrl+z unfocus Ctrl+w layout Ctrl+r restart'
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return 'Ctrl+z unfocus Ctrl+w layout typing goes to active tab'
|
|
32
|
+
function hintForMode(config: ResolvedKeymapConfig, modeId: ModeId): string {
|
|
33
|
+
return buildHintText(config, modeId, HINT_LIMIT)
|
|
50
34
|
}
|
|
51
35
|
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return 'j/k file Ctrl+d/u page a stage d unstage/delete c commit p push Esc exit'
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function getStatusBarModel(state: AppState, activeTab?: TabSession): StatusBarModel {
|
|
36
|
+
export function getStatusBarModel(
|
|
37
|
+
state: AppState,
|
|
38
|
+
activeTab: TabSession | undefined,
|
|
39
|
+
config: ResolvedKeymapConfig
|
|
40
|
+
): StatusBarModel {
|
|
61
41
|
const currentSession = state.currentSessionId
|
|
62
42
|
? state.sessions.find((session) => session.id === state.currentSessionId)
|
|
63
43
|
: undefined
|
|
@@ -73,33 +53,81 @@ export function getStatusBarModel(state: AppState, activeTab?: TabSession): Stat
|
|
|
73
53
|
case 'terminal-input':
|
|
74
54
|
return {
|
|
75
55
|
left: `${getActiveTabLabel(activeTab)} ${sessionIcon} ${sessionLabel}`,
|
|
76
|
-
right:
|
|
56
|
+
right: hintForMode(config, 'terminal-input'),
|
|
77
57
|
}
|
|
78
|
-
case 'modal':
|
|
58
|
+
case 'modal': {
|
|
59
|
+
const modalMode = deriveModalModeId(state.modal.type)
|
|
79
60
|
return {
|
|
80
61
|
left: `${sessionIcon} ${sessionLabel}`,
|
|
81
|
-
right:
|
|
62
|
+
right: modalMode ? hintForMode(config, modalMode) : '',
|
|
82
63
|
}
|
|
64
|
+
}
|
|
83
65
|
case 'layout':
|
|
84
66
|
return {
|
|
85
67
|
left: `${getActiveTabLabel(activeTab)} ${sessionIcon} ${sessionLabel}`,
|
|
86
|
-
right:
|
|
68
|
+
right: hintForMode(config, 'layout'),
|
|
87
69
|
}
|
|
88
70
|
case 'git':
|
|
89
71
|
return {
|
|
90
72
|
left: `${sessionIcon} ${sessionLabel}`,
|
|
91
|
-
right:
|
|
73
|
+
right: hintForMode(config, 'git-mode'),
|
|
92
74
|
}
|
|
93
|
-
case 'command-edit':
|
|
75
|
+
case 'command-edit': {
|
|
76
|
+
const commandEditMode = deriveCommandEditModeId(state.modal.type)
|
|
94
77
|
return {
|
|
95
78
|
left: `${sessionIcon} ${sessionLabel}`,
|
|
96
|
-
right:
|
|
79
|
+
right: commandEditMode ? hintForMode(config, commandEditMode) : '',
|
|
97
80
|
}
|
|
81
|
+
}
|
|
98
82
|
case 'navigation':
|
|
99
83
|
default:
|
|
100
84
|
return {
|
|
101
85
|
left: `${sessionIcon} ${sessionLabel} ${getActiveTabLabel(activeTab)}`,
|
|
102
|
-
right:
|
|
86
|
+
right: hintForMode(config, 'navigation'),
|
|
103
87
|
}
|
|
104
88
|
}
|
|
105
89
|
}
|
|
90
|
+
|
|
91
|
+
function deriveModalModeId(modalType: AppState['modal']['type']): ModeId | null {
|
|
92
|
+
switch (modalType) {
|
|
93
|
+
case 'help':
|
|
94
|
+
return 'modal.help'
|
|
95
|
+
case 'new-tab':
|
|
96
|
+
return 'modal.new-tab'
|
|
97
|
+
case 'session-picker':
|
|
98
|
+
return 'modal.session-picker'
|
|
99
|
+
case 'snippet-picker':
|
|
100
|
+
return 'modal.snippet-picker'
|
|
101
|
+
case 'split-picker':
|
|
102
|
+
return 'modal.split-picker'
|
|
103
|
+
case 'theme-picker':
|
|
104
|
+
return 'modal.theme-picker'
|
|
105
|
+
case 'update-available':
|
|
106
|
+
return 'modal.update-available'
|
|
107
|
+
default:
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function deriveCommandEditModeId(modalType: AppState['modal']['type']): ModeId | null {
|
|
113
|
+
switch (modalType) {
|
|
114
|
+
case 'create-session':
|
|
115
|
+
return 'modal.create-session'
|
|
116
|
+
case 'git-commit':
|
|
117
|
+
return 'modal.git-commit'
|
|
118
|
+
case 'new-tab':
|
|
119
|
+
return 'modal.new-tab.command-edit'
|
|
120
|
+
case 'rename-tab':
|
|
121
|
+
return 'modal.rename-tab'
|
|
122
|
+
case 'session-name':
|
|
123
|
+
return 'modal.session-name'
|
|
124
|
+
case 'session-picker':
|
|
125
|
+
return 'modal.session-picker.filtering'
|
|
126
|
+
case 'snippet-editor':
|
|
127
|
+
return 'modal.snippet-editor'
|
|
128
|
+
case 'snippet-picker':
|
|
129
|
+
return 'modal.snippet-picker.filtering'
|
|
130
|
+
default:
|
|
131
|
+
return null
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { logDebug } from '../debug/input-log'
|
|
2
|
+
|
|
3
|
+
const NPM_REGISTRY_BASE = 'https://registry.npmjs.org'
|
|
4
|
+
|
|
5
|
+
export async function getCurrentPackageVersion(): Promise<string> {
|
|
6
|
+
const { version } = await import('../../package.json')
|
|
7
|
+
return version
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function fetchLatestNpmVersion(packageName: string): Promise<string | null> {
|
|
11
|
+
const debugOverride = process.env.AIMUX_DEBUG_UPDATE_LATEST
|
|
12
|
+
if (debugOverride) {
|
|
13
|
+
return debugOverride
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const url = `${NPM_REGISTRY_BASE}/${encodeURIComponent(packageName).replace('%40', '@')}/latest`
|
|
18
|
+
const controller = new AbortController()
|
|
19
|
+
const timer = setTimeout(() => controller.abort(), 5_000)
|
|
20
|
+
const res = await fetch(url, { signal: controller.signal })
|
|
21
|
+
clearTimeout(timer)
|
|
22
|
+
if (!res.ok) {
|
|
23
|
+
logDebug('update.fetchLatest.nonOk', { packageName, status: res.status })
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
const data = (await res.json()) as { version?: unknown }
|
|
27
|
+
if (typeof data.version !== 'string' || data.version.length === 0) {
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
return data.version
|
|
31
|
+
} catch (error) {
|
|
32
|
+
logDebug('update.fetchLatest.error', {
|
|
33
|
+
error: error instanceof Error ? error.message : String(error),
|
|
34
|
+
packageName,
|
|
35
|
+
})
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseSemver(version: string): number[] | null {
|
|
41
|
+
const trimmed = version.trim().replace(/^v/, '')
|
|
42
|
+
const core = trimmed.split(/[-+]/)[0] ?? ''
|
|
43
|
+
const parts = core.split('.')
|
|
44
|
+
if (parts.length === 0) return null
|
|
45
|
+
const numeric: number[] = []
|
|
46
|
+
for (const part of parts) {
|
|
47
|
+
const n = Number.parseInt(part, 10)
|
|
48
|
+
if (!Number.isFinite(n) || n < 0) return null
|
|
49
|
+
numeric.push(n)
|
|
50
|
+
}
|
|
51
|
+
return numeric
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isNewerVersion(latest: string, current: string): boolean {
|
|
55
|
+
const a = parseSemver(latest)
|
|
56
|
+
const b = parseSemver(current)
|
|
57
|
+
if (!a || !b) return false
|
|
58
|
+
|
|
59
|
+
const len = Math.max(a.length, b.length)
|
|
60
|
+
for (let i = 0; i < len; i++) {
|
|
61
|
+
const av = a[i] ?? 0
|
|
62
|
+
const bv = b[i] ?? 0
|
|
63
|
+
if (av > bv) return true
|
|
64
|
+
if (av < bv) return false
|
|
65
|
+
}
|
|
66
|
+
return false
|
|
67
|
+
}
|