@brimveyn/aimux 1.12.6 → 1.13.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/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 +29 -13
- package/src/app-runtime/backend-runtime-events.ts +1 -1
- 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 +563 -85
- package/src/app-runtime/snippet-actions.ts +3 -3
- 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 +6 -5
- package/src/app.tsx +12 -7
- package/src/auto-commit/headless-commands.ts +4 -6
- package/src/daemon/daemon.ts +21 -11
- 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 +19 -3
- 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 +11 -1
- package/src/input/raw-input-handler.ts +1 -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 +4 -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 +2 -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 +60 -45
- package/src/session-backend/types.ts +2 -2
- package/src/snippets/expand-variables.ts +7 -7
- package/src/snippets/run-shell-var.ts +1 -1
- package/src/snippets/trigger-detector.ts +1 -1
- 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 +355 -21
- 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/store.ts +1 -0
- package/src/state/toast-store.ts +95 -0
- package/src/state/types.ts +92 -13
- package/src/state/validation.ts +25 -2
- package/src/terminal-manager/manager-client.ts +15 -13
- package/src/ui/components/git/diff-renderer/fold-strip.tsx +32 -24
- 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 +39 -11
- package/src/ui/components/git/diff-renderer/stacked-view.tsx +29 -7
- package/src/ui/components/git/diff-renderer/use-diff-prefetch.ts +28 -11
- package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +10 -8
- package/src/ui/components/git/git-panel.tsx +129 -83
- package/src/ui/components/git/git-view.tsx +96 -28
- package/src/ui/components/git/image-diff/image-diff-view.tsx +2 -2
- package/src/ui/components/git/image-diff/terminal-image-pane.tsx +14 -13
- 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 +135 -117
- package/src/ui/components/layout/sidebar/sidebar.tsx +244 -82
- package/src/ui/components/layout/sidebar/tab-item.tsx +68 -41
- 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 +27 -20
- package/src/ui/components/layout/terminal-pane.tsx +163 -120
- package/src/ui/components/modals/app/ai-usage-modal.tsx +8 -5
- package/src/ui/components/modals/app/help-modal.tsx +29 -20
- package/src/ui/components/modals/git/git-commit-modal.tsx +17 -10
- package/src/ui/components/modals/sessions/create-session-modal.tsx +18 -10
- package/src/ui/components/modals/sessions/session-picker-modal.tsx +36 -28
- package/src/ui/components/modals/shared/form.tsx +6 -6
- package/src/ui/components/modals/shared/modal-shell.tsx +3 -3
- package/src/ui/components/modals/shared/picker.tsx +71 -32
- package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +50 -29
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +209 -25
- package/src/ui/components/modals/themes/theme-picker-modal.tsx +24 -15
- package/src/ui/components/modals/worktree/worktree-move-modal.tsx +111 -0
- package/src/ui/components/overlays/ai-usage/ai-usage-indicator.tsx +14 -9
- package/src/ui/components/overlays/context-menu/context-menu-box.tsx +20 -11
- package/src/ui/components/overlays/context-menu/context-menu-overlay.tsx +69 -34
- 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 +23 -7
- package/src/ui/filter-themes.ts +1 -1
- package/src/ui/root.tsx +121 -38
- 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
|
@@ -42,13 +42,13 @@ export function saveSnippetEditorState(state: AppState): SnippetRecord[] | null
|
|
|
42
42
|
|
|
43
43
|
const snippetId = state.modal.sessionTargetId
|
|
44
44
|
// Config-pinned snippets are sticky and read-only in the UI.
|
|
45
|
-
if (snippetId && isConfigSnippetId(snippetId)) {
|
|
45
|
+
if (snippetId != null && snippetId !== '' && isConfigSnippetId(snippetId)) {
|
|
46
46
|
return null
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
const trigger = editorValue.trigger.length > 0 ? editorValue.trigger : undefined
|
|
50
50
|
|
|
51
|
-
if (snippetId) {
|
|
51
|
+
if (snippetId != null && snippetId !== '') {
|
|
52
52
|
return state.snippets.map((snippet) =>
|
|
53
53
|
snippet.id === snippetId
|
|
54
54
|
? { ...snippet, content: editorValue.content, name: editorValue.name, trigger }
|
|
@@ -74,7 +74,7 @@ export function pasteSnippetToTab(
|
|
|
74
74
|
snippet: SnippetRecord | undefined,
|
|
75
75
|
dispatch?: (action: AppAction) => void
|
|
76
76
|
): void {
|
|
77
|
-
if (!snippet ||
|
|
77
|
+
if (!snippet || activeTabId == null || activeTabId === '' || !activeTab) {
|
|
78
78
|
return
|
|
79
79
|
}
|
|
80
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,4 +1,5 @@
|
|
|
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'
|
|
@@ -202,7 +203,7 @@ export function useRendererBindings({
|
|
|
202
203
|
defaultPrevented: event.defaultPrevented ?? false,
|
|
203
204
|
})
|
|
204
205
|
|
|
205
|
-
if (event.defaultPrevented) {
|
|
206
|
+
if (event.defaultPrevented === true) {
|
|
206
207
|
return
|
|
207
208
|
}
|
|
208
209
|
|
|
@@ -220,12 +221,12 @@ export function useRendererBindings({
|
|
|
220
221
|
})
|
|
221
222
|
|
|
222
223
|
if (currentFocusMode === 'command-edit') {
|
|
223
|
-
const sanitized = payload.
|
|
224
|
+
const sanitized = payload.replaceAll(/\r\n?/g, '\n')
|
|
224
225
|
dispatch({ char: sanitized, type: 'update-command-edit' })
|
|
225
226
|
return
|
|
226
227
|
}
|
|
227
228
|
|
|
228
|
-
if (currentFocusMode !== 'terminal-input' ||
|
|
229
|
+
if (currentFocusMode !== 'terminal-input' || tabId == null || tabId === '' || !tab) {
|
|
229
230
|
return
|
|
230
231
|
}
|
|
231
232
|
|
|
@@ -246,7 +247,7 @@ export function useRendererBindings({
|
|
|
246
247
|
textLength: selectedText.length,
|
|
247
248
|
})
|
|
248
249
|
|
|
249
|
-
if (selection.isDragging || selectedText.length === 0) {
|
|
250
|
+
if (selection.isDragging === true || selectedText.length === 0) {
|
|
250
251
|
return
|
|
251
252
|
}
|
|
252
253
|
|
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,6 +34,7 @@ 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 { getSessionProjectPath } from './state/session-worktrees'
|
|
37
38
|
import { loadSnippetCatalog, mergeConfigSnippets } from './state/snippet-catalog'
|
|
38
39
|
import { createInitialState } from './state/store'
|
|
39
40
|
import { KeymapContext } from './ui/keymap-context'
|
|
@@ -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)
|
|
@@ -180,7 +184,7 @@ export function App({
|
|
|
180
184
|
}, [dispatch])
|
|
181
185
|
|
|
182
186
|
useEffect(() => {
|
|
183
|
-
if (!resolvedConfig.theme?.beta?.harmonizeClaudeTheme) return
|
|
187
|
+
if (!(resolvedConfig.theme?.beta?.harmonizeClaudeTheme === true)) return
|
|
184
188
|
ensureClaudeSettingsThemePref()
|
|
185
189
|
syncClaudeTheme(getCurrentTheme(), getCurrentMode())
|
|
186
190
|
return subscribeThemeChanges((resolved, mode) => {
|
|
@@ -190,7 +194,7 @@ export function App({
|
|
|
190
194
|
|
|
191
195
|
useEffect(() => {
|
|
192
196
|
const aiUsage = resolvedConfig.statusBar?.aiUsage
|
|
193
|
-
if (!aiUsage?.enabled) {
|
|
197
|
+
if (!(aiUsage?.enabled === true)) {
|
|
194
198
|
aiUsageStore.getState().setEnabled(false)
|
|
195
199
|
return
|
|
196
200
|
}
|
|
@@ -215,7 +219,7 @@ export function App({
|
|
|
215
219
|
getCurrentPackageVersion(),
|
|
216
220
|
fetchLatestNpmVersion('@brimveyn/aimux'),
|
|
217
221
|
])
|
|
218
|
-
if (cancelled || !latest) return
|
|
222
|
+
if (cancelled || !(latest != null && latest !== '')) return
|
|
219
223
|
if (!isNewerVersion(latest, current)) return
|
|
220
224
|
if (loadConfig().skippedUpdateVersion === latest) return
|
|
221
225
|
dispatch({
|
|
@@ -395,9 +399,10 @@ export function App({
|
|
|
395
399
|
clearStartupGrace,
|
|
396
400
|
dispatch,
|
|
397
401
|
getCurrentSessionProjectPath: () => {
|
|
398
|
-
if (!state.currentSessionId) return
|
|
399
|
-
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))
|
|
400
404
|
},
|
|
405
|
+
getState: () => stateRef.current,
|
|
401
406
|
renderer,
|
|
402
407
|
setThemeId,
|
|
403
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
|
}
|
|
@@ -293,12 +293,16 @@ export async function runDaemon(): Promise<void> {
|
|
|
293
293
|
* previous broadcast state, matching pre-fix behaviour).
|
|
294
294
|
*/
|
|
295
295
|
const updateTmBroadcastForClientCount = (count: number): void => {
|
|
296
|
-
void
|
|
297
|
-
|
|
298
|
-
count
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
296
|
+
void (async () => {
|
|
297
|
+
try {
|
|
298
|
+
await manager.setBroadcastEnabled(count > 0)
|
|
299
|
+
} catch (error) {
|
|
300
|
+
logDebug('daemon.setBroadcastEnabled.error', {
|
|
301
|
+
count,
|
|
302
|
+
error: error instanceof Error ? error.message : String(error),
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
})()
|
|
302
306
|
}
|
|
303
307
|
|
|
304
308
|
// Initial state: no clients yet, ask the TM to suspend broadcast.
|
|
@@ -317,7 +321,13 @@ export async function runDaemon(): Promise<void> {
|
|
|
317
321
|
let processing: Promise<void> = Promise.resolve()
|
|
318
322
|
|
|
319
323
|
socket.on('data', (chunk) => {
|
|
320
|
-
|
|
324
|
+
const previous = processing
|
|
325
|
+
processing = (async () => {
|
|
326
|
+
try {
|
|
327
|
+
await previous
|
|
328
|
+
} catch {
|
|
329
|
+
// A prior chunk's failure shouldn't block later chunks on this socket.
|
|
330
|
+
}
|
|
321
331
|
try {
|
|
322
332
|
for (const message of decoder.push(chunk)) {
|
|
323
333
|
try {
|
|
@@ -527,7 +537,7 @@ export async function runDaemon(): Promise<void> {
|
|
|
527
537
|
case 'disposeAll': {
|
|
528
538
|
const sessionId = attachedSessions.get(socket)
|
|
529
539
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
530
|
-
if (sessionId) {
|
|
540
|
+
if (sessionId != null && sessionId !== '') {
|
|
531
541
|
for (const [tabId, entry] of tabRegistry) {
|
|
532
542
|
if (entry.sessionId === sessionId) tabRegistry.delete(tabId)
|
|
533
543
|
}
|
|
@@ -556,7 +566,7 @@ export async function runDaemon(): Promise<void> {
|
|
|
556
566
|
decoder.reset()
|
|
557
567
|
send(socket, { id: crypto.randomUUID(), payload: { message }, type: 'error' })
|
|
558
568
|
}
|
|
559
|
-
})
|
|
569
|
+
})()
|
|
560
570
|
})
|
|
561
571
|
|
|
562
572
|
socket.on('close', () => {
|
|
@@ -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
|
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
type WorkspaceSnapshotV1,
|
|
18
18
|
} from '../state/types'
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
interface SessionRegistryEvents {
|
|
21
21
|
render: [tabId: string, viewport: TerminalSnapshot, terminalModes: TerminalModeState]
|
|
22
22
|
exit: [tabId: string, exitCode: number]
|
|
23
23
|
error: [tabId: string, message: string]
|
|
@@ -81,7 +81,9 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
|
|
|
81
81
|
this.tabs.set(tab.id, tab)
|
|
82
82
|
}
|
|
83
83
|
this.activeTabId =
|
|
84
|
-
snapshot.activeTabId
|
|
84
|
+
snapshot.activeTabId != null &&
|
|
85
|
+
snapshot.activeTabId !== '' &&
|
|
86
|
+
restoredTabs.some((tab) => tab.id === snapshot.activeTabId)
|
|
85
87
|
? snapshot.activeTabId
|
|
86
88
|
: (restoredTabs[0]?.id ?? null)
|
|
87
89
|
} else if (this.tabs.size > 0 && snapshot) {
|
|
@@ -90,9 +92,14 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
|
|
|
90
92
|
if (existing) {
|
|
91
93
|
existing.title = persisted.title
|
|
92
94
|
existing.scrollIntent = persisted.scrollIntent ?? DEFAULT_SCROLL_INTENT
|
|
95
|
+
existing.worktreeId = persisted.worktreeId
|
|
93
96
|
}
|
|
94
97
|
}
|
|
95
|
-
if (
|
|
98
|
+
if (
|
|
99
|
+
snapshot.activeTabId != null &&
|
|
100
|
+
snapshot.activeTabId !== '' &&
|
|
101
|
+
this.tabs.has(snapshot.activeTabId)
|
|
102
|
+
) {
|
|
96
103
|
this.activeTabId = snapshot.activeTabId
|
|
97
104
|
}
|
|
98
105
|
}
|
|
@@ -64,7 +64,7 @@ export function processFile(
|
|
|
64
64
|
const lines = hunk.split(SPLIT_WITH_NEWLINES)
|
|
65
65
|
const firstLine = lines.shift()
|
|
66
66
|
if (firstLine == null) {
|
|
67
|
-
if (throwOnError) throw Error('parsePatchContent: invalid hunk')
|
|
67
|
+
if (throwOnError) throw new Error('parsePatchContent: invalid hunk')
|
|
68
68
|
else console.error('parsePatchContent: invalid hunk', hunk)
|
|
69
69
|
continue
|
|
70
70
|
}
|
|
@@ -73,7 +73,7 @@ export function processFile(
|
|
|
73
73
|
let deletionLines = 0
|
|
74
74
|
if (fileHeaderMatch == null || currentFile == null) {
|
|
75
75
|
if (currentFile != null) {
|
|
76
|
-
if (throwOnError) throw Error('parsePatchContent: Invalid hunk')
|
|
76
|
+
if (throwOnError) throw new Error('parsePatchContent: Invalid hunk')
|
|
77
77
|
else console.error('parsePatchContent: Invalid hunk', hunk)
|
|
78
78
|
continue
|
|
79
79
|
}
|
|
@@ -139,8 +139,9 @@ export function processFile(
|
|
|
139
139
|
currentFile.mode = line.replace('deleted file mode', '').trim()
|
|
140
140
|
}
|
|
141
141
|
if (line.startsWith('similarity index')) {
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
currentFile.type = line.startsWith('similarity index 100%')
|
|
143
|
+
? 'rename-pure'
|
|
144
|
+
: 'rename-changed'
|
|
144
145
|
}
|
|
145
146
|
if (line.startsWith('index ')) {
|
|
146
147
|
const [, prevObjectId, newObjectId, mode] = line.trim().match(INDEX_LINE_METADATA) ?? []
|
|
@@ -162,24 +163,24 @@ export function processFile(
|
|
|
162
163
|
let lastLineType: 'addition' | 'deletion' | 'context' | undefined
|
|
163
164
|
while (
|
|
164
165
|
lines.length > 0 &&
|
|
165
|
-
(lines
|
|
166
|
-
lines
|
|
167
|
-
lines
|
|
168
|
-
lines
|
|
166
|
+
(lines.at(-1) === '\n' ||
|
|
167
|
+
lines.at(-1) === '\r' ||
|
|
168
|
+
lines.at(-1) === '\r\n' ||
|
|
169
|
+
lines.at(-1) === '')
|
|
169
170
|
) {
|
|
170
171
|
lines.pop()
|
|
171
172
|
}
|
|
172
|
-
const additionStart = parseInt(fileHeaderMatch[3] ?? '')
|
|
173
|
-
const deletionStart = parseInt(fileHeaderMatch[1] ?? '')
|
|
173
|
+
const additionStart = parseInt(fileHeaderMatch[3] ?? '', 10)
|
|
174
|
+
const deletionStart = parseInt(fileHeaderMatch[1] ?? '', 10)
|
|
174
175
|
deletionLineIndex = isPartial ? deletionLineIndex : deletionStart - 1
|
|
175
176
|
additionLineIndex = isPartial ? additionLineIndex : additionStart - 1
|
|
176
177
|
const hunkData = {
|
|
177
|
-
additionCount: parseInt(fileHeaderMatch[4] ?? '1'),
|
|
178
|
+
additionCount: parseInt(fileHeaderMatch[4] ?? '1', 10),
|
|
178
179
|
additionLineIndex,
|
|
179
180
|
additionLines,
|
|
180
181
|
additionStart,
|
|
181
182
|
collapsedBefore: 0,
|
|
182
|
-
deletionCount: parseInt(fileHeaderMatch[2] ?? '1'),
|
|
183
|
+
deletionCount: parseInt(fileHeaderMatch[2] ?? '1', 10),
|
|
183
184
|
deletionLineIndex,
|
|
184
185
|
deletionLines,
|
|
185
186
|
deletionStart,
|
|
@@ -199,7 +200,7 @@ export function processFile(
|
|
|
199
200
|
isNaN(hunkData.additionStart) ||
|
|
200
201
|
isNaN(hunkData.deletionStart)
|
|
201
202
|
) {
|
|
202
|
-
if (throwOnError) throw Error('parsePatchContent: invalid hunk metadata')
|
|
203
|
+
if (throwOnError) throw new Error('parsePatchContent: invalid hunk metadata')
|
|
203
204
|
else console.error('parsePatchContent: invalid hunk metadata', hunkData)
|
|
204
205
|
continue
|
|
205
206
|
}
|
|
@@ -285,7 +286,7 @@ export function processFile(
|
|
|
285
286
|
currentFile.unifiedLineCount += hunkData.collapsedBefore + hunkData.unifiedLineCount
|
|
286
287
|
}
|
|
287
288
|
if (currentFile == null) return
|
|
288
|
-
const lastHunk = currentFile.hunks
|
|
289
|
+
const lastHunk = currentFile.hunks.at(-1)
|
|
289
290
|
if (
|
|
290
291
|
lastHunk != null &&
|
|
291
292
|
!isPartial &&
|
|
@@ -300,8 +301,7 @@ export function processFile(
|
|
|
300
301
|
}
|
|
301
302
|
if (!isGitDiff) {
|
|
302
303
|
if (currentFile.prevName != null && currentFile.name !== currentFile.prevName) {
|
|
303
|
-
|
|
304
|
-
else currentFile.type = 'rename-pure'
|
|
304
|
+
currentFile.type = currentFile.hunks.length > 0 ? 'rename-changed' : 'rename-pure'
|
|
305
305
|
} else if (newFile != null && newFile.contents === '') currentFile.type = 'deleted'
|
|
306
306
|
else if (oldFile != null && oldFile.contents === '') currentFile.type = 'new'
|
|
307
307
|
}
|
|
@@ -323,12 +323,12 @@ export function processPatch(
|
|
|
323
323
|
for (const fileOrPatchMetadata of rawFiles) {
|
|
324
324
|
if (isGitDiff && !GIT_DIFF_FILE_BREAK_REGEX.test(fileOrPatchMetadata)) {
|
|
325
325
|
if (patchMetadata == null) patchMetadata = fileOrPatchMetadata
|
|
326
|
-
else if (throwOnError) throw Error('parsePatchContent: unknown file blob')
|
|
326
|
+
else if (throwOnError) throw new Error('parsePatchContent: unknown file blob')
|
|
327
327
|
else console.error('parsePatchContent: unknown file blob:', fileOrPatchMetadata)
|
|
328
328
|
continue
|
|
329
329
|
} else if (!isGitDiff && !UNIFIED_DIFF_FILE_BREAK_REGEX.test(fileOrPatchMetadata)) {
|
|
330
330
|
if (patchMetadata == null) patchMetadata = fileOrPatchMetadata
|
|
331
|
-
else if (throwOnError) throw Error('parsePatchContent: unknown file blob')
|
|
331
|
+
else if (throwOnError) throw new Error('parsePatchContent: unknown file blob')
|
|
332
332
|
else console.error('parsePatchContent: unknown file blob:', fileOrPatchMetadata)
|
|
333
333
|
continue
|
|
334
334
|
}
|
package/src/git/command-queue.ts
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
let tail: Promise<unknown> = Promise.resolve()
|
|
2
2
|
|
|
3
|
-
export function enqueueGitOp<T>(op: () => Promise<T>): Promise<T> {
|
|
4
|
-
const
|
|
5
|
-
|
|
3
|
+
export async function enqueueGitOp<T>(op: () => Promise<T>): Promise<T> {
|
|
4
|
+
const previous = tail
|
|
5
|
+
const run = async (): Promise<T> => {
|
|
6
|
+
try {
|
|
7
|
+
await previous
|
|
8
|
+
} catch {
|
|
9
|
+
// Previous op already surfaced its own failure; keep the queue moving.
|
|
10
|
+
}
|
|
11
|
+
return op()
|
|
12
|
+
}
|
|
13
|
+
const next = run()
|
|
14
|
+
// Advance the queue tail without letting this op's rejection break the chain.
|
|
15
|
+
tail = (async () => {
|
|
16
|
+
try {
|
|
17
|
+
await next
|
|
18
|
+
} catch {
|
|
19
|
+
// Swallow so the next enqueued op still runs.
|
|
20
|
+
}
|
|
21
|
+
})()
|
|
6
22
|
return next
|
|
7
23
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { $ } from 'bun'
|
|
2
|
+
|
|
3
|
+
import type { BranchDivergence } from '../state/types'
|
|
4
|
+
|
|
5
|
+
// Parses `git rev-list --left-right --count <base>...<branch>`, which prints two
|
|
6
|
+
// whitespace-separated counts: "<left> <right>". With the `<base>...<branch>`
|
|
7
|
+
// range the left count is commits in base but not branch (how far the branch is
|
|
8
|
+
// *behind* its base) and the right count is commits in branch but not base (how
|
|
9
|
+
// far it is *ahead*).
|
|
10
|
+
export function parseDivergenceCount(output: string): BranchDivergence | undefined {
|
|
11
|
+
const match = /(\d+)\s+(\d+)/.exec(output.trim())
|
|
12
|
+
if (match == null) return undefined
|
|
13
|
+
return {
|
|
14
|
+
ahead: Number.parseInt(match[2] ?? '', 10),
|
|
15
|
+
behind: Number.parseInt(match[1] ?? '', 10),
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Commits a worktree branch is ahead/behind the ref it forked from. Returns
|
|
20
|
+
// undefined when the refs can't be compared (missing ref, not a repo, …) so
|
|
21
|
+
// callers can simply render nothing.
|
|
22
|
+
export async function getBranchDivergence(
|
|
23
|
+
repoPath: string,
|
|
24
|
+
baseRef: string,
|
|
25
|
+
branch: string
|
|
26
|
+
): Promise<BranchDivergence | undefined> {
|
|
27
|
+
const range = `${baseRef}...${branch}`
|
|
28
|
+
const result = await $`git -C ${repoPath} rev-list --left-right --count ${range}`
|
|
29
|
+
.quiet()
|
|
30
|
+
.nothrow()
|
|
31
|
+
if (result.exitCode !== 0) return undefined
|
|
32
|
+
return parseDivergenceCount(result.text())
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Fork point: the most recent commit shared by baseRef and ref. Diffing the
|
|
36
|
+
// working tree against this shows everything the worktree changed since it
|
|
37
|
+
// branched off (commits + staged + unstaged) — i.e. what a squash-move lands.
|
|
38
|
+
export async function getMergeBase(
|
|
39
|
+
repoPath: string,
|
|
40
|
+
baseRef: string,
|
|
41
|
+
ref = 'HEAD'
|
|
42
|
+
): Promise<string | undefined> {
|
|
43
|
+
const result = await $`git -C ${repoPath} merge-base ${baseRef} ${ref}`.quiet().nothrow()
|
|
44
|
+
if (result.exitCode !== 0) return undefined
|
|
45
|
+
return result.text().trim() || undefined
|
|
46
|
+
}
|