@brimveyn/aimux 1.19.3 → 1.19.5
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 +1 -1
- package/package.json +2 -2
- package/src/auto-commit/headless-commands.ts +14 -2
- package/src/cli/commands/tab/await-turn.ts +159 -0
- package/src/cli/commands/tab/await.ts +97 -0
- package/src/cli/commands/tab/create.ts +108 -17
- package/src/cli/commands/tab/prompt-io.ts +25 -0
- package/src/cli/commands/tab/run.ts +16 -147
- package/src/cli/commands/tab/send.ts +27 -1
- package/src/cli/commands/worktree/create-core.ts +96 -0
- package/src/cli/commands/worktree/create.ts +7 -80
- package/src/cli/flags.ts +8 -1
- package/src/cli/index.ts +3 -2
- package/src/cli/registry.ts +2 -0
- package/src/config.ts +16 -7
- package/src/doctor.ts +5 -4
- package/src/git/diff-limits.ts +8 -0
- package/src/git/git-diff.ts +67 -14
- package/src/git/git-status.ts +7 -0
- package/src/index.tsx +1 -1
- package/src/pty/assistant-question-extractor.ts +1 -0
- package/src/pty/assistant-status-detector.ts +45 -2
- package/src/pty/command-registry.ts +11 -0
- package/src/state/types.ts +15 -2
- package/src/ui/components/git/git-view.tsx +8 -0
- package/src/ui/components/git/image-diff/dimensions.ts +2 -1
package/src/git/git-diff.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { $ } from 'bun'
|
|
|
2
2
|
|
|
3
3
|
import type { DiffData, DiffFileStatus, GitFileEntry } from '../state/types'
|
|
4
4
|
|
|
5
|
+
import { MAX_DIFF_BYTES } from './diff-limits'
|
|
5
6
|
import { imageFormatLabel, imageMimeFromPath, isImagePath } from './image-detect'
|
|
6
7
|
|
|
7
8
|
function resolveStatus(entry: GitFileEntry): { status: DiffFileStatus; oldPath?: string } {
|
|
@@ -15,19 +16,37 @@ function resolveStatus(entry: GitFileEntry): { status: DiffFileStatus; oldPath?:
|
|
|
15
16
|
return { status: 'modified' }
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
// numstat marks a binary file by replacing both line counts with a dash.
|
|
20
|
+
function numstatSaysBinary(text: string): boolean {
|
|
21
|
+
const first = text.trim().split('\n')[0] ?? ''
|
|
22
|
+
return first.startsWith('-\t-\t')
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
async function isBinary(cwd: string, ref: string, path: string): Promise<boolean> {
|
|
19
|
-
const
|
|
20
|
-
if (
|
|
21
|
-
|
|
26
|
+
const tracked = await $`git -C ${cwd} diff ${ref} --numstat -- ${path}`.quiet().nothrow()
|
|
27
|
+
if (tracked.exitCode === 0) {
|
|
28
|
+
const text = tracked.text().trim()
|
|
29
|
+
if (text) return numstatSaysBinary(text)
|
|
30
|
+
}
|
|
31
|
+
// An untracked path is absent from `git diff <ref>` altogether, so the probe above
|
|
32
|
+
// comes back empty and says nothing about it. Compare against /dev/null to classify
|
|
33
|
+
// it — without this, every untracked binary reads as text and gets slurped as one
|
|
34
|
+
// string.
|
|
35
|
+
const untracked = await $`git -C ${cwd} diff --no-index --numstat /dev/null -- ${path}`
|
|
36
|
+
.quiet()
|
|
37
|
+
.nothrow()
|
|
38
|
+
const text = untracked.text().trim()
|
|
22
39
|
if (!text) return false
|
|
23
|
-
|
|
24
|
-
return first.startsWith('-\t-\t')
|
|
40
|
+
return numstatSaysBinary(text)
|
|
25
41
|
}
|
|
26
42
|
|
|
27
43
|
async function readHeadSize(cwd: string, ref: string, path: string): Promise<number> {
|
|
28
|
-
|
|
44
|
+
// `cat-file -s` reads the blob header only. `git show` would materialise the entire
|
|
45
|
+
// blob as a string just to measure it — the very thing MAX_DIFF_BYTES exists to stop.
|
|
46
|
+
const result = await $`git -C ${cwd} cat-file -s ${ref}:${path}`.quiet().nothrow()
|
|
29
47
|
if (result.exitCode !== 0) return 0
|
|
30
|
-
|
|
48
|
+
const size = Number.parseInt(result.text().trim(), 10)
|
|
49
|
+
return Number.isFinite(size) ? size : 0
|
|
31
50
|
}
|
|
32
51
|
|
|
33
52
|
async function readWorkingSize(cwd: string, path: string): Promise<number> {
|
|
@@ -91,6 +110,22 @@ function resolveCompareRef(headOffset: number, compareRef: string | undefined):
|
|
|
91
110
|
return headOffset > 0 ? `HEAD~${headOffset}` : 'HEAD'
|
|
92
111
|
}
|
|
93
112
|
|
|
113
|
+
// Both sides are measured from metadata only (blob header + stat), never by reading
|
|
114
|
+
// content — the sizes are what decide whether reading content is safe at all.
|
|
115
|
+
async function diffSizes(
|
|
116
|
+
cwd: string,
|
|
117
|
+
ref: string,
|
|
118
|
+
path: string,
|
|
119
|
+
headPath: string,
|
|
120
|
+
status: DiffFileStatus
|
|
121
|
+
): Promise<{ after: number; before: number }> {
|
|
122
|
+
const [before, after] = await Promise.all([
|
|
123
|
+
status === 'new' ? Promise.resolve(0) : readHeadSize(cwd, ref, headPath),
|
|
124
|
+
status === 'deleted' ? Promise.resolve(0) : readWorkingSize(cwd, path),
|
|
125
|
+
])
|
|
126
|
+
return { after, before }
|
|
127
|
+
}
|
|
128
|
+
|
|
94
129
|
export async function fetchDiff(
|
|
95
130
|
cwd: string,
|
|
96
131
|
file: GitFileEntry,
|
|
@@ -99,9 +134,31 @@ export async function fetchDiff(
|
|
|
99
134
|
): Promise<DiffData> {
|
|
100
135
|
const { oldPath, status } = resolveStatus(file)
|
|
101
136
|
const ref = resolveCompareRef(headOffset, compareRef)
|
|
137
|
+
const headPath = oldPath ?? file.path
|
|
138
|
+
|
|
139
|
+
const { after: sizeAfter, before: sizeBefore } = await diffSizes(
|
|
140
|
+
cwd,
|
|
141
|
+
ref,
|
|
142
|
+
file.path,
|
|
143
|
+
headPath,
|
|
144
|
+
status
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
// Ahead of the image branch on purpose: a multi-gigabyte .png must not be read
|
|
148
|
+
// into memory either.
|
|
149
|
+
if (sizeBefore > MAX_DIFF_BYTES || sizeAfter > MAX_DIFF_BYTES) {
|
|
150
|
+
const data: DiffData = {
|
|
151
|
+
binarySizeAfter: sizeAfter,
|
|
152
|
+
binarySizeBefore: sizeBefore,
|
|
153
|
+
path: file.path,
|
|
154
|
+
rawDiff: '',
|
|
155
|
+
status: 'too-large',
|
|
156
|
+
}
|
|
157
|
+
if (oldPath != null && oldPath !== '') data.oldPath = oldPath
|
|
158
|
+
return data
|
|
159
|
+
}
|
|
102
160
|
|
|
103
161
|
if (isImagePath(file.path)) {
|
|
104
|
-
const headPath = oldPath ?? file.path
|
|
105
162
|
const wantsBefore = status !== 'new'
|
|
106
163
|
const wantsAfter = status !== 'deleted'
|
|
107
164
|
const [imageBytesBefore, imageBytesAfter] = await Promise.all([
|
|
@@ -124,13 +181,9 @@ export async function fetchDiff(
|
|
|
124
181
|
}
|
|
125
182
|
|
|
126
183
|
if (await isBinary(cwd, ref, file.path)) {
|
|
127
|
-
const [binarySizeBefore, binarySizeAfter] = await Promise.all([
|
|
128
|
-
readHeadSize(cwd, ref, file.path),
|
|
129
|
-
readWorkingSize(cwd, file.path),
|
|
130
|
-
])
|
|
131
184
|
return {
|
|
132
|
-
binarySizeAfter,
|
|
133
|
-
binarySizeBefore,
|
|
185
|
+
binarySizeAfter: sizeAfter,
|
|
186
|
+
binarySizeBefore: sizeBefore,
|
|
134
187
|
path: file.path,
|
|
135
188
|
rawDiff: '',
|
|
136
189
|
status: 'binary',
|
package/src/git/git-status.ts
CHANGED
|
@@ -8,6 +8,8 @@ import type {
|
|
|
8
8
|
GitRefreshPayload,
|
|
9
9
|
} from '../state/types'
|
|
10
10
|
|
|
11
|
+
import { MAX_DIFF_BYTES } from './diff-limits'
|
|
12
|
+
|
|
11
13
|
interface NumstatRow {
|
|
12
14
|
added: number | null
|
|
13
15
|
removed: number | null
|
|
@@ -163,6 +165,11 @@ async function countUntrackedLines(cwd: string, path: string): Promise<number |
|
|
|
163
165
|
try {
|
|
164
166
|
const file = Bun.file(`${cwd}/${path}`)
|
|
165
167
|
if (!(await file.exists())) return null
|
|
168
|
+
// Size gate before the read, not after: `.text()` aborts the process on an
|
|
169
|
+
// oversized blob rather than throwing, so the catch below would never run. Callers
|
|
170
|
+
// treat null as "no count available", which is the honest answer for a 3 GB
|
|
171
|
+
// download sitting untracked in the working tree.
|
|
172
|
+
if (file.size > MAX_DIFF_BYTES) return null
|
|
166
173
|
const text = await file.text()
|
|
167
174
|
if (text.length === 0) return 0
|
|
168
175
|
let count = 0
|
package/src/index.tsx
CHANGED
|
@@ -83,7 +83,7 @@ if (command === '--help' || command === '-h' || command === 'help') {
|
|
|
83
83
|
'',
|
|
84
84
|
'Common CLI verbs at a glance',
|
|
85
85
|
' aimux tab list Enumerate tabs (+ activeTabId)',
|
|
86
|
-
' aimux tab create --assistant <id> [--title …] Spawn claude / codex / opencode / terminal / …',
|
|
86
|
+
' aimux tab create --assistant <id> [--title …] Spawn claude / codex / opencode / grok / terminal / …',
|
|
87
87
|
' aimux tab send <tabId> [text] [--enter|--keys|--stdin] Type, chord, or paste into a tab',
|
|
88
88
|
' aimux tab focus <tabId> Bring a tab to the foreground',
|
|
89
89
|
' aimux tab close <tabId> Terminate a tab',
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* `src/detect.rs` in that repo.
|
|
7
7
|
*
|
|
8
8
|
* The detector classifies a terminal session as `working`, `waiting-input`,
|
|
9
|
-
* or `idle`. Built-in CLIs (claude, codex, opencode)
|
|
9
|
+
* or `idle`. Built-in CLIs (claude, codex, opencode, grok) have dedicated classify* functions.
|
|
10
10
|
* tables. Custom CLIs fall back to a generic heuristic that (a) recognises
|
|
11
11
|
* common shells as always-idle and (b) uses pane-tail change velocity plus
|
|
12
12
|
* generic y/n / confirm prompt patterns.
|
|
@@ -95,7 +95,7 @@ export class AssistantStatusDetector {
|
|
|
95
95
|
export function extractTailLines(viewport: TerminalSnapshot, lineCount: number): string[] {
|
|
96
96
|
const isScrolledToBottom = viewport.viewportY === viewport.baseY
|
|
97
97
|
const lines = isScrolledToBottom ? viewport.lines : (viewport.tailLines ?? viewport.lines)
|
|
98
|
-
// Full-screen TUIs (claude, opencode) paint in the alternate buffer and
|
|
98
|
+
// Full-screen TUIs (claude, opencode, grok) paint in the alternate buffer and
|
|
99
99
|
// often leave the last rows blank, putting their status bar higher up.
|
|
100
100
|
// Skip trailing blank rows before taking the last `lineCount`.
|
|
101
101
|
let end = lines.length
|
|
@@ -138,6 +138,8 @@ function classifyBuiltin(
|
|
|
138
138
|
return classifyCodex(haystack)
|
|
139
139
|
case 'opencode':
|
|
140
140
|
return classifyOpencode(haystack)
|
|
141
|
+
case 'grok':
|
|
142
|
+
return classifyGrok(haystack, rawTail)
|
|
141
143
|
default:
|
|
142
144
|
return null
|
|
143
145
|
}
|
|
@@ -212,6 +214,47 @@ function classifyOpencode(haystack: string): TabActivity {
|
|
|
212
214
|
return 'idle'
|
|
213
215
|
}
|
|
214
216
|
|
|
217
|
+
function classifyGrok(haystack: string, _rawTail: string): TabActivity {
|
|
218
|
+
// Waiting for user decision / input (plan approval, Q&A, permissions, confirms).
|
|
219
|
+
// These are the distinctive Grok Build TUI states that must produce 'waiting-input'
|
|
220
|
+
// so the rest of the system (turn lifecycle, question events, UI chips, orchestrators)
|
|
221
|
+
// treats grok the same as claude/codex/opencode.
|
|
222
|
+
if (
|
|
223
|
+
haystack.includes('waiting on answers') ||
|
|
224
|
+
haystack.includes('pprove') || // stylized "[ a ] pprove [ c ] omment [ q ] uit plan"
|
|
225
|
+
haystack.includes('omment') ||
|
|
226
|
+
haystack.includes('uit plan') ||
|
|
227
|
+
(haystack.includes('approve') &&
|
|
228
|
+
(haystack.includes('comment') || haystack.includes('quit') || haystack.includes('plan'))) ||
|
|
229
|
+
haystack.includes('enter :select') ||
|
|
230
|
+
haystack.includes('enter to select') ||
|
|
231
|
+
haystack.includes('enter submit') ||
|
|
232
|
+
haystack.includes('do you want') ||
|
|
233
|
+
haystack.includes('would you like') ||
|
|
234
|
+
haystack.includes('permission required') ||
|
|
235
|
+
haystack.includes('permission to') ||
|
|
236
|
+
haystack.includes('approve?') ||
|
|
237
|
+
haystack.includes('allow?')
|
|
238
|
+
) {
|
|
239
|
+
return 'waiting-input'
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Agent actively reasoning or executing (mirrors claude spinner + interrupt logic).
|
|
243
|
+
// "Thought for Xs" is the primary visible trace while Grok thinks/plans.
|
|
244
|
+
if (
|
|
245
|
+
haystack.includes('thought for') || // "Thought for 3.4s", etc.
|
|
246
|
+
haystack.includes('thinking…') ||
|
|
247
|
+
haystack.includes('thinking ...') ||
|
|
248
|
+
haystack.includes('esc to interrupt') ||
|
|
249
|
+
haystack.includes('esc interrupt') ||
|
|
250
|
+
haystack.includes('esc: interrupt')
|
|
251
|
+
) {
|
|
252
|
+
return 'working'
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return 'idle'
|
|
256
|
+
}
|
|
257
|
+
|
|
215
258
|
const GENERIC_WAITING_PATTERNS: string[] = [
|
|
216
259
|
'[y/n]',
|
|
217
260
|
'(y/n)',
|
|
@@ -59,6 +59,17 @@ export const ASSISTANT_OPTIONS: AssistantOption[] = [
|
|
|
59
59
|
buildModelArgs: (model) => ['--model', model],
|
|
60
60
|
},
|
|
61
61
|
},
|
|
62
|
+
{
|
|
63
|
+
command: 'grok',
|
|
64
|
+
description: 'xAI Grok Build CLI',
|
|
65
|
+
id: 'grok',
|
|
66
|
+
label: 'Grok',
|
|
67
|
+
model: {
|
|
68
|
+
// Grok supports -m (and likely --model) plus --effort (alias --reasoning-effort).
|
|
69
|
+
buildEffortArgs: (effort) => ['--effort', effort],
|
|
70
|
+
buildModelArgs: (model) => ['-m', model],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
62
73
|
{
|
|
63
74
|
command: 'agy',
|
|
64
75
|
description: 'Antigravity CLI',
|
package/src/state/types.ts
CHANGED
|
@@ -4,7 +4,13 @@ import type { ThemedToken } from 'shiki'
|
|
|
4
4
|
import type { WorktreeTemplate } from '../config'
|
|
5
5
|
import type { LayoutNode, SplitDirection } from './layout-tree'
|
|
6
6
|
|
|
7
|
-
export type BuiltinAssistantId =
|
|
7
|
+
export type BuiltinAssistantId =
|
|
8
|
+
| 'claude'
|
|
9
|
+
| 'codex'
|
|
10
|
+
| 'opencode'
|
|
11
|
+
| 'grok'
|
|
12
|
+
| 'terminal'
|
|
13
|
+
| 'antigravity'
|
|
8
14
|
|
|
9
15
|
export type AssistantId = BuiltinAssistantId | (string & {})
|
|
10
16
|
|
|
@@ -256,7 +262,14 @@ export interface GitPanelState {
|
|
|
256
262
|
error: GitPanelError | null
|
|
257
263
|
}
|
|
258
264
|
|
|
259
|
-
export type DiffFileStatus =
|
|
265
|
+
export type DiffFileStatus =
|
|
266
|
+
| 'modified'
|
|
267
|
+
| 'new'
|
|
268
|
+
| 'deleted'
|
|
269
|
+
| 'binary'
|
|
270
|
+
| 'renamed'
|
|
271
|
+
| 'image'
|
|
272
|
+
| 'too-large'
|
|
260
273
|
|
|
261
274
|
export interface DiffData {
|
|
262
275
|
path: string
|
|
@@ -7,6 +7,7 @@ import type { DiffData, GitDiffView } from '../../../state/types'
|
|
|
7
7
|
import type { ThemeId } from '../../themes'
|
|
8
8
|
|
|
9
9
|
import { diffHash } from '../../../git/diff-hash'
|
|
10
|
+
import { MAX_DIFF_BYTES } from '../../../git/diff-limits'
|
|
10
11
|
import { getMergeBase } from '../../../git/divergence'
|
|
11
12
|
import { fetchDiff } from '../../../git/git-diff'
|
|
12
13
|
import { useGitPanelPolling } from '../../../git/git-poller'
|
|
@@ -21,6 +22,7 @@ import { PierreDiff, type PierreDiffHandle } from './diff-renderer'
|
|
|
21
22
|
import { useDiffPrefetch } from './diff-renderer/use-diff-prefetch'
|
|
22
23
|
import { GitPanel } from './git-panel'
|
|
23
24
|
import { ImageDiffView } from './image-diff'
|
|
25
|
+
import { formatBytes } from './image-diff/dimensions'
|
|
24
26
|
import { GitPaneHeader } from './pane/git-pane-header'
|
|
25
27
|
|
|
26
28
|
interface DiffStageProps {
|
|
@@ -33,6 +35,12 @@ interface DiffStageProps {
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
function placeholderText(diff: DiffData): string | null {
|
|
38
|
+
if (diff.status === 'too-large') {
|
|
39
|
+
const before = diff.binarySizeBefore ?? 0
|
|
40
|
+
const after = diff.binarySizeAfter ?? 0
|
|
41
|
+
const largest = Math.max(before, after)
|
|
42
|
+
return `(file too large to diff — ${formatBytes(largest)}, limit ${formatBytes(MAX_DIFF_BYTES)})`
|
|
43
|
+
}
|
|
36
44
|
if (diff.status === 'binary') {
|
|
37
45
|
const before = diff.binarySizeBefore ?? 0
|
|
38
46
|
const after = diff.binarySizeAfter ?? 0
|
|
@@ -92,5 +92,6 @@ export function readImageDimensions(bytes: Uint8Array): ImageDimensions | null {
|
|
|
92
92
|
export function formatBytes(n: number): string {
|
|
93
93
|
if (n < 1024) return `${n} B`
|
|
94
94
|
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
|
|
95
|
-
return `${(n / (1024 * 1024)).toFixed(2)} MB`
|
|
95
|
+
if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(2)} MB`
|
|
96
|
+
return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`
|
|
96
97
|
}
|