@brimveyn/aimux 1.12.6 → 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/selection-scroll.ts +2 -2
- package/src/app-runtime/session-actions.ts +16 -4
- package/src/app-runtime/side-effects.ts +538 -83
- 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 +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 +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 +3 -3
- 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 +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-picker-modal.tsx +11 -3
- 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 +57 -3
- 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
|
@@ -17,11 +17,15 @@ export function getLineText(line: TerminalLine): string {
|
|
|
17
17
|
*/
|
|
18
18
|
export function extractStreamText(
|
|
19
19
|
lines: TerminalLine[],
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
startRowArg: number,
|
|
21
|
+
startColArg: number,
|
|
22
|
+
endRowArg: number,
|
|
23
|
+
endColArg: number
|
|
24
24
|
): string {
|
|
25
|
+
let startRow = startRowArg
|
|
26
|
+
let startCol = startColArg
|
|
27
|
+
let endRow = endRowArg
|
|
28
|
+
let endCol = endColArg
|
|
25
29
|
if (startRow > endRow || (startRow === endRow && startCol > endCol)) {
|
|
26
30
|
;[startRow, endRow] = [endRow, startRow]
|
|
27
31
|
;[startCol, endCol] = [endCol, startCol]
|
|
@@ -60,7 +64,7 @@ export function getWordAtColumn(
|
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
const ch = lineText[column]
|
|
63
|
-
if (
|
|
67
|
+
if (ch == null || ch === '' || !/\S/.test(ch)) {
|
|
64
68
|
return { endCol: column, startCol: column, text: '' }
|
|
65
69
|
}
|
|
66
70
|
|
|
@@ -103,10 +103,10 @@ export async function warmClaudeSyntaxOverlay(): Promise<void> {
|
|
|
103
103
|
warmedHighlighter = h
|
|
104
104
|
const themeName = await ensureActiveShikiTheme(h)
|
|
105
105
|
activeThemeName = themeName
|
|
106
|
-
await Promise.all(PRELOAD_LANGS.map((lang) => ensureShikiLang(h, lang)))
|
|
106
|
+
await Promise.all(PRELOAD_LANGS.map(async (lang) => ensureShikiLang(h, lang)))
|
|
107
107
|
ready = true
|
|
108
|
-
} catch (
|
|
109
|
-
logDebug('claude-syntax-overlay:warm-failed', { err: String(
|
|
108
|
+
} catch (error) {
|
|
109
|
+
logDebug('claude-syntax-overlay:warm-failed', { err: String(error) })
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
@@ -150,7 +150,7 @@ function lineText(line: TerminalLine): string {
|
|
|
150
150
|
function dominantBg(line: TerminalLine): string | undefined {
|
|
151
151
|
const counts = new Map<string, number>()
|
|
152
152
|
for (const span of line.spans) {
|
|
153
|
-
if (!span.bg) continue
|
|
153
|
+
if (!(span.bg != null && span.bg !== '')) continue
|
|
154
154
|
if (span.text.trim().length === 0) continue
|
|
155
155
|
counts.set(span.bg, (counts.get(span.bg) ?? 0) + span.text.length)
|
|
156
156
|
}
|
|
@@ -166,7 +166,7 @@ function dominantBg(line: TerminalLine): string | undefined {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
function tokenize(code: string, lang: string): ShikiToken[][] {
|
|
169
|
-
if (!ready ||
|
|
169
|
+
if (!ready || activeThemeName == null || activeThemeName === '' || !warmedHighlighter) return []
|
|
170
170
|
try {
|
|
171
171
|
/* eslint-disable typescript-eslint/no-explicit-any */
|
|
172
172
|
return warmedHighlighter.codeToTokens(code, {
|
|
@@ -210,7 +210,7 @@ function buildSpans(
|
|
|
210
210
|
if (!tok.content) continue
|
|
211
211
|
const fs = tok.fontStyle ?? 0
|
|
212
212
|
const tokColor = tok.color?.toLowerCase()
|
|
213
|
-
const fg = tokColor && accents.has(tokColor) ? tok.color : fallbackFg
|
|
213
|
+
const fg = tokColor != null && tokColor !== '' && accents.has(tokColor) ? tok.color : fallbackFg
|
|
214
214
|
spans.push({
|
|
215
215
|
bg,
|
|
216
216
|
bold: (fs & 2) !== 0 || undefined,
|
|
@@ -381,7 +381,7 @@ function processLines(
|
|
|
381
381
|
accents: Set<string>,
|
|
382
382
|
targetWidth: number
|
|
383
383
|
): TerminalLine[] {
|
|
384
|
-
const out = lines
|
|
384
|
+
const out = [...lines]
|
|
385
385
|
let block: BlockContext | null = null
|
|
386
386
|
|
|
387
387
|
for (let i = 0; i < out.length; i += 1) {
|
|
@@ -393,7 +393,7 @@ function processLines(
|
|
|
393
393
|
const headerMatch = text.match(TOOL_HEADER_RE)
|
|
394
394
|
if (headerMatch) {
|
|
395
395
|
const lang = inferLangFromPath(headerMatch[1] ?? '')
|
|
396
|
-
if (lang) tabLang.set(tabId, lang)
|
|
396
|
+
if (lang != null && lang !== '') tabLang.set(tabId, lang)
|
|
397
397
|
}
|
|
398
398
|
|
|
399
399
|
const prefixMatch = text.match(PREFIX_RE)
|
|
@@ -37,13 +37,13 @@ function writeAtomic(target: string, contents: string): void {
|
|
|
37
37
|
writeFileSync(tmp, contents, 'utf8')
|
|
38
38
|
try {
|
|
39
39
|
renameSync(tmp, target)
|
|
40
|
-
} catch (
|
|
40
|
+
} catch (error) {
|
|
41
41
|
try {
|
|
42
42
|
unlinkSync(tmp)
|
|
43
43
|
} catch {
|
|
44
44
|
/* ignore */
|
|
45
45
|
}
|
|
46
|
-
throw
|
|
46
|
+
throw error
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -60,8 +60,8 @@ export function syncClaudeTheme(resolved: ResolvedTuiTheme, mode: ThemeMode): vo
|
|
|
60
60
|
let theme: ClaudeThemeFile
|
|
61
61
|
try {
|
|
62
62
|
theme = resolveClaudeTheme(resolved, mode)
|
|
63
|
-
} catch (
|
|
64
|
-
logSyncWarn('resolve-failed', { err: String(
|
|
63
|
+
} catch (error) {
|
|
64
|
+
logSyncWarn('resolve-failed', { err: String(error) })
|
|
65
65
|
return
|
|
66
66
|
}
|
|
67
67
|
|
|
@@ -69,8 +69,8 @@ export function syncClaudeTheme(resolved: ResolvedTuiTheme, mode: ThemeMode): vo
|
|
|
69
69
|
try {
|
|
70
70
|
mkdirSync(join(claudeDir(), 'themes'), { recursive: true })
|
|
71
71
|
writeAtomic(target, `${JSON.stringify(theme, null, 2)}\n`)
|
|
72
|
-
} catch (
|
|
73
|
-
logSyncWarn('write-failed', { err: String(
|
|
72
|
+
} catch (error) {
|
|
73
|
+
logSyncWarn('write-failed', { err: String(error), path: target })
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
@@ -86,8 +86,8 @@ export function ensureClaudeSettingsThemePref(): void {
|
|
|
86
86
|
let raw: string
|
|
87
87
|
try {
|
|
88
88
|
raw = readFileSync(target, 'utf8')
|
|
89
|
-
} catch (
|
|
90
|
-
logSyncWarn('settings-read-failed', { err: String(
|
|
89
|
+
} catch (error) {
|
|
90
|
+
logSyncWarn('settings-read-failed', { err: String(error), path: target })
|
|
91
91
|
return
|
|
92
92
|
}
|
|
93
93
|
if (raw.trim().length > 0) {
|
|
@@ -98,8 +98,8 @@ export function ensureClaudeSettingsThemePref(): void {
|
|
|
98
98
|
return
|
|
99
99
|
}
|
|
100
100
|
parsed = json as Record<string, unknown>
|
|
101
|
-
} catch (
|
|
102
|
-
logSyncWarn('settings-parse-failed', { err: String(
|
|
101
|
+
} catch (error) {
|
|
102
|
+
logSyncWarn('settings-parse-failed', { err: String(error), path: target })
|
|
103
103
|
return
|
|
104
104
|
}
|
|
105
105
|
}
|
|
@@ -112,7 +112,7 @@ export function ensureClaudeSettingsThemePref(): void {
|
|
|
112
112
|
try {
|
|
113
113
|
mkdirSync(claudeDir(), { recursive: true })
|
|
114
114
|
writeAtomic(target, `${JSON.stringify(parsed, null, 2)}\n`)
|
|
115
|
-
} catch (
|
|
116
|
-
logSyncWarn('settings-write-failed', { err: String(
|
|
115
|
+
} catch (error) {
|
|
116
|
+
logSyncWarn('settings-write-failed', { err: String(error), path: target })
|
|
117
117
|
}
|
|
118
118
|
}
|
package/src/ipc/protocol.ts
CHANGED
|
@@ -10,8 +10,8 @@ import type {
|
|
|
10
10
|
|
|
11
11
|
import { isWorkspaceSnapshotV1 } from '../state/validation'
|
|
12
12
|
|
|
13
|
-
export const IPC_PROTOCOL_MIN_VERSION =
|
|
14
|
-
export const IPC_PROTOCOL_VERSION =
|
|
13
|
+
export const IPC_PROTOCOL_MIN_VERSION = 8
|
|
14
|
+
export const IPC_PROTOCOL_VERSION = 8
|
|
15
15
|
|
|
16
16
|
export interface ProtocolHelloRequest {
|
|
17
17
|
minVersion: number
|
|
@@ -43,7 +43,7 @@ export interface AttachResult {
|
|
|
43
43
|
* right state immediately, without the per-event race where a separate
|
|
44
44
|
* `sessionStatus` event could arrive before the session was in state.
|
|
45
45
|
*/
|
|
46
|
-
initialSessionStatuses:
|
|
46
|
+
initialSessionStatuses: { sessionId: string; status: SessionStatus }[]
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export type ClientRequest =
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { logDebug } from '../debug/input-log'
|
|
2
|
+
import { toast } from '../state/toast-store'
|
|
2
3
|
|
|
3
4
|
export function copyToSystemClipboard(text: string): void {
|
|
4
5
|
try {
|
|
5
6
|
const proc = Bun.spawn(['pbcopy'], { stdin: 'pipe' })
|
|
6
|
-
proc.stdin.write(text)
|
|
7
|
-
proc.stdin.end()
|
|
7
|
+
void proc.stdin.write(text)
|
|
8
|
+
void proc.stdin.end()
|
|
8
9
|
} catch (error) {
|
|
9
10
|
logDebug('platform.clipboard.copyError', {
|
|
10
11
|
error: error instanceof Error ? error.message : String(error),
|
|
11
12
|
})
|
|
13
|
+
toast.error('Copy failed')
|
|
12
14
|
}
|
|
13
15
|
}
|
|
14
16
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { realpath, symlink } from 'node:fs/promises'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Symlink the source checkout's `node_modules` into a freshly created worktree
|
|
7
|
+
* so a JS project's dependencies don't need reinstalling. No-op when the source
|
|
8
|
+
* isn't a JS project, has no installed `node_modules`, or the worktree already
|
|
9
|
+
* has one. Returns true when a symlink was created.
|
|
10
|
+
*/
|
|
11
|
+
export async function linkNodeModules(sourceDir: string, worktreePath: string): Promise<boolean> {
|
|
12
|
+
if (!existsSync(join(sourceDir, 'package.json'))) return false
|
|
13
|
+
const sourceModules = join(sourceDir, 'node_modules')
|
|
14
|
+
if (!existsSync(sourceModules)) return false
|
|
15
|
+
const targetModules = join(worktreePath, 'node_modules')
|
|
16
|
+
if (existsSync(targetModules)) return false
|
|
17
|
+
// Resolve through any existing symlink so the new link points at the real
|
|
18
|
+
// dependency store rather than chaining through another worktree.
|
|
19
|
+
const resolved = await realpath(sourceModules)
|
|
20
|
+
await symlink(resolved, targetModules, 'dir')
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { lstat, mkdir, realpath } from 'node:fs/promises'
|
|
3
|
+
import { join, resolve } from 'node:path'
|
|
4
|
+
|
|
5
|
+
const DEFAULT_WORKTREE_ROOT = '/tmp/aimux-wt'
|
|
6
|
+
const MAX_SLUG_LENGTH = 24
|
|
7
|
+
|
|
8
|
+
export function getAimuxWorktreeRoot(): string {
|
|
9
|
+
const root = process.env.AIMUX_WORKTREE_ROOT
|
|
10
|
+
return root != null && root !== '' ? root : DEFAULT_WORKTREE_ROOT
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function sanitizePathSegment(input: string, maxLength = MAX_SLUG_LENGTH): string {
|
|
14
|
+
const sanitized = input
|
|
15
|
+
.trim()
|
|
16
|
+
.replaceAll(/[^A-Za-z0-9._-]+/g, '-')
|
|
17
|
+
.replaceAll(/\.\.+/g, '-')
|
|
18
|
+
.replaceAll(/^-+|-+$/g, '')
|
|
19
|
+
return (sanitized || 'worktree').slice(0, maxLength).replaceAll(/[-_.]+$/g, '') || 'worktree'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function shortHash(input: string, length = 8): string {
|
|
23
|
+
return createHash('sha1').update(input).digest('hex').slice(0, length)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function makeWorktreePath({
|
|
27
|
+
repoRoot,
|
|
28
|
+
worktreeId,
|
|
29
|
+
worktreeName,
|
|
30
|
+
}: {
|
|
31
|
+
repoRoot: string
|
|
32
|
+
worktreeName: string
|
|
33
|
+
worktreeId: string
|
|
34
|
+
}): string {
|
|
35
|
+
const repoKey = `r-${shortHash(resolve(repoRoot))}`
|
|
36
|
+
const idSuffix = shortHash(worktreeId, 5)
|
|
37
|
+
const slug = `${sanitizePathSegment(worktreeName)}-${idSuffix}`
|
|
38
|
+
return join(getAimuxWorktreeRoot(), repoKey, slug)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isInsideAimuxWorktreeRoot(path: string): boolean {
|
|
42
|
+
const normalizeTmp = (value: string) => value.replace(/^\/private\/tmp(?=\/|$)/, '/tmp')
|
|
43
|
+
const root = `${normalizeTmp(resolve(getAimuxWorktreeRoot()))}/`
|
|
44
|
+
const target = `${normalizeTmp(resolve(path))}/`
|
|
45
|
+
return target.startsWith(root)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function ensureAimuxWorktreeRoot(): Promise<string> {
|
|
49
|
+
const root = getAimuxWorktreeRoot()
|
|
50
|
+
await mkdir(root, { recursive: true })
|
|
51
|
+
const stat = await lstat(root)
|
|
52
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
53
|
+
throw new Error(`unsafe Aimux worktree root: ${root}`)
|
|
54
|
+
}
|
|
55
|
+
return root
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function assertSafeAimuxWorktreePath(path: string): Promise<void> {
|
|
59
|
+
const root = await ensureAimuxWorktreeRoot()
|
|
60
|
+
if (!isInsideAimuxWorktreeRoot(path)) {
|
|
61
|
+
throw new Error(`refusing worktree path outside Aimux temp root: ${path}`)
|
|
62
|
+
}
|
|
63
|
+
const realRoot = await realpath(root)
|
|
64
|
+
const realParent = await realpath(resolve(path, '..'))
|
|
65
|
+
if (`${realParent}/`.startsWith(`${realRoot}/`)) return
|
|
66
|
+
throw new Error(`unsafe Aimux worktree parent: ${realParent}`)
|
|
67
|
+
}
|
package/src/profile-paths.ts
CHANGED
|
@@ -4,14 +4,14 @@ export const DEFAULT_PROFILE = 'default'
|
|
|
4
4
|
|
|
5
5
|
function sanitizeProfile(profile: string): string {
|
|
6
6
|
const trimmed = profile.trim().toLowerCase()
|
|
7
|
-
const normalized = trimmed.
|
|
8
|
-
const collapsed = normalized.
|
|
7
|
+
const normalized = trimmed.replaceAll(/[^a-z0-9._-]+/g, '-')
|
|
8
|
+
const collapsed = normalized.replaceAll(/-+/g, '-').replaceAll(/^-|-$/g, '')
|
|
9
9
|
return collapsed || DEFAULT_PROFILE
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function getProfileName(): string {
|
|
13
13
|
const configured = process.env.AIMUX_PROFILE ?? process.env.AIMUX_RUNTIME_PROFILE
|
|
14
|
-
if (!configured) {
|
|
14
|
+
if (!(configured != null && configured !== '')) {
|
|
15
15
|
return DEFAULT_PROFILE
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -58,9 +58,9 @@ export interface StatusDetectionLoopHandle {
|
|
|
58
58
|
/** Last session flags, for on-attach replay. */
|
|
59
59
|
getSessionStatus: (sessionId: string) => SessionStatus | undefined
|
|
60
60
|
/** Snapshot of every known session's flags. */
|
|
61
|
-
snapshotSessions: () =>
|
|
61
|
+
snapshotSessions: () => { sessionId: string; status: SessionStatus }[]
|
|
62
62
|
/** Snapshot of every known tab's status plus its session. */
|
|
63
|
-
snapshotTabs: () =>
|
|
63
|
+
snapshotTabs: () => { tabId: string; sessionId: string; status: TabActivity }[]
|
|
64
64
|
/**
|
|
65
65
|
* Synchronously run classification for a single session. Used on client
|
|
66
66
|
* attach so the replay snapshot is populated *before* the client reads it,
|
|
@@ -20,6 +20,7 @@ const ACTIVE_CHANGE_WINDOW_MS = 600
|
|
|
20
20
|
|
|
21
21
|
/** Spinner glyphs used by claude code's status lines. */
|
|
22
22
|
const CLAUDE_SPINNER_GLYPHS = '·✱✲✳✴✵✶✷✸✹✺✻✼✽✾✿❀❁❂❃❇❈❉❊❋✢✣✤✥✦✧✨⊛⊕⊙◉◎◍⁂⁕※⍟☼★☆'
|
|
23
|
+
const CLAUDE_SPINNER_GLYPH_SET = new Set(CLAUDE_SPINNER_GLYPHS)
|
|
23
24
|
|
|
24
25
|
const SHELL_COMMAND_PATTERN =
|
|
25
26
|
/(^|\/)(bash|zsh|fish|sh|dash|ash|ksh|tcsh|csh|nu|pwsh|powershell|elvish|xonsh)(\.exe)?$/i
|
|
@@ -85,7 +86,8 @@ export class AssistantStatusDetector {
|
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
function extractTailText(viewport: TerminalSnapshot, lineCount: number): string {
|
|
88
|
-
const
|
|
89
|
+
const isScrolledToBottom = viewport.viewportY === viewport.baseY
|
|
90
|
+
const lines = isScrolledToBottom ? viewport.lines : (viewport.tailLines ?? viewport.lines)
|
|
89
91
|
// Full-screen TUIs (claude, opencode) paint in the alternate buffer and
|
|
90
92
|
// often leave the last rows blank, putting their status bar higher up.
|
|
91
93
|
// Skip trailing blank rows before taking the last `lineCount`.
|
|
@@ -152,10 +154,17 @@ function classifyClaude(haystack: string, rawTail: string): TabActivity {
|
|
|
152
154
|
return 'idle'
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
// A real spinner line looks like `✱ Thinking…`: glyph at the line start, with
|
|
158
|
+
// the ellipsis on the same line. Anchoring to the line start rejects the `·`
|
|
159
|
+
// separators Claude scatters mid-line through its header/footer (e.g.
|
|
160
|
+
// `Opus 4.7 … · Claude Max`, `… · ← for agents`), which would otherwise pair
|
|
161
|
+
// with a stray `...` placeholder and mark an idle home screen as working.
|
|
155
162
|
function hasClaudeSpinner(rawTail: string): boolean {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
163
|
+
for (const line of rawTail.split('\n')) {
|
|
164
|
+
const trimmed = line.trimStart()
|
|
165
|
+
const first = trimmed[0]
|
|
166
|
+
if (first == null || !CLAUDE_SPINNER_GLYPH_SET.has(first)) continue
|
|
167
|
+
if (trimmed.includes('…') || trimmed.includes('...')) return true
|
|
159
168
|
}
|
|
160
169
|
return false
|
|
161
170
|
}
|
|
@@ -219,8 +228,8 @@ function classifyGeneric(haystack: string, changedAt: number, now: number): TabA
|
|
|
219
228
|
}
|
|
220
229
|
|
|
221
230
|
export function isShellCommand(command: string | undefined): boolean {
|
|
222
|
-
if (!command) return false
|
|
231
|
+
if (!(command != null && command !== '')) return false
|
|
223
232
|
const first = command.trim().split(/\s+/u)[0]
|
|
224
|
-
if (!first) return false
|
|
233
|
+
if (!(first != null && first !== '')) return false
|
|
225
234
|
return SHELL_COMMAND_PATTERN.test(first)
|
|
226
235
|
}
|
|
@@ -7,7 +7,8 @@ export interface AssistantOption {
|
|
|
7
7
|
description: string
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
const DEFAULT_SHELL =
|
|
10
|
+
const DEFAULT_SHELL =
|
|
11
|
+
process.env.SHELL != null && process.env.SHELL !== '' ? process.env.SHELL : 'sh'
|
|
11
12
|
const SHELL_NAME = DEFAULT_SHELL.split('/').pop() ?? 'shell'
|
|
12
13
|
|
|
13
14
|
export const ASSISTANT_OPTIONS: AssistantOption[] = [
|
package/src/pty/pty-manager.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { ScrollIntent, TerminalModeState, TerminalSnapshot } from '../state
|
|
|
7
7
|
import { logDebug } from '../debug/input-log'
|
|
8
8
|
import { areTerminalSnapshotsEqual, snapshotTerminal } from './terminal-snapshot'
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
interface PtyManagerEvents {
|
|
11
11
|
render: [tabId: string, viewport: TerminalSnapshot, terminalModes: TerminalModeState]
|
|
12
12
|
exit: [tabId: string, exitCode: number]
|
|
13
13
|
error: [tabId: string, message: string]
|
|
@@ -47,7 +47,7 @@ let cachedCreds: ClaudeOAuthCreds | null = null
|
|
|
47
47
|
const CREDS_EXPIRY_BUFFER_MS = 60_000
|
|
48
48
|
|
|
49
49
|
function normalizePlanTier(raw: string | undefined | null): string | null {
|
|
50
|
-
if (!raw) return null
|
|
50
|
+
if (!(raw != null && raw !== '')) return null
|
|
51
51
|
const trimmed = raw.trim()
|
|
52
52
|
if (!trimmed) return null
|
|
53
53
|
const lower = trimmed.toLowerCase()
|
|
@@ -64,7 +64,7 @@ function planTierFromPayload(payload: ClaudeKeychainPayload): string | null {
|
|
|
64
64
|
if (!o) return null
|
|
65
65
|
const raw = o.rateLimitTier ?? o.rate_limit_tier ?? o.subscriptionType
|
|
66
66
|
const normalized = normalizePlanTier(raw)
|
|
67
|
-
if (normalized) return normalized
|
|
67
|
+
if (normalized != null && normalized !== '') return normalized
|
|
68
68
|
if (Array.isArray(o.scopes)) {
|
|
69
69
|
for (const scope of o.scopes) {
|
|
70
70
|
const s = scope.toLowerCase()
|
|
@@ -101,7 +101,7 @@ async function readClaudeCreds(): Promise<ClaudeOAuthCreds> {
|
|
|
101
101
|
}
|
|
102
102
|
const parsed = JSON.parse(result.stdout.trim()) as ClaudeKeychainPayload
|
|
103
103
|
const access = parsed.claudeAiOauth?.accessToken
|
|
104
|
-
if (!access) {
|
|
104
|
+
if (!(access != null && access !== '')) {
|
|
105
105
|
throw new Error('no accessToken in Claude Code keychain')
|
|
106
106
|
}
|
|
107
107
|
cachedCreds = {
|
|
@@ -123,8 +123,8 @@ function buildWindow(
|
|
|
123
123
|
const util = typeof payload.utilization === 'number' ? payload.utilization : null
|
|
124
124
|
const percent = util === null ? null : Math.max(0, Math.min(100, util))
|
|
125
125
|
const resetAt = payload.resets_at ?? null
|
|
126
|
-
const resetAtMs = resetAt ? new Date(resetAt).getTime() : null
|
|
127
|
-
if (percent === null && !resetAt) return null
|
|
126
|
+
const resetAtMs = resetAt != null && resetAt !== '' ? new Date(resetAt).getTime() : null
|
|
127
|
+
if (percent === null && !(resetAt != null && resetAt !== '')) return null
|
|
128
128
|
return {
|
|
129
129
|
kind,
|
|
130
130
|
label,
|
|
@@ -40,7 +40,7 @@ const AUTH_TIMEOUT_MS = 15_000
|
|
|
40
40
|
|
|
41
41
|
function codexHome(): string {
|
|
42
42
|
const env = process.env.CODEX_HOME?.trim()
|
|
43
|
-
if (env) return env
|
|
43
|
+
if (env != null && env !== '') return env
|
|
44
44
|
return join(homedir(), '.codex')
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -81,7 +81,7 @@ async function resolveUsageURL(): Promise<string> {
|
|
|
81
81
|
try {
|
|
82
82
|
const contents = await readFile(join(codexHome(), 'config.toml'), 'utf8')
|
|
83
83
|
const parsed = parseChatGPTBaseFromConfig(contents)
|
|
84
|
-
if (parsed) base = parsed
|
|
84
|
+
if (parsed != null && parsed !== '') base = parsed
|
|
85
85
|
} catch {
|
|
86
86
|
// no config.toml or unreadable — fall back to default
|
|
87
87
|
}
|
|
@@ -94,7 +94,7 @@ async function loadAuth(): Promise<{ accessToken: string; accountId: string | nu
|
|
|
94
94
|
const raw = await readFile(join(codexHome(), 'auth.json'), 'utf8')
|
|
95
95
|
const parsed = JSON.parse(raw) as CodexAuthFile
|
|
96
96
|
const accessToken = parsed.tokens?.access_token
|
|
97
|
-
if (!accessToken) {
|
|
97
|
+
if (!(accessToken != null && accessToken !== '')) {
|
|
98
98
|
throw new Error('no access_token in ~/.codex/auth.json — run `codex` to sign in')
|
|
99
99
|
}
|
|
100
100
|
return {
|
|
@@ -104,7 +104,7 @@ async function loadAuth(): Promise<{ accessToken: string; accountId: string | nu
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
function normalizePlanTier(raw: string | undefined | null): string | null {
|
|
107
|
-
if (!raw) return null
|
|
107
|
+
if (!(raw != null && raw !== '')) return null
|
|
108
108
|
const trimmed = raw.trim()
|
|
109
109
|
if (!trimmed) return null
|
|
110
110
|
const lower = trimmed.toLowerCase()
|
|
@@ -128,11 +128,11 @@ function buildCodexWindow(
|
|
|
128
128
|
if (!window) return null
|
|
129
129
|
const percent =
|
|
130
130
|
typeof window.used_percent === 'number' ? Math.max(0, Math.min(100, window.used_percent)) : null
|
|
131
|
-
const resetAtMs = window.reset_at ? window.reset_at * 1000 : null
|
|
132
|
-
const resetAt = resetAtMs ? new Date(resetAtMs).toISOString() : null
|
|
131
|
+
const resetAtMs = window.reset_at != null && window.reset_at !== 0 ? window.reset_at * 1000 : null
|
|
132
|
+
const resetAt = resetAtMs != null && resetAtMs !== 0 ? new Date(resetAtMs).toISOString() : null
|
|
133
133
|
const windowSeconds =
|
|
134
134
|
typeof window.limit_window_seconds === 'number' ? window.limit_window_seconds : null
|
|
135
|
-
if (percent === null && !resetAt) return null
|
|
135
|
+
if (percent === null && !(resetAt != null && resetAt !== '')) return null
|
|
136
136
|
return {
|
|
137
137
|
kind,
|
|
138
138
|
label,
|
|
@@ -186,7 +186,7 @@ export async function fetchCodexUsage(_config: AIUsageToolConfig): Promise<Usage
|
|
|
186
186
|
'Authorization': `Bearer ${accessToken}`,
|
|
187
187
|
'User-Agent': 'aimux',
|
|
188
188
|
}
|
|
189
|
-
if (accountId) headers['ChatGPT-Account-Id'] = accountId
|
|
189
|
+
if (accountId != null && accountId !== '') headers['ChatGPT-Account-Id'] = accountId
|
|
190
190
|
|
|
191
191
|
const controller = new AbortController()
|
|
192
192
|
const timer = setTimeout(() => controller.abort(), AUTH_TIMEOUT_MS)
|
|
@@ -40,12 +40,12 @@ export function loadCachedSnapshot(tool: AIUsageTool, maxAgeMs: number): CachedS
|
|
|
40
40
|
if (typeof entry.fetchedAt !== 'number') return null
|
|
41
41
|
const ageMs = Date.now() - entry.fetchedAt
|
|
42
42
|
if (ageMs > maxAgeMs) return null
|
|
43
|
-
if (
|
|
43
|
+
if (entry.snapshot.tool !== tool) return null
|
|
44
44
|
return { ageMs, snapshot: entry.snapshot }
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export function saveCachedSnapshot(snapshot: UsageSnapshot): void {
|
|
48
|
-
if (snapshot.error) return
|
|
48
|
+
if (snapshot.error != null && snapshot.error !== '') return
|
|
49
49
|
if (snapshot.percent === null) return
|
|
50
50
|
try {
|
|
51
51
|
mkdirSync(CACHE_DIR, { recursive: true })
|
|
@@ -29,7 +29,7 @@ export function computePace(opts: {
|
|
|
29
29
|
}): UsagePace | null {
|
|
30
30
|
const { percent, resetAtMs, windowSeconds } = opts
|
|
31
31
|
const now = opts.now ?? Date.now()
|
|
32
|
-
if (percent === null || resetAtMs === null ||
|
|
32
|
+
if (percent === null || resetAtMs === null || windowSeconds == null || windowSeconds <= 0) {
|
|
33
33
|
return null
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -49,11 +49,8 @@ export function computePace(opts: {
|
|
|
49
49
|
if (rate > 0) {
|
|
50
50
|
const remaining = Math.max(0, 100 - actual)
|
|
51
51
|
const candidate = remaining / rate
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
} else {
|
|
55
|
-
rightText = `Runs out in ${formatDuration(candidate)}`
|
|
56
|
-
}
|
|
52
|
+
rightText =
|
|
53
|
+
candidate >= timeUntilReset ? 'Lasts to reset' : `Runs out in ${formatDuration(candidate)}`
|
|
57
54
|
}
|
|
58
55
|
} else if (elapsed > 0 && actual === 0) {
|
|
59
56
|
rightText = 'Lasts to reset'
|
|
@@ -49,7 +49,7 @@ export function startAIUsageService(
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
if (toFetch.length > 0) {
|
|
52
|
-
const results = await Promise.allSettled(toFetch.map((t) => fetchFor(t, config)))
|
|
52
|
+
const results = await Promise.allSettled(toFetch.map(async (t) => fetchFor(t, config)))
|
|
53
53
|
if (stopped) return
|
|
54
54
|
for (let i = 0; i < results.length; i++) {
|
|
55
55
|
const result = results[i]
|
|
@@ -48,7 +48,7 @@ async function spawnDaemon(): Promise<void> {
|
|
|
48
48
|
|
|
49
49
|
async function canConnectToDaemon(socketPath: string): Promise<boolean> {
|
|
50
50
|
const securityIssue = getSocketSecurityIssue(socketPath)
|
|
51
|
-
if (securityIssue) {
|
|
51
|
+
if (securityIssue != null && securityIssue !== '') {
|
|
52
52
|
logDebug('backend.healthcheck.socketIssue', { issue: securityIssue, socketPath })
|
|
53
53
|
return false
|
|
54
54
|
}
|
|
@@ -77,7 +77,7 @@ export async function probeDaemonProtocolCompatibility(
|
|
|
77
77
|
socketPath: string
|
|
78
78
|
): Promise<DaemonHandshakeProbeResult> {
|
|
79
79
|
const securityIssue = getSocketSecurityIssue(socketPath)
|
|
80
|
-
if (securityIssue) {
|
|
80
|
+
if (securityIssue != null && securityIssue !== '') {
|
|
81
81
|
return { compatible: false, error: securityIssue }
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -114,7 +114,7 @@ export class LocalSessionBackend
|
|
|
114
114
|
rows: number
|
|
115
115
|
cwd?: string
|
|
116
116
|
}): void {
|
|
117
|
-
if (!this.currentSessionId) {
|
|
117
|
+
if (!(this.currentSessionId != null && this.currentSessionId !== '')) {
|
|
118
118
|
logDebug('backend.local.skipCreateWithoutSession', { tabId: options.tabId })
|
|
119
119
|
return
|
|
120
120
|
}
|
|
@@ -127,7 +127,7 @@ export class LocalSessionBackend
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
write(tabId: string, input: string): void {
|
|
130
|
-
if (!this.currentSessionId) {
|
|
130
|
+
if (!(this.currentSessionId != null && this.currentSessionId !== '')) {
|
|
131
131
|
logDebug('backend.local.skipWriteWithoutSession', { inputLength: input.length, tabId })
|
|
132
132
|
return
|
|
133
133
|
}
|
|
@@ -140,22 +140,22 @@ export class LocalSessionBackend
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
scrollViewport(tabId: string, deltaLines: number): void {
|
|
143
|
-
if (!this.currentSessionId) return
|
|
143
|
+
if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
|
|
144
144
|
this.sessionManager.scroll(this.currentSessionId, tabId, deltaLines)
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
scrollViewportToBottom(tabId: string): void {
|
|
148
|
-
if (!this.currentSessionId) return
|
|
148
|
+
if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
|
|
149
149
|
this.sessionManager.scrollToBottom(this.currentSessionId, tabId)
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
reapplyScrollIntent(tabId: string, intent: ScrollIntent): void {
|
|
153
|
-
if (!this.currentSessionId) return
|
|
153
|
+
if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
|
|
154
154
|
this.sessionManager.reapplyScrollIntent(this.currentSessionId, tabId, intent)
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
setActiveTab(tabId: string | null): void {
|
|
158
|
-
if (!this.currentSessionId) return
|
|
158
|
+
if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
|
|
159
159
|
logDebug('backend.local.setActiveTab', { sessionId: this.currentSessionId, tabId })
|
|
160
160
|
this.sessionManager.setActiveTab(this.currentSessionId, tabId)
|
|
161
161
|
}
|
|
@@ -166,7 +166,7 @@ export class LocalSessionBackend
|
|
|
166
166
|
intents?: Map<string, ScrollIntent>,
|
|
167
167
|
options?: { sync?: boolean }
|
|
168
168
|
): void {
|
|
169
|
-
if (!this.currentSessionId) return
|
|
169
|
+
if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
|
|
170
170
|
this.sessionManager.resize(this.currentSessionId, cols, rows, intents, options)
|
|
171
171
|
}
|
|
172
172
|
|
|
@@ -177,25 +177,25 @@ export class LocalSessionBackend
|
|
|
177
177
|
intent?: ScrollIntent,
|
|
178
178
|
options?: { sync?: boolean }
|
|
179
179
|
): void {
|
|
180
|
-
if (!this.currentSessionId) return
|
|
180
|
+
if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
|
|
181
181
|
this.sessionManager.resizeTab(this.currentSessionId, tabId, cols, rows, intent, options)
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
disposeSession(tabId: string): void {
|
|
185
|
-
if (!this.currentSessionId) return
|
|
185
|
+
if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
|
|
186
186
|
logDebug('backend.local.disposeSession', { sessionId: this.currentSessionId, tabId })
|
|
187
187
|
this.sessionManager.closeTab(this.currentSessionId, tabId)
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
disposeAll(): void {
|
|
191
|
-
if (!this.currentSessionId) return
|
|
191
|
+
if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
|
|
192
192
|
logDebug('backend.local.disposeAll', { sessionId: this.currentSessionId })
|
|
193
193
|
this.sessionManager.disposeSession(this.currentSessionId)
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
destroy(keepSessions = true): void {
|
|
197
197
|
logDebug('backend.local.destroy', { keepSessions, sessionId: this.currentSessionId })
|
|
198
|
-
if (!keepSessions && this.currentSessionId) {
|
|
198
|
+
if (!keepSessions && this.currentSessionId != null && this.currentSessionId !== '') {
|
|
199
199
|
this.sessionManager.disposeSession(this.currentSessionId)
|
|
200
200
|
}
|
|
201
201
|
this.statusLoop.stop()
|