@brimveyn/aimux 1.7.1 → 1.7.2
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 +3 -4
- package/src/app-runtime/pty-write.ts +8 -3
- package/src/app-runtime/side-effects.ts +10 -2
- package/src/app-runtime/use-renderer-bindings.ts +2 -1
- package/src/app.tsx +5 -8
- package/src/config.ts +2 -10
- package/src/input/modes/bridge.ts +1 -1
- package/src/input/modes/types.ts +6 -1
- package/src/input/raw-input-handler.ts +12 -3
- package/src/pty/terminal-snapshot.ts +4 -4
- package/src/state/reducers/modal-state.ts +3 -2
- package/src/state/reducers/session-state.ts +0 -7
- package/src/state/types.ts +7 -1
- package/src/ui/components/bare-input.tsx +5 -5
- package/src/ui/components/context-menu-box.tsx +21 -0
- package/src/ui/components/context-menu-overlay.tsx +114 -0
- package/src/ui/components/create-session-modal.tsx +12 -41
- package/src/ui/components/diff-renderer/fold-strip.tsx +7 -7
- package/src/ui/components/diff-renderer/highlight.ts +6 -10
- package/src/ui/components/diff-renderer/pierre-diff.tsx +3 -3
- package/src/ui/components/diff-renderer/prepare-diff.ts +2 -3
- package/src/ui/components/diff-renderer/split-view.tsx +22 -29
- package/src/ui/components/diff-renderer/stacked-view.tsx +18 -31
- package/src/ui/components/diff-renderer/use-diff-prefetch.ts +0 -1
- package/src/ui/components/diff-renderer/use-diff-preparation.ts +1 -1
- package/src/ui/components/git-commit-modal.tsx +4 -16
- package/src/ui/components/git-panel.tsx +37 -73
- package/src/ui/components/git-view.tsx +17 -19
- package/src/ui/components/help-modal.tsx +5 -13
- package/src/ui/components/input-field.tsx +5 -7
- package/src/ui/components/list-item.tsx +3 -11
- package/src/ui/components/modal-keybinds-overlay.tsx +4 -4
- package/src/ui/components/modal-shell.tsx +6 -11
- package/src/ui/components/new-tab-modal.tsx +7 -15
- package/src/ui/components/pending-chord-overlay.tsx +5 -5
- package/src/ui/components/picker.tsx +6 -6
- package/src/ui/components/session-bar.tsx +34 -20
- package/src/ui/components/session-picker-modal.tsx +7 -20
- package/src/ui/components/sidebar.tsx +36 -35
- package/src/ui/components/snippet-editor-modal.tsx +4 -18
- package/src/ui/components/snippet-picker-modal.tsx +5 -9
- package/src/ui/components/split-layout.tsx +3 -3
- package/src/ui/components/status-bar.tsx +12 -13
- package/src/ui/components/surface.tsx +8 -11
- package/src/ui/components/tab-item.tsx +54 -29
- package/src/ui/components/terminal-pane.tsx +59 -25
- package/src/ui/components/theme-picker-modal.tsx +7 -21
- package/src/ui/components/update-available-modal.tsx +3 -13
- package/src/ui/context-menu/controller.ts +34 -0
- package/src/ui/root.tsx +5 -2
- package/src/ui/shiki.ts +29 -21
- package/src/ui/theme-store.ts +110 -21
- package/src/ui/theme.ts +6 -3
- package/src/ui/themes.ts +18 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brimveyn/aimux",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.2",
|
|
4
4
|
"description": "A terminal multiplexer for AI CLIs. Run Claude, Codex, OpenCode side-by-side with tabbed navigation, split panes, and persistent sessions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -57,11 +57,10 @@
|
|
|
57
57
|
"format": "oxfmt --write .",
|
|
58
58
|
"format:check": "oxfmt --check .",
|
|
59
59
|
"knip": "knip-bun",
|
|
60
|
-
"bump": "bun run scripts/bump.ts"
|
|
61
|
-
"generate-themes": "bun run scripts/generate-themes.ts"
|
|
60
|
+
"bump": "bun run scripts/bump.ts"
|
|
62
61
|
},
|
|
63
62
|
"dependencies": {
|
|
64
|
-
"@brimveyn/aimux-config": "0.4.
|
|
63
|
+
"@brimveyn/aimux-config": "0.4.5",
|
|
65
64
|
"@opentui/core": "^0.1.90",
|
|
66
65
|
"@opentui/react": "^0.1.90",
|
|
67
66
|
"@xterm/headless": "^6.0.0",
|
|
@@ -3,6 +3,10 @@ import type { AppAction, TabSession } from '../state/types'
|
|
|
3
3
|
|
|
4
4
|
import { buildPtyPastePayload } from '../input/paste'
|
|
5
5
|
|
|
6
|
+
export interface PtyWriteOptions {
|
|
7
|
+
autoBottom?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
6
10
|
function shouldScrollViewportToBottom(tab: TabSession): boolean {
|
|
7
11
|
const viewport = tab.viewport
|
|
8
12
|
return viewport !== undefined && viewport.viewportY < viewport.baseY
|
|
@@ -13,9 +17,10 @@ export function writeToTab(
|
|
|
13
17
|
tabId: string,
|
|
14
18
|
tab: TabSession | undefined,
|
|
15
19
|
input: string,
|
|
16
|
-
dispatch?: (action: AppAction) => void
|
|
20
|
+
dispatch?: (action: AppAction) => void,
|
|
21
|
+
options?: PtyWriteOptions
|
|
17
22
|
): void {
|
|
18
|
-
if (tab && shouldScrollViewportToBottom(tab)) {
|
|
23
|
+
if ((options?.autoBottom ?? true) && tab && shouldScrollViewportToBottom(tab)) {
|
|
19
24
|
backend.scrollViewportToBottom(tabId)
|
|
20
25
|
dispatch?.({ intent: { kind: 'bottom' }, tabId, type: 'set-scroll-intent' })
|
|
21
26
|
}
|
|
@@ -31,5 +36,5 @@ export function writePasteToTab(
|
|
|
31
36
|
dispatch?: (action: AppAction) => void
|
|
32
37
|
): void {
|
|
33
38
|
const payload = buildPtyPastePayload(text, tab?.terminalModes.bracketedPasteMode ?? false)
|
|
34
|
-
writeToTab(backend, tabId, tab, payload, dispatch)
|
|
39
|
+
writeToTab(backend, tabId, tab, payload, dispatch, { autoBottom: true })
|
|
35
40
|
}
|
|
@@ -385,6 +385,10 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
|
|
|
385
385
|
handleSelectedSessionDelete(ctx)
|
|
386
386
|
return
|
|
387
387
|
}
|
|
388
|
+
case 'delete-session': {
|
|
389
|
+
handleDeleteSessionEffect(state, backend, dispatch, effect.sessionId)
|
|
390
|
+
return
|
|
391
|
+
}
|
|
388
392
|
case 'open-rename-selected-session': {
|
|
389
393
|
openSelectedSessionRename(ctx)
|
|
390
394
|
return
|
|
@@ -453,8 +457,12 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
|
|
|
453
457
|
return
|
|
454
458
|
}
|
|
455
459
|
case 'split-pane': {
|
|
456
|
-
const
|
|
457
|
-
|
|
460
|
+
const sourceTab = effect.sourceTabId
|
|
461
|
+
? state.tabs.find((t) => t.id === effect.sourceTabId)
|
|
462
|
+
: undefined
|
|
463
|
+
const assistant = sourceTab?.assistant ?? 'terminal'
|
|
464
|
+
const customCommand = state.customCommands[assistant]
|
|
465
|
+
const tab = createTabSession(assistant, customCommand, state.customCommands)
|
|
458
466
|
executeSplitPane(ctx, effect.direction, tab)
|
|
459
467
|
return
|
|
460
468
|
}
|
|
@@ -60,7 +60,8 @@ export function useRendererBindings({
|
|
|
60
60
|
activeTabRef.current?.terminalModes.bracketedPasteMode ?? false,
|
|
61
61
|
getFocusMode: () => focusModeRef.current,
|
|
62
62
|
handleTerminalShortcut,
|
|
63
|
-
writeToPty: (tabId, data) =>
|
|
63
|
+
writeToPty: (tabId, data, options) =>
|
|
64
|
+
writeToTab(backend, tabId, activeTabRef.current, data, dispatch, options),
|
|
64
65
|
})
|
|
65
66
|
|
|
66
67
|
const handlePasteEvent = (event: { bytes: Uint8Array; defaultPrevented?: boolean }) => {
|
package/src/app.tsx
CHANGED
|
@@ -38,7 +38,7 @@ import { appReducer, createInitialState } from './state/store'
|
|
|
38
38
|
import { KeymapContext } from './ui/keymap-context'
|
|
39
39
|
import { RootView } from './ui/root'
|
|
40
40
|
import { applyTheme, setTransparent } from './ui/theme'
|
|
41
|
-
import { isKnownThemeId,
|
|
41
|
+
import { isKnownThemeId, type ThemeId } from './ui/themes'
|
|
42
42
|
import {
|
|
43
43
|
fetchLatestNpmVersion,
|
|
44
44
|
getCurrentPackageVersion,
|
|
@@ -66,15 +66,12 @@ export function App({
|
|
|
66
66
|
const renderer = useRenderer()
|
|
67
67
|
const dimensions = useTerminalDimensions()
|
|
68
68
|
const [themeId, setThemeId] = useState<ThemeId>(() => {
|
|
69
|
-
registerUserThemes(resolvedConfig.themes)
|
|
70
69
|
const config = loadConfig()
|
|
71
70
|
const persisted = config.themeId && isKnownThemeId(config.themeId) ? config.themeId : undefined
|
|
72
|
-
const fromConfig =
|
|
73
|
-
resolvedConfig.theme
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const initial = persisted ?? fromConfig ?? 'aimux'
|
|
77
|
-
applyTheme(initial)
|
|
71
|
+
const fromConfig: ThemeId =
|
|
72
|
+
resolvedConfig.theme?.mode === 'light' ? 'aimux-light' : 'aimux-dark'
|
|
73
|
+
const initial: ThemeId = persisted ?? fromConfig
|
|
74
|
+
applyTheme(initial, resolvedConfig.theme?.paletteOverrides)
|
|
78
75
|
setTransparent(config.themeTransparent ?? false)
|
|
79
76
|
return initial
|
|
80
77
|
})
|
package/src/config.ts
CHANGED
|
@@ -5,19 +5,11 @@ import type { GitFileListMode, SessionBarPosition, WorkspaceSnapshotV1 } from '.
|
|
|
5
5
|
import { logDebug } from './debug/input-log'
|
|
6
6
|
import { getProfileConfigDir } from './profile-paths'
|
|
7
7
|
import { isWorkspaceSnapshotV1 } from './state/validation'
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
const LEGACY_THEME_ALIASES: Record<string, ThemeId> = {
|
|
11
|
-
'everforest': 'everforest-dark',
|
|
12
|
-
'gruvbox-dark': 'gruvbox-dark-hard',
|
|
13
|
-
'kanagawa': 'kanagawa-wave',
|
|
14
|
-
'one-dark': 'one-dark-pro',
|
|
15
|
-
}
|
|
8
|
+
import { migrateThemeId as resolveLegacyThemeId, type ThemeId } from './ui/themes'
|
|
16
9
|
|
|
17
10
|
function migrateThemeId(value: unknown): ThemeId | undefined {
|
|
18
11
|
if (typeof value !== 'string') return undefined
|
|
19
|
-
|
|
20
|
-
return LEGACY_THEME_ALIASES[value]
|
|
12
|
+
return resolveLegacyThemeId(value)
|
|
21
13
|
}
|
|
22
14
|
|
|
23
15
|
export const CONFIG_PATH = `${getProfileConfigDir()}/aimux.json`
|
|
@@ -19,11 +19,11 @@ const COMMAND_EDIT_MODE_IDS: Partial<Record<SupportedModalType, ModeId>> = {
|
|
|
19
19
|
'session-picker': 'modal.session-picker.filtering',
|
|
20
20
|
'snippet-editor': 'modal.snippet-editor',
|
|
21
21
|
'snippet-picker': 'modal.snippet-picker.filtering',
|
|
22
|
+
'split-picker': 'modal.split-picker',
|
|
22
23
|
'theme-picker': 'modal.theme-picker.filtering',
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
const MODAL_MODE_IDS: Partial<Record<SupportedModalType, ModeId>> = {
|
|
26
|
-
'split-picker': 'modal.split-picker',
|
|
27
27
|
'update-available': 'modal.update-available',
|
|
28
28
|
}
|
|
29
29
|
|
package/src/input/modes/types.ts
CHANGED
|
@@ -40,7 +40,11 @@ export type SideEffect =
|
|
|
40
40
|
| { type: 'apply-theme'; action: 'confirm' }
|
|
41
41
|
| { type: 'apply-theme'; action: 'preview'; delta: 1 | -1 }
|
|
42
42
|
| { type: 'rename-session'; sessionId: string; name: string }
|
|
43
|
-
| {
|
|
43
|
+
| {
|
|
44
|
+
type: 'split-pane'
|
|
45
|
+
direction: import('../../state/layout-tree').SplitDirection
|
|
46
|
+
sourceTabId?: string
|
|
47
|
+
}
|
|
44
48
|
| { type: 'confirm-split' }
|
|
45
49
|
| { type: 'scroll-git-diff'; delta: number }
|
|
46
50
|
| { type: 'persist-git-diff-mode-ratio'; ratio: number }
|
|
@@ -54,6 +58,7 @@ export type SideEffect =
|
|
|
54
58
|
| { type: 'git-push' }
|
|
55
59
|
| { type: 'confirm-update-selection' }
|
|
56
60
|
| { type: 'switch-session-by-index'; index: number }
|
|
61
|
+
| { type: 'delete-session'; sessionId: string }
|
|
57
62
|
| { type: 'toggle-transparent' }
|
|
58
63
|
|
|
59
64
|
export interface KeyResult {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PtyWriteOptions } from '../app-runtime/pty-write'
|
|
1
2
|
import type { FocusMode } from '../state/types'
|
|
2
3
|
|
|
3
4
|
import { logInputDebug } from '../debug/input-log'
|
|
@@ -10,6 +11,8 @@ const KITTY_MOD_SUPER = 8
|
|
|
10
11
|
const KITTY_MOD_HYPER = 16
|
|
11
12
|
const KITTY_MOD_META = 32
|
|
12
13
|
const KITTY_HOST_MOD_MASK = KITTY_MOD_SUPER | KITTY_MOD_HYPER | KITTY_MOD_META
|
|
14
|
+
const FOCUS_IN_SEQUENCE = `${ESC}[I`
|
|
15
|
+
const FOCUS_OUT_SEQUENCE = `${ESC}[O`
|
|
13
16
|
|
|
14
17
|
function normalizeControlSequence(sequence: string): string | null {
|
|
15
18
|
const match = KITTY_CTRL_RE.exec(sequence)
|
|
@@ -79,7 +82,7 @@ export function createRawInputHandler(deps: {
|
|
|
79
82
|
getFocusMode: () => FocusMode
|
|
80
83
|
getActiveTabId: () => string | null
|
|
81
84
|
getBracketedPasteModeEnabled: () => boolean
|
|
82
|
-
writeToPty: (tabId: string, data: string) => void
|
|
85
|
+
writeToPty: (tabId: string, data: string, options?: PtyWriteOptions) => void
|
|
83
86
|
/**
|
|
84
87
|
* Dispatch a configured terminal-input shortcut.
|
|
85
88
|
* Returns true if the chord was consumed by the keymap, false otherwise.
|
|
@@ -88,6 +91,10 @@ export function createRawInputHandler(deps: {
|
|
|
88
91
|
}): (sequence: string) => boolean {
|
|
89
92
|
let bracketedPasteBuffer: string | null = null
|
|
90
93
|
|
|
94
|
+
function isFocusReportSequence(sequence: string): boolean {
|
|
95
|
+
return sequence === FOCUS_IN_SEQUENCE || sequence === FOCUS_OUT_SEQUENCE
|
|
96
|
+
}
|
|
97
|
+
|
|
91
98
|
function flushPaste(tabId: string, payload: string): void {
|
|
92
99
|
logInputDebug('raw.flushPaste', {
|
|
93
100
|
bracketedPasteModeEnabled: deps.getBracketedPasteModeEnabled(),
|
|
@@ -95,7 +102,9 @@ export function createRawInputHandler(deps: {
|
|
|
95
102
|
payloadPreview: payload.slice(0, 120),
|
|
96
103
|
tabId,
|
|
97
104
|
})
|
|
98
|
-
deps.writeToPty(tabId, buildPtyPastePayload(payload, deps.getBracketedPasteModeEnabled())
|
|
105
|
+
deps.writeToPty(tabId, buildPtyPastePayload(payload, deps.getBracketedPasteModeEnabled()), {
|
|
106
|
+
autoBottom: true,
|
|
107
|
+
})
|
|
99
108
|
}
|
|
100
109
|
|
|
101
110
|
function handleTerminalShortcut(sequence: string): boolean {
|
|
@@ -162,7 +171,7 @@ export function createRawInputHandler(deps: {
|
|
|
162
171
|
return true
|
|
163
172
|
}
|
|
164
173
|
|
|
165
|
-
deps.writeToPty(tabId, normalized)
|
|
174
|
+
deps.writeToPty(tabId, normalized, { autoBottom: !isFocusReportSequence(normalized) })
|
|
166
175
|
return true
|
|
167
176
|
}
|
|
168
177
|
|
|
@@ -113,15 +113,15 @@ function buildLine(
|
|
|
113
113
|
let bg = getColorHex(current.getBgColor(), bgMode)
|
|
114
114
|
|
|
115
115
|
if (current.isInverse()) {
|
|
116
|
-
const resolvedFg = fg ?? getCurrentTheme().
|
|
117
|
-
const resolvedBg = bg ?? getCurrentTheme().
|
|
116
|
+
const resolvedFg = fg ?? getCurrentTheme().palette.ink
|
|
117
|
+
const resolvedBg = bg ?? getCurrentTheme().palette.neutral
|
|
118
118
|
;[fg, bg] = [resolvedBg, resolvedFg]
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
const isCursorCell = cursorVisible && cursorColumn === column
|
|
122
122
|
if (isCursorCell) {
|
|
123
|
-
const resolvedFg = fg ?? getCurrentTheme().
|
|
124
|
-
const resolvedBg = bg ?? getCurrentTheme().
|
|
123
|
+
const resolvedFg = fg ?? getCurrentTheme().palette.ink
|
|
124
|
+
const resolvedBg = bg ?? getCurrentTheme().palette.neutral
|
|
125
125
|
;[fg, bg] = [resolvedBg, resolvedFg]
|
|
126
126
|
}
|
|
127
127
|
|
|
@@ -74,10 +74,10 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
74
74
|
case 'open-split-picker':
|
|
75
75
|
return {
|
|
76
76
|
...state,
|
|
77
|
-
focusMode: '
|
|
77
|
+
focusMode: 'command-edit',
|
|
78
78
|
modal: {
|
|
79
79
|
cursorPos: 0,
|
|
80
|
-
editBuffer:
|
|
80
|
+
editBuffer: '',
|
|
81
81
|
selectedIndex: 0,
|
|
82
82
|
sessionTargetId: null,
|
|
83
83
|
splitDirection: action.direction,
|
|
@@ -104,6 +104,7 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
104
104
|
modal: {
|
|
105
105
|
cursorPos: initialName.length,
|
|
106
106
|
editBuffer: initialName,
|
|
107
|
+
returnToSessionPicker: action.returnToSessionPicker ?? true,
|
|
107
108
|
selectedIndex: 0,
|
|
108
109
|
sessionTargetId: action.sessionTargetId ?? null,
|
|
109
110
|
type: 'session-name',
|
|
@@ -46,13 +46,6 @@ export function reduceSessionState(state: AppState, action: AppAction): AppState
|
|
|
46
46
|
case 'rename-session-record':
|
|
47
47
|
return {
|
|
48
48
|
...state,
|
|
49
|
-
focusMode: 'modal',
|
|
50
|
-
modal: {
|
|
51
|
-
editBuffer: null,
|
|
52
|
-
selectedIndex: state.modal.selectedIndex,
|
|
53
|
-
sessionTargetId: null,
|
|
54
|
-
type: 'session-picker',
|
|
55
|
-
},
|
|
56
49
|
sessions: state.sessions.map((session) =>
|
|
57
50
|
session.id === action.sessionId
|
|
58
51
|
? { ...session, name: action.name, updatedAt: new Date().toISOString() }
|
package/src/state/types.ts
CHANGED
|
@@ -291,6 +291,7 @@ export interface ModalSessionPicker extends ModalBase {
|
|
|
291
291
|
|
|
292
292
|
export interface ModalSessionName extends ModalBase {
|
|
293
293
|
type: 'session-name'
|
|
294
|
+
returnToSessionPicker: boolean
|
|
294
295
|
}
|
|
295
296
|
|
|
296
297
|
export interface ModalRenameTab extends ModalBase {
|
|
@@ -407,7 +408,12 @@ export type ModalAction =
|
|
|
407
408
|
| { type: 'open-help-modal'; scope?: ModeId }
|
|
408
409
|
| { type: 'open-split-picker'; direction: import('./layout-tree').SplitDirection }
|
|
409
410
|
| { type: 'open-session-picker' }
|
|
410
|
-
| {
|
|
411
|
+
| {
|
|
412
|
+
type: 'open-session-name-modal'
|
|
413
|
+
sessionTargetId?: string
|
|
414
|
+
initialName?: string
|
|
415
|
+
returnToSessionPicker?: boolean
|
|
416
|
+
}
|
|
411
417
|
| { type: 'close-modal' }
|
|
412
418
|
| { type: 'move-modal-selection'; delta: number }
|
|
413
419
|
| { type: 'update-command-edit'; char: string }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useTokens } from '../theme'
|
|
2
2
|
|
|
3
3
|
interface BareInputProps {
|
|
4
4
|
value: string
|
|
@@ -7,10 +7,10 @@ interface BareInputProps {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export function BareInput({ cursorPos, placeholder = '', value }: BareInputProps) {
|
|
10
|
-
const
|
|
11
|
-
const fg =
|
|
12
|
-
const bg =
|
|
13
|
-
const placeholderFg =
|
|
10
|
+
const t = useTokens()
|
|
11
|
+
const fg = t.palette.ink
|
|
12
|
+
const bg = t.palette.neutral
|
|
13
|
+
const placeholderFg = t.faint
|
|
14
14
|
|
|
15
15
|
if (!value) {
|
|
16
16
|
const firstChar = placeholder.charAt(0) || ' '
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { BoxRenderable, MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
2
|
+
import type { BoxProps } from '@opentui/react'
|
|
3
|
+
|
|
4
|
+
import { type ContextMenuItem, openContextMenu } from '../context-menu/controller'
|
|
5
|
+
|
|
6
|
+
interface ContextMenuBoxProps extends BoxProps {
|
|
7
|
+
rightClickMenu?: ContextMenuItem[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function ContextMenuBox({ onMouseDown, rightClickMenu, ...boxProps }: ContextMenuBoxProps) {
|
|
11
|
+
function handleMouseDown(this: BoxRenderable, event: OtuiMouseEvent): void {
|
|
12
|
+
if (event.button === 2 && rightClickMenu && rightClickMenu.length > 0) {
|
|
13
|
+
event.preventDefault()
|
|
14
|
+
event.stopPropagation()
|
|
15
|
+
openContextMenu(event.x, event.y, rightClickMenu)
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
onMouseDown?.call(this, event)
|
|
19
|
+
}
|
|
20
|
+
return <box {...boxProps} onMouseDown={handleMouseDown} />
|
|
21
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { useKeyboard } from '@opentui/react'
|
|
2
|
+
import { useEffect, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import { useAppStore } from '../../state/app-store'
|
|
5
|
+
import {
|
|
6
|
+
closeContextMenu,
|
|
7
|
+
type ContextMenuState,
|
|
8
|
+
subscribeContextMenu,
|
|
9
|
+
} from '../context-menu/controller'
|
|
10
|
+
import { useTokens } from '../theme'
|
|
11
|
+
|
|
12
|
+
export function ContextMenuOverlay() {
|
|
13
|
+
const [menu, setMenu] = useState<ContextMenuState | null>(null)
|
|
14
|
+
const [selected, setSelected] = useState(0)
|
|
15
|
+
const t = useTokens()
|
|
16
|
+
const terminalCols = useAppStore((s) => s.layout.terminalCols)
|
|
17
|
+
const terminalRows = useAppStore((s) => s.layout.terminalRows)
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
return subscribeContextMenu((state) => {
|
|
21
|
+
setMenu(state)
|
|
22
|
+
setSelected(0)
|
|
23
|
+
})
|
|
24
|
+
}, [])
|
|
25
|
+
|
|
26
|
+
useKeyboard((key) => {
|
|
27
|
+
if (!menu) return
|
|
28
|
+
if (key.name === 'escape') {
|
|
29
|
+
key.preventDefault()
|
|
30
|
+
closeContextMenu()
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
if (key.name === 'up') {
|
|
34
|
+
key.preventDefault()
|
|
35
|
+
setSelected((i) => (i - 1 + menu.items.length) % menu.items.length)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
if (key.name === 'down') {
|
|
39
|
+
key.preventDefault()
|
|
40
|
+
setSelected((i) => (i + 1) % menu.items.length)
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
if (key.name === 'return') {
|
|
44
|
+
key.preventDefault()
|
|
45
|
+
const item = menu.items[selected]
|
|
46
|
+
closeContextMenu()
|
|
47
|
+
item?.[1]()
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
if (!menu) return null
|
|
52
|
+
|
|
53
|
+
const maxLabel = Math.max(...menu.items.map(([label]) => label.length))
|
|
54
|
+
const width = maxLabel + 4
|
|
55
|
+
const height = menu.items.length + 2
|
|
56
|
+
const left = Math.max(0, Math.min(menu.anchorX, terminalCols - width))
|
|
57
|
+
const top = Math.max(0, Math.min(menu.anchorY, terminalRows - height))
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<box position="absolute" top={0} left={0} width="100%" height="100%">
|
|
61
|
+
<box
|
|
62
|
+
position="absolute"
|
|
63
|
+
top={0}
|
|
64
|
+
left={0}
|
|
65
|
+
width="100%"
|
|
66
|
+
height="100%"
|
|
67
|
+
onMouseDown={(e) => {
|
|
68
|
+
e.preventDefault()
|
|
69
|
+
e.stopPropagation()
|
|
70
|
+
closeContextMenu()
|
|
71
|
+
}}
|
|
72
|
+
/>
|
|
73
|
+
<box
|
|
74
|
+
position="absolute"
|
|
75
|
+
top={top}
|
|
76
|
+
left={left}
|
|
77
|
+
width={width}
|
|
78
|
+
flexDirection="column"
|
|
79
|
+
border
|
|
80
|
+
borderColor={t.palette.primary}
|
|
81
|
+
backgroundColor={t.elevated}
|
|
82
|
+
onMouseDown={(e) => {
|
|
83
|
+
e.stopPropagation()
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
{menu.items.map(([label, onSelect], index) => {
|
|
87
|
+
const active = index === selected
|
|
88
|
+
return (
|
|
89
|
+
<box
|
|
90
|
+
key={`${label}-${index}`}
|
|
91
|
+
width={width - 2}
|
|
92
|
+
flexShrink={0}
|
|
93
|
+
paddingLeft={1}
|
|
94
|
+
paddingRight={1}
|
|
95
|
+
backgroundColor={active ? t.selected : undefined}
|
|
96
|
+
onMouseOver={() => setSelected(index)}
|
|
97
|
+
onMouseDown={(e) => {
|
|
98
|
+
e.preventDefault()
|
|
99
|
+
e.stopPropagation()
|
|
100
|
+
if (e.button !== 0) return
|
|
101
|
+
closeContextMenu()
|
|
102
|
+
onSelect()
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
<text fg={active ? t.palette.ink : t.muted} selectable={false}>
|
|
106
|
+
{label}
|
|
107
|
+
</text>
|
|
108
|
+
</box>
|
|
109
|
+
)
|
|
110
|
+
})}
|
|
111
|
+
</box>
|
|
112
|
+
</box>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { DirectoryResult } from '../../state/types'
|
|
2
2
|
|
|
3
3
|
import { abbreviatePath } from '../path-format'
|
|
4
|
-
import {
|
|
4
|
+
import { getCurrentTokens, useTokens } from '../theme'
|
|
5
5
|
import { uiTokens } from '../ui-tokens'
|
|
6
6
|
import { InputField } from './input-field'
|
|
7
7
|
import { ListItem } from './list-item'
|
|
@@ -10,27 +10,16 @@ import { ModalShell } from './modal-shell'
|
|
|
10
10
|
const VISIBLE_ROWS = 8
|
|
11
11
|
|
|
12
12
|
function getDirectoryResultIcon(result: DirectoryResult): string {
|
|
13
|
-
if (result.type === 'worktree') {
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (result.type === 'workspace') {
|
|
18
|
-
return '\u{f07c}'
|
|
19
|
-
}
|
|
20
|
-
|
|
13
|
+
if (result.type === 'worktree') return '\u{e728}'
|
|
14
|
+
if (result.type === 'workspace') return '\u{f07c}'
|
|
21
15
|
return '\u{e702}'
|
|
22
16
|
}
|
|
23
17
|
|
|
24
18
|
function getDirectoryResultColor(result: DirectoryResult): string {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (result.type === 'workspace') {
|
|
30
|
-
return getCurrentTheme().colors['terminal.ansiMagenta']
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return getCurrentTheme().colors['textLink.foreground']
|
|
19
|
+
const t = getCurrentTokens()
|
|
20
|
+
if (result.type === 'worktree') return t.palette.warning
|
|
21
|
+
if (result.type === 'workspace') return t.accent
|
|
22
|
+
return t.palette.primary
|
|
34
23
|
}
|
|
35
24
|
|
|
36
25
|
interface CreateSessionModalProps {
|
|
@@ -50,7 +39,7 @@ export function CreateSessionModal({
|
|
|
50
39
|
selectedIndex,
|
|
51
40
|
sessionName,
|
|
52
41
|
}: CreateSessionModalProps) {
|
|
53
|
-
const
|
|
42
|
+
const t = useTokens()
|
|
54
43
|
const dirActive = activeField === 'directory'
|
|
55
44
|
const nameActive = activeField === 'name'
|
|
56
45
|
|
|
@@ -64,11 +53,7 @@ export function CreateSessionModal({
|
|
|
64
53
|
width={uiTokens.modalWidth.xl}
|
|
65
54
|
>
|
|
66
55
|
<box flexDirection="column">
|
|
67
|
-
<text
|
|
68
|
-
fg={dirActive ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']}
|
|
69
|
-
>
|
|
70
|
-
Search projects
|
|
71
|
-
</text>
|
|
56
|
+
<text fg={dirActive ? t.palette.ink : t.muted}>Search projects</text>
|
|
72
57
|
<InputField
|
|
73
58
|
active={dirActive}
|
|
74
59
|
placeholder="Type a project name..."
|
|
@@ -80,7 +65,7 @@ export function CreateSessionModal({
|
|
|
80
65
|
|
|
81
66
|
<box flexDirection="column" height={VISIBLE_ROWS}>
|
|
82
67
|
{results.length === 0 ? (
|
|
83
|
-
<text fg={
|
|
68
|
+
<text fg={t.muted}>
|
|
84
69
|
{directoryQuery.length > 0 ? 'No matches' : 'Type a project name to search...'}
|
|
85
70
|
</text>
|
|
86
71
|
) : (
|
|
@@ -94,15 +79,7 @@ export function CreateSessionModal({
|
|
|
94
79
|
<text fg={getDirectoryResultColor(result)}>{getDirectoryResultIcon(result)}</text>
|
|
95
80
|
}
|
|
96
81
|
title={
|
|
97
|
-
<text
|
|
98
|
-
fg={
|
|
99
|
-
active
|
|
100
|
-
? theme.colors['editor.foreground']
|
|
101
|
-
: theme.colors['descriptionForeground']
|
|
102
|
-
}
|
|
103
|
-
>
|
|
104
|
-
{abbreviatePath(result.path)}
|
|
105
|
-
</text>
|
|
82
|
+
<text fg={active ? t.palette.ink : t.muted}>{abbreviatePath(result.path)}</text>
|
|
106
83
|
}
|
|
107
84
|
/>
|
|
108
85
|
)
|
|
@@ -111,13 +88,7 @@ export function CreateSessionModal({
|
|
|
111
88
|
</box>
|
|
112
89
|
|
|
113
90
|
<box flexDirection="column">
|
|
114
|
-
<text
|
|
115
|
-
fg={
|
|
116
|
-
nameActive ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']
|
|
117
|
-
}
|
|
118
|
-
>
|
|
119
|
-
Session name
|
|
120
|
-
</text>
|
|
91
|
+
<text fg={nameActive ? t.palette.ink : t.muted}>Session name</text>
|
|
121
92
|
<InputField active={nameActive} value={sessionName} />
|
|
122
93
|
</box>
|
|
123
94
|
</ModalShell>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useBg,
|
|
1
|
+
import { useBg, useTokens } from '../../theme'
|
|
2
2
|
import { FOLD_STEP, type FoldInfo } from './build-rows'
|
|
3
3
|
import { type FoldDispatch } from './pierre-diff'
|
|
4
4
|
|
|
@@ -8,11 +8,11 @@ interface Props {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
function Button({ label, onPress }: { label: string; onPress: () => void }) {
|
|
11
|
-
const
|
|
12
|
-
const bg = useBg('
|
|
11
|
+
const t = useTokens()
|
|
12
|
+
const bg = useBg('elevated')
|
|
13
13
|
return (
|
|
14
14
|
<box paddingLeft={1} paddingRight={1} backgroundColor={bg} onMouseDown={onPress}>
|
|
15
|
-
<text fg={
|
|
15
|
+
<text fg={t.palette.primary}>{label}</text>
|
|
16
16
|
</box>
|
|
17
17
|
)
|
|
18
18
|
}
|
|
@@ -22,8 +22,8 @@ function Spacer() {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export function FoldStrip({ dispatch, fold }: Props) {
|
|
25
|
-
const
|
|
26
|
-
const headerBg = useBg('
|
|
25
|
+
const t = useTokens()
|
|
26
|
+
const headerBg = useBg('elevated')
|
|
27
27
|
const { bottomExpanded, foldId, hidden, topExpanded, total } = fold
|
|
28
28
|
const stepUp = Math.min(FOLD_STEP, hidden)
|
|
29
29
|
const stepDown = Math.min(FOLD_STEP, hidden)
|
|
@@ -71,7 +71,7 @@ export function FoldStrip({ dispatch, fold }: Props) {
|
|
|
71
71
|
|
|
72
72
|
return (
|
|
73
73
|
<box flexDirection="row" backgroundColor={headerBg} paddingLeft={1} paddingRight={1}>
|
|
74
|
-
<text fg={
|
|
74
|
+
<text fg={t.muted}>{`⋯ ${hidden} hidden `}</text>
|
|
75
75
|
{controls}
|
|
76
76
|
</box>
|
|
77
77
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ThemedToken } from 'shiki'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { ensureActiveShikiTheme, ensureShikiLang, getShikiHighlighter } from '../../shiki'
|
|
4
4
|
|
|
5
5
|
export interface HighlightSpan {
|
|
6
6
|
bold?: boolean
|
|
@@ -22,24 +22,20 @@ export function tokenToSpan(token: ThemedToken): HighlightSpan {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export async function tokenizeSide(
|
|
26
|
-
lines: string[],
|
|
27
|
-
lang: string,
|
|
28
|
-
theme: string
|
|
29
|
-
): Promise<ThemedToken[][]> {
|
|
25
|
+
export async function tokenizeSide(lines: string[], lang: string): Promise<ThemedToken[][]> {
|
|
30
26
|
if (lines.length === 0) return []
|
|
31
27
|
const highlighter = await getShikiHighlighter()
|
|
32
|
-
const [langOk,
|
|
28
|
+
const [langOk, themeName] = await Promise.all([
|
|
33
29
|
ensureShikiLang(highlighter, lang),
|
|
34
|
-
|
|
30
|
+
ensureActiveShikiTheme(highlighter),
|
|
35
31
|
])
|
|
36
|
-
if (!langOk
|
|
32
|
+
if (!langOk) return []
|
|
37
33
|
try {
|
|
38
34
|
const result = highlighter.codeToTokens(lines.join(''), {
|
|
39
35
|
// eslint-disable-next-line typescript/no-explicit-any
|
|
40
36
|
lang: lang as any,
|
|
41
37
|
// eslint-disable-next-line typescript/no-explicit-any
|
|
42
|
-
theme:
|
|
38
|
+
theme: themeName as any,
|
|
43
39
|
})
|
|
44
40
|
return result.tokens
|
|
45
41
|
} catch {
|