@brimveyn/aimux 1.12.2 → 1.13.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 +4 -3
- package/src/app-runtime/auto-commit-driver.ts +8 -4
- package/src/app-runtime/auto-commit-ref.ts +1 -1
- package/src/app-runtime/backend-attach-runtime.ts +18 -2
- package/src/app-runtime/backend-runtime-events.ts +1 -1
- package/src/app-runtime/pty-write.ts +52 -0
- package/src/app-runtime/selection-scroll.ts +2 -2
- package/src/app-runtime/session-actions.ts +16 -4
- package/src/app-runtime/side-effects.ts +598 -102
- package/src/app-runtime/snippet-actions.ts +25 -11
- package/src/app-runtime/use-auto-commit-driver.ts +7 -4
- package/src/app-runtime/use-backend-runtime.ts +3 -3
- package/src/app-runtime/use-directory-search.ts +7 -5
- package/src/app-runtime/use-mouse-handlers.ts +11 -6
- package/src/app-runtime/use-pane-size-report.ts +1 -1
- package/src/app-runtime/use-renderer-bindings.ts +143 -8
- package/src/app.tsx +25 -9
- package/src/auto-commit/headless-commands.ts +4 -6
- package/src/daemon/daemon.ts +3 -3
- package/src/daemon/runtime-paths.ts +1 -1
- package/src/daemon/session-manager.ts +1 -1
- package/src/daemon/session-registry.ts +10 -3
- package/src/diff-parser/parse-patch-files.ts +18 -18
- package/src/git/command-queue.ts +1 -1
- package/src/git/divergence.ts +46 -0
- package/src/git/git-diff.ts +12 -5
- package/src/git/git-poller.ts +33 -11
- package/src/git/git-status.ts +14 -3
- package/src/git/move-worktree.ts +96 -0
- package/src/git/use-repo-discovery.ts +2 -1
- package/src/git/worktree-divergence-poller.ts +59 -0
- package/src/git/worktree.ts +99 -0
- package/src/index.tsx +1 -1
- package/src/input/keymap/describe-bindings.ts +27 -9
- package/src/input/keymap/key-chord.ts +4 -4
- package/src/input/keymap/key-format.ts +2 -2
- package/src/input/keymap/sequence-resolver.ts +1 -1
- package/src/input/keymap/trie.ts +1 -1
- package/src/input/modes/bridge.ts +7 -0
- package/src/input/modes/transitions.ts +2 -1
- package/src/input/modes/types.ts +13 -1
- package/src/input/raw-input-handler.ts +34 -1
- package/src/input/terminal-text-extraction.ts +9 -5
- package/src/integrations/claude-syntax-overlay.ts +8 -8
- package/src/integrations/claude-theme-sync.ts +12 -12
- package/src/ipc/protocol.ts +3 -3
- package/src/platform/clipboard.ts +18 -2
- package/src/platform/worktree-deps.ts +22 -0
- package/src/platform/worktree-paths.ts +67 -0
- package/src/profile-paths.ts +3 -3
- package/src/pty/assistant-status-detection-loop.ts +2 -2
- package/src/pty/assistant-status-detector.ts +15 -6
- package/src/pty/command-registry.ts +8 -1
- package/src/pty/pty-manager.ts +1 -1
- package/src/services/ai-usage/adapters/claude.ts +5 -5
- package/src/services/ai-usage/adapters/codex.ts +8 -8
- package/src/services/ai-usage/cache.ts +2 -2
- package/src/services/ai-usage/pace.ts +3 -6
- package/src/services/ai-usage/provider.ts +1 -1
- package/src/session-backend/bootstrap.ts +2 -2
- package/src/session-backend/local-session-backend.ts +11 -11
- package/src/session-backend/remote-session-backend.ts +3 -3
- package/src/session-backend/types.ts +2 -2
- package/src/snippets/expand-variables.ts +112 -0
- package/src/snippets/run-shell-var.ts +93 -0
- package/src/snippets/trigger-detector.ts +105 -0
- package/src/state/ai-usage-store.ts +1 -1
- package/src/state/git-tree.ts +8 -4
- package/src/state/layout-tree.ts +8 -5
- package/src/state/reducers/git-mode-state.ts +44 -14
- package/src/state/reducers/git-panel-state.ts +38 -2
- package/src/state/reducers/modal-state.ts +385 -24
- package/src/state/reducers/session-state.ts +60 -1
- package/src/state/reducers/tab-state.ts +151 -85
- package/src/state/selectors.ts +6 -4
- package/src/state/session-catalog.ts +20 -5
- package/src/state/session-persistence.ts +8 -4
- package/src/state/session-worktrees.ts +227 -0
- package/src/state/snippet-catalog.ts +65 -4
- package/src/state/store.ts +1 -0
- package/src/state/toast-store.ts +95 -0
- package/src/state/types.ts +108 -16
- package/src/state/validation.ts +47 -3
- package/src/terminal-manager/manager-client.ts +15 -13
- package/src/ui/components/git/diff-renderer/fold-strip.tsx +2 -1
- package/src/ui/components/git/diff-renderer/pierre-diff.tsx +1 -1
- package/src/ui/components/git/diff-renderer/prepare-diff.ts +2 -2
- package/src/ui/components/git/diff-renderer/split-view.tsx +6 -4
- package/src/ui/components/git/diff-renderer/stacked-view.tsx +6 -4
- package/src/ui/components/git/diff-renderer/use-diff-prefetch.ts +19 -6
- package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +2 -2
- package/src/ui/components/git/git-panel.tsx +17 -10
- package/src/ui/components/git/git-view.tsx +79 -19
- package/src/ui/components/git/image-diff/image-diff-view.tsx +2 -2
- package/src/ui/components/git/image-diff/terminal-image-pane.tsx +5 -4
- package/src/ui/components/git/pane/git-pane-context-menu.ts +2 -2
- package/src/ui/components/git/pane/git-pane-widget.tsx +6 -4
- package/src/ui/components/layout/session-bar.tsx +3 -3
- package/src/ui/components/layout/sidebar/sidebar.tsx +162 -36
- package/src/ui/components/layout/sidebar/tab-item.tsx +17 -1
- package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +2 -2
- package/src/ui/components/layout/sidebar/use-sidebar-branch.ts +1 -1
- package/src/ui/components/layout/split-layout.tsx +1 -1
- package/src/ui/components/layout/terminal-pane.tsx +43 -40
- package/src/ui/components/modals/app/ai-usage-modal.tsx +8 -5
- package/src/ui/components/modals/app/help-modal.tsx +4 -2
- package/src/ui/components/modals/git/git-commit-modal.tsx +2 -2
- package/src/ui/components/modals/sessions/create-session-modal.tsx +5 -1
- package/src/ui/components/modals/sessions/session-picker-modal.tsx +5 -4
- package/src/ui/components/modals/shared/form.tsx +4 -5
- package/src/ui/components/modals/shared/modal-shell.tsx +3 -3
- package/src/ui/components/modals/shared/picker.tsx +28 -5
- package/src/ui/components/modals/snippets/snippet-editor-modal.tsx +10 -6
- package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +25 -6
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +173 -5
- package/src/ui/components/modals/worktree/worktree-move-modal.tsx +94 -0
- package/src/ui/components/overlays/ai-usage/ai-usage-indicator.tsx +3 -3
- package/src/ui/components/overlays/toast/toast-item.tsx +88 -0
- package/src/ui/components/overlays/toast/toast-position.ts +45 -0
- package/src/ui/components/overlays/toast/toast-stack.tsx +30 -0
- package/src/ui/components/overlays/toast/toast-viewport.tsx +24 -0
- package/src/ui/components/overlays/toast/use-toast-animation.ts +64 -0
- package/src/ui/components/primitives/input-field.tsx +1 -1
- package/src/ui/components/primitives/list-item.tsx +3 -3
- package/src/ui/filter-themes.ts +1 -1
- package/src/ui/root.tsx +65 -14
- package/src/ui/session-ordering.ts +2 -2
- package/src/ui/status-bar-model.ts +14 -7
- package/src/ui/terminal-graphics/capabilities.ts +1 -1
- package/src/ui/terminal-graphics/format-fallback.ts +7 -9
- package/src/ui/terminal-graphics/kitty.ts +2 -7
- package/src/update/version-check.ts +1 -1
|
@@ -2,25 +2,31 @@ import type { SessionBackend } from '../session-backend/types'
|
|
|
2
2
|
import type { AppAction, AppState, SnippetRecord, TabSession } from '../state/types'
|
|
3
3
|
|
|
4
4
|
import { createPrefixedId } from '../platform/id'
|
|
5
|
-
import { saveSnippetCatalog } from '../state/snippet-catalog'
|
|
5
|
+
import { isConfigSnippetId, saveSnippetCatalog } from '../state/snippet-catalog'
|
|
6
6
|
import { writePasteToTab } from './pty-write'
|
|
7
7
|
|
|
8
8
|
function createSnippetId(): string {
|
|
9
9
|
return createPrefixedId('snip')
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export
|
|
12
|
+
export interface SnippetEditorValue {
|
|
13
|
+
name: string
|
|
14
|
+
trigger: string
|
|
15
|
+
content: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getSnippetEditorValue(state: AppState): SnippetEditorValue | null {
|
|
13
19
|
if (state.modal.type !== 'snippet-editor') {
|
|
14
20
|
return null
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
const { modal } = state
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
24
|
+
const editValue = (modal.editBuffer ?? '').trim()
|
|
25
|
+
const name = modal.activeField === 'name' ? editValue : modal.nameBuffer.trim()
|
|
26
|
+
const trigger = modal.activeField === 'trigger' ? editValue : modal.triggerBuffer.trim()
|
|
27
|
+
const content = modal.activeField === 'content' ? editValue : modal.contentBuffer.trim()
|
|
22
28
|
|
|
23
|
-
return { content, name }
|
|
29
|
+
return { content, name, trigger }
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
export function saveSnippetEditorState(state: AppState): SnippetRecord[] | null {
|
|
@@ -35,21 +41,29 @@ export function saveSnippetEditorState(state: AppState): SnippetRecord[] | null
|
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
const snippetId = state.modal.sessionTargetId
|
|
38
|
-
|
|
44
|
+
// Config-pinned snippets are sticky and read-only in the UI.
|
|
45
|
+
if (snippetId != null && snippetId !== '' && isConfigSnippetId(snippetId)) {
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const trigger = editorValue.trigger.length > 0 ? editorValue.trigger : undefined
|
|
50
|
+
|
|
51
|
+
if (snippetId != null && snippetId !== '') {
|
|
39
52
|
return state.snippets.map((snippet) =>
|
|
40
53
|
snippet.id === snippetId
|
|
41
|
-
? { ...snippet, content: editorValue.content, name: editorValue.name }
|
|
54
|
+
? { ...snippet, content: editorValue.content, name: editorValue.name, trigger }
|
|
42
55
|
: snippet
|
|
43
56
|
)
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
return [
|
|
47
60
|
...state.snippets,
|
|
48
|
-
{ content: editorValue.content, id: createSnippetId(), name: editorValue.name },
|
|
61
|
+
{ content: editorValue.content, id: createSnippetId(), name: editorValue.name, trigger },
|
|
49
62
|
]
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
export function deleteSnippetState(snippets: SnippetRecord[], snippetId: string): SnippetRecord[] {
|
|
66
|
+
if (isConfigSnippetId(snippetId)) return snippets
|
|
53
67
|
return snippets.filter((snippet) => snippet.id !== snippetId)
|
|
54
68
|
}
|
|
55
69
|
|
|
@@ -60,7 +74,7 @@ export function pasteSnippetToTab(
|
|
|
60
74
|
snippet: SnippetRecord | undefined,
|
|
61
75
|
dispatch?: (action: AppAction) => void
|
|
62
76
|
): void {
|
|
63
|
-
if (!snippet ||
|
|
77
|
+
if (!snippet || activeTabId == null || activeTabId === '' || !activeTab) {
|
|
64
78
|
return
|
|
65
79
|
}
|
|
66
80
|
|
|
@@ -52,7 +52,7 @@ export function useAutoCommitDriver({
|
|
|
52
52
|
depsRef.current = deps
|
|
53
53
|
|
|
54
54
|
useEffect(() => {
|
|
55
|
-
const handler: (args: Parameters<typeof onManualTrigger>[1]) => Promise<void> = (args) =>
|
|
55
|
+
const handler: (args: Parameters<typeof onManualTrigger>[1]) => Promise<void> = async (args) =>
|
|
56
56
|
onManualTrigger(depsRef.current, args)
|
|
57
57
|
setActiveAutoCommitDriver(handler)
|
|
58
58
|
return () => clearActiveAutoCommitDriverIfMatches(handler)
|
|
@@ -74,7 +74,7 @@ export function useAutoCommitDriver({
|
|
|
74
74
|
(before === 'working' || before === 'waiting-input') && tab.activity === 'idle'
|
|
75
75
|
if (becameIdle) {
|
|
76
76
|
const sessionId = state.currentSessionId
|
|
77
|
-
if (!sessionId) continue
|
|
77
|
+
if (!(sessionId != null && sessionId !== '')) continue
|
|
78
78
|
const session = state.sessions.find((s) => s.id === sessionId)
|
|
79
79
|
const git = gitPayloadFromState(state)
|
|
80
80
|
void onActivityTransition(depsRef.current, {
|
|
@@ -95,7 +95,7 @@ export function useAutoCommitDriver({
|
|
|
95
95
|
useEffect(() => {
|
|
96
96
|
if (!configRef.current.enabled) return
|
|
97
97
|
const sessionId = state.currentSessionId
|
|
98
|
-
if (!sessionId) return
|
|
98
|
+
if (!(sessionId != null && sessionId !== '')) return
|
|
99
99
|
const payload = gitPayloadFromState(state)
|
|
100
100
|
if (!payload) return
|
|
101
101
|
const cacheKey = JSON.stringify(payload)
|
|
@@ -109,7 +109,10 @@ export function useAutoCommitDriver({
|
|
|
109
109
|
// fast mode) and where the user edits via an external editor.
|
|
110
110
|
if (gitStabilizeTimerRef.current) clearTimeout(gitStabilizeTimerRef.current)
|
|
111
111
|
const activeTabId = state.activeTabId
|
|
112
|
-
const activeTab =
|
|
112
|
+
const activeTab =
|
|
113
|
+
activeTabId != null && activeTabId !== ''
|
|
114
|
+
? state.tabs.find((tab) => tab.id === activeTabId)
|
|
115
|
+
: undefined
|
|
113
116
|
if (!activeTab) return
|
|
114
117
|
const session = state.sessions.find((s) => s.id === sessionId)
|
|
115
118
|
gitStabilizeTimerRef.current = setTimeout(() => {
|
|
@@ -41,7 +41,7 @@ export function useBackendRuntime({
|
|
|
41
41
|
const { clearAllTimers, clearIdleTimer, clearStartupGrace, startStartupGrace } = timeouts
|
|
42
42
|
|
|
43
43
|
useEffect(() => {
|
|
44
|
-
if (!currentSessionId) {
|
|
44
|
+
if (!(currentSessionId != null && currentSessionId !== '')) {
|
|
45
45
|
attachRequestIdRef.current += 1
|
|
46
46
|
return
|
|
47
47
|
}
|
|
@@ -57,12 +57,12 @@ export function useBackendRuntime({
|
|
|
57
57
|
}, [backend, currentSessionId, currentSessionWorkspaceSnapshot, dispatch, layoutRef])
|
|
58
58
|
|
|
59
59
|
useEffect(() => {
|
|
60
|
-
if (!currentSessionId) {
|
|
60
|
+
if (!(currentSessionId != null && currentSessionId !== '')) {
|
|
61
61
|
return
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
backend.setActiveTab(activeTabId)
|
|
65
|
-
if (activeTabId && activeTabScrollIntentRef.current) {
|
|
65
|
+
if (activeTabId != null && activeTabId !== '' && activeTabScrollIntentRef.current) {
|
|
66
66
|
backend.reapplyScrollIntent(activeTabId, activeTabScrollIntentRef.current)
|
|
67
67
|
}
|
|
68
68
|
}, [activeTabId, activeTabScrollIntentRef, backend, currentSessionId])
|
|
@@ -42,11 +42,13 @@ export function useDirectorySearch(
|
|
|
42
42
|
return
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
const timer = setTimeout(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
const timer = setTimeout(() => {
|
|
46
|
+
void (async () => {
|
|
47
|
+
const results = await searchProjectDirectories(directoryQuery)
|
|
48
|
+
if (isCurrent) {
|
|
49
|
+
dispatch({ results, type: 'set-directory-results' })
|
|
50
|
+
}
|
|
51
|
+
})()
|
|
50
52
|
}, debounceMs)
|
|
51
53
|
|
|
52
54
|
return () => {
|
|
@@ -84,7 +84,7 @@ function getTargetTerminalTabId(
|
|
|
84
84
|
activeTabId: string | null,
|
|
85
85
|
isEnabled: boolean
|
|
86
86
|
): string | null {
|
|
87
|
-
if (focusMode !== 'terminal-input' ||
|
|
87
|
+
if (focusMode !== 'terminal-input' || activeTabId == null || activeTabId === '' || !isEnabled) {
|
|
88
88
|
return null
|
|
89
89
|
}
|
|
90
90
|
|
|
@@ -248,12 +248,12 @@ export function useMouseHandlers({
|
|
|
248
248
|
state.activeTabId,
|
|
249
249
|
activeMouseForwardingEnabled
|
|
250
250
|
)
|
|
251
|
-
if (!targetTabId) {
|
|
251
|
+
if (!(targetTabId != null && targetTabId !== '')) {
|
|
252
252
|
return
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
const sequence = getForwardedMouseSequence(event, origin)
|
|
256
|
-
if (!sequence) {
|
|
256
|
+
if (!(sequence != null && sequence !== '')) {
|
|
257
257
|
return
|
|
258
258
|
}
|
|
259
259
|
|
|
@@ -262,7 +262,12 @@ export function useMouseHandlers({
|
|
|
262
262
|
|
|
263
263
|
const handleTerminalScrollEvent = (event: OtuiMouseEvent) => {
|
|
264
264
|
const targetTabId = getTargetTerminalTabId(state.focusMode, state.activeTabId, true)
|
|
265
|
-
if (
|
|
265
|
+
if (
|
|
266
|
+
targetTabId == null ||
|
|
267
|
+
targetTabId === '' ||
|
|
268
|
+
activeMouseForwardingEnabled ||
|
|
269
|
+
!activeLocalScrollbackEnabled
|
|
270
|
+
) {
|
|
266
271
|
return
|
|
267
272
|
}
|
|
268
273
|
|
|
@@ -389,7 +394,7 @@ export function useMouseHandlers({
|
|
|
389
394
|
tabId?: string
|
|
390
395
|
) => {
|
|
391
396
|
const targetTabId = tabId ?? state.activeTabId
|
|
392
|
-
if (
|
|
397
|
+
if (targetTabId == null || targetTabId === '' || !event.target) {
|
|
393
398
|
return
|
|
394
399
|
}
|
|
395
400
|
|
|
@@ -437,7 +442,7 @@ export function useMouseHandlers({
|
|
|
437
442
|
}
|
|
438
443
|
|
|
439
444
|
logInputDebug('click.done', {
|
|
440
|
-
hasSelection: !!renderer.hasSelection,
|
|
445
|
+
hasSelection: !!(renderer.hasSelection === true),
|
|
441
446
|
mode: selection.mode,
|
|
442
447
|
targetSelectable: isPositionedNode(event.target) ? !!event.target.selectable : false,
|
|
443
448
|
})
|
|
@@ -44,7 +44,7 @@ export function usePaneSizeReport(
|
|
|
44
44
|
|
|
45
45
|
const measure = useCallback(() => {
|
|
46
46
|
const box = boxRef.current
|
|
47
|
-
if (!box ||
|
|
47
|
+
if (!box || tabId == null || tabId === '' || !enabled || !onMeasure) {
|
|
48
48
|
return
|
|
49
49
|
}
|
|
50
50
|
const cols = Math.round(box.width)
|
|
@@ -1,15 +1,27 @@
|
|
|
1
|
-
import { useRenderer } from '@opentui/react'
|
|
1
|
+
import type { useRenderer } from '@opentui/react'
|
|
2
|
+
|
|
2
3
|
import { type MutableRefObject, useEffect, useRef } from 'react'
|
|
3
4
|
|
|
4
5
|
import type { KeyChord } from '../input/keymap/key-chord'
|
|
5
6
|
import type { SessionBackend } from '../session-backend/types'
|
|
6
|
-
import type { AppAction, FocusMode, TabSession } from '../state/types'
|
|
7
|
+
import type { AppAction, FocusMode, SnippetRecord, TabSession } from '../state/types'
|
|
7
8
|
|
|
8
9
|
import { INPUT_DEBUG_LOG_PATH, logInputDebug } from '../debug/input-log'
|
|
9
10
|
import { createRawInputHandler } from '../input/raw-input-handler'
|
|
10
|
-
import { copyToSystemClipboard } from '../platform/clipboard'
|
|
11
|
+
import { copyToSystemClipboard, readFromSystemClipboard } from '../platform/clipboard'
|
|
12
|
+
import {
|
|
13
|
+
expandSnippet,
|
|
14
|
+
expandSnippetSync,
|
|
15
|
+
requiresAsyncExpansion,
|
|
16
|
+
} from '../snippets/expand-variables'
|
|
17
|
+
import { runShellVar } from '../snippets/run-shell-var'
|
|
18
|
+
import {
|
|
19
|
+
createTriggerDetector,
|
|
20
|
+
type TriggerDetector,
|
|
21
|
+
type TriggerMatch,
|
|
22
|
+
} from '../snippets/trigger-detector'
|
|
11
23
|
import { shouldSuppressSelectionCopy } from './multi-click-clipboard-guard'
|
|
12
|
-
import { writePasteToTab, writeToTab } from './pty-write'
|
|
24
|
+
import { writeMacroExpansionToTab, writePasteToTab, writeToTab } from './pty-write'
|
|
13
25
|
import { type OtuiSelection, resolveSelectionClipboardText } from './selection-clipboard'
|
|
14
26
|
import { applyViewportObservation, type ViewportObservation } from './selection-scroll'
|
|
15
27
|
|
|
@@ -28,6 +40,9 @@ interface UseRendererBindingsOptions {
|
|
|
28
40
|
focusModeRef: MutableRefObject<FocusMode>
|
|
29
41
|
activeTabIdRef: MutableRefObject<string | null>
|
|
30
42
|
activeTabRef: MutableRefObject<TabSession | undefined>
|
|
43
|
+
snippetsRef: MutableRefObject<readonly SnippetRecord[]>
|
|
44
|
+
branchRef: MutableRefObject<string | null>
|
|
45
|
+
triggerCharRef: MutableRefObject<string>
|
|
31
46
|
handleTerminalShortcut: (chord: KeyChord) => boolean
|
|
32
47
|
}
|
|
33
48
|
|
|
@@ -41,13 +56,27 @@ export function useRendererBindings({
|
|
|
41
56
|
activeTabRef,
|
|
42
57
|
activeTabViewportY,
|
|
43
58
|
backend,
|
|
59
|
+
branchRef,
|
|
44
60
|
dispatch,
|
|
45
61
|
focusMode,
|
|
46
62
|
focusModeRef,
|
|
47
63
|
handleTerminalShortcut,
|
|
48
64
|
renderer,
|
|
65
|
+
snippetsRef,
|
|
66
|
+
triggerCharRef,
|
|
49
67
|
}: UseRendererBindingsOptions): void {
|
|
50
68
|
const lastViewportRef = useRef<ViewportObservation | null>(null)
|
|
69
|
+
const triggerDetectorsRef = useRef<Map<string, TriggerDetector>>(new Map())
|
|
70
|
+
const pendingMacroUndoRef = useRef<Map<string, { fullLength: number; suffixLength: number }>>(
|
|
71
|
+
new Map()
|
|
72
|
+
)
|
|
73
|
+
/**
|
|
74
|
+
* Tabs currently waiting on an async expansion (shell vars or {{clipboard}}).
|
|
75
|
+
* While a tab is in this set, new trigger detections on that tab are
|
|
76
|
+
* suppressed — without this, a second trigger typed during the await would
|
|
77
|
+
* race with the in-flight expansion and corrupt PTY state + undo bookkeeping.
|
|
78
|
+
*/
|
|
79
|
+
const inFlightAsyncExpansionRef = useRef<Set<string>>(new Set())
|
|
51
80
|
|
|
52
81
|
useEffect(() => {
|
|
53
82
|
renderer.useMouse = true
|
|
@@ -55,12 +84,115 @@ export function useRendererBindings({
|
|
|
55
84
|
renderer.console.hide()
|
|
56
85
|
renderer.console.show = () => {}
|
|
57
86
|
|
|
87
|
+
const getOrCreateDetector = (tabId: string): TriggerDetector => {
|
|
88
|
+
let detector = triggerDetectorsRef.current.get(tabId)
|
|
89
|
+
if (!detector) {
|
|
90
|
+
detector = createTriggerDetector({
|
|
91
|
+
getSnippets: () => snippetsRef.current,
|
|
92
|
+
getTriggerChar: () => triggerCharRef.current,
|
|
93
|
+
})
|
|
94
|
+
triggerDetectorsRef.current.set(tabId, detector)
|
|
95
|
+
}
|
|
96
|
+
return detector
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const expandMacroForTab = (tabId: string, match: TriggerMatch): void => {
|
|
100
|
+
const tab = activeTabRef.current
|
|
101
|
+
const snippet = match.snippet
|
|
102
|
+
const branch = branchRef.current
|
|
103
|
+
const cwd = process.cwd()
|
|
104
|
+
const now = new Date()
|
|
105
|
+
|
|
106
|
+
const registerUndo = (text: string, cursorOffset: number) => {
|
|
107
|
+
// Undo window: only meaningful for short inline expansions where
|
|
108
|
+
// cursor positions stay predictable in raw mode.
|
|
109
|
+
if (!text.includes('\n')) {
|
|
110
|
+
pendingMacroUndoRef.current.set(tabId, {
|
|
111
|
+
fullLength: text.length,
|
|
112
|
+
suffixLength: text.length - cursorOffset,
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!requiresAsyncExpansion(snippet)) {
|
|
118
|
+
const { cursorOffset, text } = expandSnippetSync(snippet.content, {
|
|
119
|
+
branch,
|
|
120
|
+
customVars: new Map(),
|
|
121
|
+
cwd,
|
|
122
|
+
now,
|
|
123
|
+
})
|
|
124
|
+
writeMacroExpansionToTab(
|
|
125
|
+
backend,
|
|
126
|
+
tabId,
|
|
127
|
+
tab,
|
|
128
|
+
match.triggerText.length,
|
|
129
|
+
text,
|
|
130
|
+
cursorOffset,
|
|
131
|
+
dispatch
|
|
132
|
+
)
|
|
133
|
+
registerUndo(text, cursorOffset)
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Eager erase: drop the typed trigger from the PTY immediately so the
|
|
138
|
+
// user doesn't stare at `:prfull ` while shell vars resolve.
|
|
139
|
+
backend.write(tabId, '\x7f'.repeat(match.triggerText.length))
|
|
140
|
+
inFlightAsyncExpansionRef.current.add(tabId)
|
|
141
|
+
|
|
142
|
+
void (async () => {
|
|
143
|
+
try {
|
|
144
|
+
const varEntries = Object.entries(snippet.vars ?? {})
|
|
145
|
+
const resolved = await Promise.all(
|
|
146
|
+
varEntries.map(async ([name, v]) => [name, await runShellVar(name, v)] as const)
|
|
147
|
+
)
|
|
148
|
+
const customVars = new Map(resolved)
|
|
149
|
+
const { cursorOffset, text } = await expandSnippet(snippet.content, {
|
|
150
|
+
branch,
|
|
151
|
+
clipboard: readFromSystemClipboard,
|
|
152
|
+
customVars,
|
|
153
|
+
cwd,
|
|
154
|
+
now,
|
|
155
|
+
})
|
|
156
|
+
// Trigger already erased above; eraseCount = 0 here.
|
|
157
|
+
writeMacroExpansionToTab(backend, tabId, tab, 0, text, cursorOffset, dispatch)
|
|
158
|
+
registerUndo(text, cursorOffset)
|
|
159
|
+
} finally {
|
|
160
|
+
inFlightAsyncExpansionRef.current.delete(tabId)
|
|
161
|
+
}
|
|
162
|
+
})()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const tryConsumeMacroUndo = (tabId: string, sequence: string): boolean => {
|
|
166
|
+
const entry = pendingMacroUndoRef.current.get(tabId)
|
|
167
|
+
if (!entry) return false
|
|
168
|
+
pendingMacroUndoRef.current.delete(tabId)
|
|
169
|
+
const isBackspace = sequence === '\x7f' || sequence === '\b'
|
|
170
|
+
if (!isBackspace) return false
|
|
171
|
+
const rightArrows = '\x1b[C'.repeat(entry.suffixLength)
|
|
172
|
+
const dels = '\x7f'.repeat(entry.fullLength)
|
|
173
|
+
backend.write(tabId, `${rightArrows}${dels}`)
|
|
174
|
+
return true
|
|
175
|
+
}
|
|
176
|
+
|
|
58
177
|
const handler = createRawInputHandler({
|
|
178
|
+
expandMacro: expandMacroForTab,
|
|
179
|
+
feedTrigger: (tabId, char) => {
|
|
180
|
+
// Drop new detections while an async expansion is in flight for this
|
|
181
|
+
// tab — otherwise the second match races with the awaited write.
|
|
182
|
+
if (inFlightAsyncExpansionRef.current.has(tabId)) {
|
|
183
|
+
triggerDetectorsRef.current.get(tabId)?.reset()
|
|
184
|
+
return null
|
|
185
|
+
}
|
|
186
|
+
return getOrCreateDetector(tabId).feed(char)
|
|
187
|
+
},
|
|
59
188
|
getActiveTabId: () => activeTabIdRef.current,
|
|
60
189
|
getBracketedPasteModeEnabled: () =>
|
|
61
190
|
activeTabRef.current?.terminalModes.bracketedPasteMode ?? false,
|
|
62
191
|
getFocusMode: () => focusModeRef.current,
|
|
192
|
+
getIsAlternateBuffer: () => activeTabRef.current?.terminalModes.isAlternateBuffer ?? false,
|
|
63
193
|
handleTerminalShortcut,
|
|
194
|
+
resetTrigger: (tabId) => triggerDetectorsRef.current.get(tabId)?.reset(),
|
|
195
|
+
tryConsumeMacroUndo,
|
|
64
196
|
writeToPty: (tabId, data, options) =>
|
|
65
197
|
writeToTab(backend, tabId, activeTabRef.current, data, dispatch, options),
|
|
66
198
|
})
|
|
@@ -71,7 +203,7 @@ export function useRendererBindings({
|
|
|
71
203
|
defaultPrevented: event.defaultPrevented ?? false,
|
|
72
204
|
})
|
|
73
205
|
|
|
74
|
-
if (event.defaultPrevented) {
|
|
206
|
+
if (event.defaultPrevented === true) {
|
|
75
207
|
return
|
|
76
208
|
}
|
|
77
209
|
|
|
@@ -89,12 +221,12 @@ export function useRendererBindings({
|
|
|
89
221
|
})
|
|
90
222
|
|
|
91
223
|
if (currentFocusMode === 'command-edit') {
|
|
92
|
-
const sanitized = payload.
|
|
224
|
+
const sanitized = payload.replaceAll(/\r\n?/g, '\n')
|
|
93
225
|
dispatch({ char: sanitized, type: 'update-command-edit' })
|
|
94
226
|
return
|
|
95
227
|
}
|
|
96
228
|
|
|
97
|
-
if (currentFocusMode !== 'terminal-input' ||
|
|
229
|
+
if (currentFocusMode !== 'terminal-input' || tabId == null || tabId === '' || !tab) {
|
|
98
230
|
return
|
|
99
231
|
}
|
|
100
232
|
|
|
@@ -115,7 +247,7 @@ export function useRendererBindings({
|
|
|
115
247
|
textLength: selectedText.length,
|
|
116
248
|
})
|
|
117
249
|
|
|
118
|
-
if (selection.isDragging || selectedText.length === 0) {
|
|
250
|
+
if (selection.isDragging === true || selectedText.length === 0) {
|
|
119
251
|
return
|
|
120
252
|
}
|
|
121
253
|
|
|
@@ -146,10 +278,13 @@ export function useRendererBindings({
|
|
|
146
278
|
activeTabIdRef,
|
|
147
279
|
activeTabRef,
|
|
148
280
|
backend,
|
|
281
|
+
branchRef,
|
|
149
282
|
dispatch,
|
|
150
283
|
focusModeRef,
|
|
151
284
|
handleTerminalShortcut,
|
|
152
285
|
renderer,
|
|
286
|
+
snippetsRef,
|
|
287
|
+
triggerCharRef,
|
|
153
288
|
])
|
|
154
289
|
|
|
155
290
|
useEffect(() => {
|
package/src/app.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } fr
|
|
|
10
10
|
import type { KeyChord } from './input/keymap/key-chord'
|
|
11
11
|
import type { TrieBinding } from './input/keymap/trie'
|
|
12
12
|
import type { KeyResult, ModeContext, ModeId } from './input/modes/types'
|
|
13
|
+
import type { TerminalContentOrigin } from './input/raw-input-handler'
|
|
13
14
|
import type { SessionBackend } from './session-backend/types'
|
|
14
15
|
|
|
15
16
|
import { executeSideEffect, type SideEffectContext } from './app-runtime/side-effects'
|
|
@@ -25,7 +26,6 @@ import { setActiveKeymap } from './input/keymap/keymap-ref'
|
|
|
25
26
|
import { deriveModeId } from './input/modes/bridge'
|
|
26
27
|
import { registerAllModes } from './input/modes/handlers'
|
|
27
28
|
import { getHandler, transitionTo } from './input/modes/registry'
|
|
28
|
-
import { type TerminalContentOrigin } from './input/raw-input-handler'
|
|
29
29
|
import { highlightSnapshot, warmClaudeSyntaxOverlay } from './integrations/claude-syntax-overlay'
|
|
30
30
|
import { ensureClaudeSettingsThemePref, syncClaudeTheme } from './integrations/claude-theme-sync'
|
|
31
31
|
import { getProfileConfigDir, getProfileName } from './profile-paths'
|
|
@@ -34,7 +34,8 @@ import { aiUsageStore } from './state/ai-usage-store'
|
|
|
34
34
|
import { appStore, useAppStore } from './state/app-store'
|
|
35
35
|
import { setActiveDispatch, setActiveSideEffectRunner } from './state/dispatch-ref'
|
|
36
36
|
import { findMostRecentSession, loadSessionCatalog } from './state/session-catalog'
|
|
37
|
-
import {
|
|
37
|
+
import { getSessionProjectPath } from './state/session-worktrees'
|
|
38
|
+
import { loadSnippetCatalog, mergeConfigSnippets } from './state/snippet-catalog'
|
|
38
39
|
import { createInitialState } from './state/store'
|
|
39
40
|
import { KeymapContext } from './ui/keymap-context'
|
|
40
41
|
import { RootView } from './ui/root'
|
|
@@ -81,7 +82,10 @@ export function App({
|
|
|
81
82
|
const dimensions = useTerminalDimensions()
|
|
82
83
|
const [themeId, setThemeId] = useState<ThemeId>(() => {
|
|
83
84
|
const config = loadConfig()
|
|
84
|
-
const persisted =
|
|
85
|
+
const persisted =
|
|
86
|
+
config.themeId != null && config.themeId !== '' && isKnownThemeId(config.themeId)
|
|
87
|
+
? config.themeId
|
|
88
|
+
: undefined
|
|
85
89
|
const initial: ThemeId = persisted ?? resolvedConfig.theme?.initialId ?? 'aimux'
|
|
86
90
|
applyTheme(initial)
|
|
87
91
|
if (resolvedConfig.theme?.initialMode) setMode(resolvedConfig.theme.initialMode)
|
|
@@ -137,10 +141,11 @@ export function App({
|
|
|
137
141
|
}
|
|
138
142
|
|
|
139
143
|
const sessionCatalog = loadSessionCatalog()
|
|
144
|
+
const mergedSnippets = mergeConfigSnippets(loadSnippetCatalog(), resolvedConfig.snippets)
|
|
140
145
|
const initial = createInitialState(
|
|
141
146
|
json.customCommands,
|
|
142
147
|
sessionCatalog,
|
|
143
|
-
|
|
148
|
+
mergedSnippets,
|
|
144
149
|
sessionCatalog.length === 0,
|
|
145
150
|
{
|
|
146
151
|
gitPane: gitPaneOverrides,
|
|
@@ -179,7 +184,7 @@ export function App({
|
|
|
179
184
|
}, [dispatch])
|
|
180
185
|
|
|
181
186
|
useEffect(() => {
|
|
182
|
-
if (!resolvedConfig.theme?.beta?.harmonizeClaudeTheme) return
|
|
187
|
+
if (!(resolvedConfig.theme?.beta?.harmonizeClaudeTheme === true)) return
|
|
183
188
|
ensureClaudeSettingsThemePref()
|
|
184
189
|
syncClaudeTheme(getCurrentTheme(), getCurrentMode())
|
|
185
190
|
return subscribeThemeChanges((resolved, mode) => {
|
|
@@ -189,7 +194,7 @@ export function App({
|
|
|
189
194
|
|
|
190
195
|
useEffect(() => {
|
|
191
196
|
const aiUsage = resolvedConfig.statusBar?.aiUsage
|
|
192
|
-
if (!aiUsage?.enabled) {
|
|
197
|
+
if (!(aiUsage?.enabled === true)) {
|
|
193
198
|
aiUsageStore.getState().setEnabled(false)
|
|
194
199
|
return
|
|
195
200
|
}
|
|
@@ -214,7 +219,7 @@ export function App({
|
|
|
214
219
|
getCurrentPackageVersion(),
|
|
215
220
|
fetchLatestNpmVersion('@brimveyn/aimux'),
|
|
216
221
|
])
|
|
217
|
-
if (cancelled || !latest) return
|
|
222
|
+
if (cancelled || !(latest != null && latest !== '')) return
|
|
218
223
|
if (!isNewerVersion(latest, current)) return
|
|
219
224
|
if (loadConfig().skippedUpdateVersion === latest) return
|
|
220
225
|
dispatch({
|
|
@@ -259,6 +264,13 @@ export function App({
|
|
|
259
264
|
const stateRef = useRef(state)
|
|
260
265
|
stateRef.current = state
|
|
261
266
|
|
|
267
|
+
const snippetsRef = useRef(state.snippets)
|
|
268
|
+
snippetsRef.current = state.snippets
|
|
269
|
+
const branchRef = useRef(state.gitPanel.branch)
|
|
270
|
+
branchRef.current = state.gitPanel.branch
|
|
271
|
+
const triggerCharRef = useRef(resolvedConfig.snippetTriggerChar)
|
|
272
|
+
triggerCharRef.current = resolvedConfig.snippetTriggerChar
|
|
273
|
+
|
|
262
274
|
const contentOriginRef = useRef<TerminalContentOrigin>({ cols: 0, rows: 0, x: 0, y: 0 })
|
|
263
275
|
const currentSessionWorkspaceSnapshot = currentSession?.workspaceSnapshot
|
|
264
276
|
|
|
@@ -370,11 +382,14 @@ export function App({
|
|
|
370
382
|
activeTabRef,
|
|
371
383
|
activeTabViewportY: activeTab?.viewport?.viewportY ?? null,
|
|
372
384
|
backend,
|
|
385
|
+
branchRef,
|
|
373
386
|
dispatch,
|
|
374
387
|
focusMode: state.focusMode,
|
|
375
388
|
focusModeRef,
|
|
376
389
|
handleTerminalShortcut,
|
|
377
390
|
renderer,
|
|
391
|
+
snippetsRef,
|
|
392
|
+
triggerCharRef,
|
|
378
393
|
})
|
|
379
394
|
|
|
380
395
|
const sideEffectCtx: SideEffectContext = {
|
|
@@ -384,9 +399,10 @@ export function App({
|
|
|
384
399
|
clearStartupGrace,
|
|
385
400
|
dispatch,
|
|
386
401
|
getCurrentSessionProjectPath: () => {
|
|
387
|
-
if (!state.currentSessionId) return
|
|
388
|
-
return state.sessions.find((s) => s.id === state.currentSessionId)
|
|
402
|
+
if (!(state.currentSessionId != null && state.currentSessionId !== '')) return
|
|
403
|
+
return getSessionProjectPath(state.sessions.find((s) => s.id === state.currentSessionId))
|
|
389
404
|
},
|
|
405
|
+
getState: () => stateRef.current,
|
|
390
406
|
renderer,
|
|
391
407
|
setThemeId,
|
|
392
408
|
startStartupGrace,
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { AssistantId } from '../state/types'
|
|
2
|
-
|
|
3
1
|
export interface HeadlessInvocation {
|
|
4
2
|
executable: string
|
|
5
3
|
args: string[]
|
|
@@ -9,25 +7,25 @@ export type SupportedProvider = 'claude' | 'codex' | 'opencode'
|
|
|
9
7
|
|
|
10
8
|
const SUPPORTED: ReadonlySet<string> = new Set<SupportedProvider>(['claude', 'codex', 'opencode'])
|
|
11
9
|
|
|
12
|
-
export function isSupportedProvider(id:
|
|
10
|
+
export function isSupportedProvider(id: string): id is SupportedProvider {
|
|
13
11
|
return SUPPORTED.has(id)
|
|
14
12
|
}
|
|
15
13
|
|
|
16
14
|
export function buildHeadlessInvocation(
|
|
17
|
-
provider:
|
|
15
|
+
provider: string,
|
|
18
16
|
prompt: string,
|
|
19
17
|
model: string | undefined
|
|
20
18
|
): HeadlessInvocation | null {
|
|
21
19
|
switch (provider) {
|
|
22
20
|
case 'claude': {
|
|
23
21
|
const args = ['-p', '--output-format', 'text']
|
|
24
|
-
if (model) args.push('--model', model)
|
|
22
|
+
if (model != null && model !== '') args.push('--model', model)
|
|
25
23
|
args.push(prompt)
|
|
26
24
|
return { args, executable: 'claude' }
|
|
27
25
|
}
|
|
28
26
|
case 'codex': {
|
|
29
27
|
const args = ['exec']
|
|
30
|
-
if (model) args.push('--model', model)
|
|
28
|
+
if (model != null && model !== '') args.push('--model', model)
|
|
31
29
|
args.push(prompt)
|
|
32
30
|
return { args, executable: 'codex' }
|
|
33
31
|
}
|
package/src/daemon/daemon.ts
CHANGED
|
@@ -84,7 +84,7 @@ function sendOk(socket: Socket, id: string): void {
|
|
|
84
84
|
|
|
85
85
|
function requireSession(socket: Socket, attachedSessions: Map<Socket, string>): string {
|
|
86
86
|
const sessionId = attachedSessions.get(socket)
|
|
87
|
-
if (!sessionId) {
|
|
87
|
+
if (!(sessionId != null && sessionId !== '')) {
|
|
88
88
|
throw new Error('No session attached')
|
|
89
89
|
}
|
|
90
90
|
return sessionId
|
|
@@ -100,7 +100,7 @@ function requireNegotiatedVersion(socket: Socket, versions: Map<Socket, number>)
|
|
|
100
100
|
|
|
101
101
|
async function canConnectToSocket(socketPath: string): Promise<boolean> {
|
|
102
102
|
const securityIssue = getSocketSecurityIssue(socketPath)
|
|
103
|
-
if (securityIssue) {
|
|
103
|
+
if (securityIssue != null && securityIssue !== '') {
|
|
104
104
|
logDebug('daemon.socketUnhealthy', { issue: securityIssue, socketPath })
|
|
105
105
|
return false
|
|
106
106
|
}
|
|
@@ -527,7 +527,7 @@ export async function runDaemon(): Promise<void> {
|
|
|
527
527
|
case 'disposeAll': {
|
|
528
528
|
const sessionId = attachedSessions.get(socket)
|
|
529
529
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
530
|
-
if (sessionId) {
|
|
530
|
+
if (sessionId != null && sessionId !== '') {
|
|
531
531
|
for (const [tabId, entry] of tabRegistry) {
|
|
532
532
|
if (entry.sessionId === sessionId) tabRegistry.delete(tabId)
|
|
533
533
|
}
|
|
@@ -8,7 +8,7 @@ export function getRuntimeProfile(): string {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
function getRuntimeBaseDir(): string {
|
|
11
|
-
if (process.env.XDG_RUNTIME_DIR) {
|
|
11
|
+
if (process.env.XDG_RUNTIME_DIR != null && process.env.XDG_RUNTIME_DIR !== '') {
|
|
12
12
|
return join(process.env.XDG_RUNTIME_DIR, `aimux-${getRuntimeProfile()}`)
|
|
13
13
|
}
|
|
14
14
|
|