@brimveyn/aimux 1.19.2 → 1.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brimveyn/aimux",
3
- "version": "1.19.2",
3
+ "version": "1.19.3",
4
4
  "description": "A terminal multiplexer for AI CLIs. Run Claude, Codex, OpenCode side-by-side with tabbed navigation, split panes, and persistent sessions.",
5
5
  "keywords": [
6
6
  "ai",
@@ -7,7 +7,11 @@ import {
7
7
  IPC_CAPABILITY_THIN_ATTACH,
8
8
  } from '../../../ipc/protocol'
9
9
  import { createPrefixedId } from '../../../platform/id'
10
- import { getAllAssistantOptions, parseCommand } from '../../../pty/command-registry'
10
+ import {
11
+ buildAssistantModelArgs,
12
+ getAllAssistantOptions,
13
+ parseCommand,
14
+ } from '../../../pty/command-registry'
11
15
  import { SHARED_FLAGS } from '../../flags'
12
16
  import { EXIT_OK, writeJson } from '../../output'
13
17
 
@@ -30,6 +34,16 @@ export const tabCreate: CliCommand = {
30
34
  kind: 'string',
31
35
  name: 'command',
32
36
  },
37
+ {
38
+ description: 'model for the worker (maps to the assistant’s model flag)',
39
+ kind: 'string',
40
+ name: 'model',
41
+ },
42
+ {
43
+ description: 'reasoning-effort level (maps to the assistant’s effort flag)',
44
+ kind: 'string',
45
+ name: 'effort',
46
+ },
33
47
  {
34
48
  description: 'worktree id the tab belongs to (defaults to the workspace’s active worktree)',
35
49
  kind: 'string',
@@ -49,8 +63,21 @@ export const tabCreate: CliCommand = {
49
63
  `unknown assistant: ${assistantId} (known: ${options.map((o) => o.id).join(', ')})`
50
64
  )
51
65
  }
52
- const command =
53
- typeof ctx.args.flags.command === 'string' ? ctx.args.flags.command : option.command
66
+ const commandOverride =
67
+ typeof ctx.args.flags.command === 'string' ? ctx.args.flags.command : undefined
68
+ const model = typeof ctx.args.flags.model === 'string' ? ctx.args.flags.model : undefined
69
+ const effort = typeof ctx.args.flags.effort === 'string' ? ctx.args.flags.effort : undefined
70
+
71
+ // A full `--command` override owns the whole invocation, so `--model` /
72
+ // `--effort` (which only make sense as additions to the assistant default)
73
+ // would be ambiguous alongside it — reject rather than silently drop them.
74
+ if (commandOverride !== undefined && (model !== undefined || effort !== undefined)) {
75
+ throw new Error(
76
+ '--model / --effort cannot be combined with --command (bake them into --command)'
77
+ )
78
+ }
79
+
80
+ const command = commandOverride ?? option.command
54
81
  const title = typeof ctx.args.flags.title === 'string' ? ctx.args.flags.title : option.label
55
82
  const cwdRaw = typeof ctx.args.flags.cwd === 'string' ? ctx.args.flags.cwd : undefined
56
83
  const cwd = cwdRaw === undefined ? undefined : resolvePath(cwdRaw)
@@ -81,7 +108,11 @@ export const tabCreate: CliCommand = {
81
108
  // Thin-attach so we don't clobber the UI's dimensions on the same session.
82
109
  await daemon.attach({ cols: 0, rows: 0, sessionId: workspace.id, thin: true })
83
110
 
84
- const { args, executable } = parseCommand(command)
111
+ const { args: baseArgs, executable } = parseCommand(command)
112
+ // Append the model/effort flags to the assistant default. Throws if the
113
+ // assistant has no control for a requested dimension.
114
+ const modelArgs = buildAssistantModelArgs(option, { effort, model })
115
+ const args = [...baseArgs, ...modelArgs]
85
116
  const tabId = createPrefixedId('tab')
86
117
 
87
118
  // cols/rows = 0 means "fall back to the session's last attached size" on
@@ -101,7 +132,16 @@ export const tabCreate: CliCommand = {
101
132
  worktreeId,
102
133
  })
103
134
 
104
- writeJson({ assistant: assistantId, command, tabId, title, worktreeId })
135
+ const resolvedCommand = [executable, ...args].join(' ')
136
+ writeJson({
137
+ assistant: assistantId,
138
+ command: resolvedCommand,
139
+ effort: effort ?? null,
140
+ model: model ?? null,
141
+ tabId,
142
+ title,
143
+ worktreeId,
144
+ })
105
145
  return EXIT_OK
106
146
  },
107
147
  summary: 'Create a new tab in the active workspace',
@@ -1,6 +1,7 @@
1
1
  import type { CliCommand } from '../../registry'
2
2
 
3
3
  import { listGitWorktrees } from '../../../git/worktree'
4
+ import { IPC_CAPABILITY_LIST_TABS } from '../../../ipc/protocol'
4
5
  import { SHARED_FLAGS } from '../../flags'
5
6
  import { EXIT_OK, writeJson } from '../../output'
6
7
 
@@ -12,6 +13,28 @@ export const worktreeList: CliCommand = {
12
13
  const workspace = ctx.getWorkspace()
13
14
  const records = workspace.worktrees ?? []
14
15
 
16
+ // Count live tabs per worktree so an orchestrator can see co-location
17
+ // (several workers sharing one worktree) at a glance. Best-effort: `null`
18
+ // when the daemon is unreachable or predates the listTabs capability —
19
+ // `worktree list` otherwise reads purely from the catalog + git, so we don't
20
+ // want a down daemon to fail it.
21
+ const tabCounts = new Map<string, number>()
22
+ let tabCountsAvailable = false
23
+ try {
24
+ const daemon = await ctx.getDaemon()
25
+ if (daemon.hasCapability(IPC_CAPABILITY_LIST_TABS)) {
26
+ const { tabs } = await daemon.listTabs(workspace.id)
27
+ for (const tab of tabs) {
28
+ if (tab.worktreeId != null) {
29
+ tabCounts.set(tab.worktreeId, (tabCounts.get(tab.worktreeId) ?? 0) + 1)
30
+ }
31
+ }
32
+ tabCountsAvailable = true
33
+ }
34
+ } catch {
35
+ // daemon unreachable — leave tabCount null rather than failing the list.
36
+ }
37
+
15
38
  // Cross-check against git so we can flag catalog entries that git no
16
39
  // longer knows about (prunable / vanished) — otherwise the CLI would
17
40
  // happily report worktrees that don't exist on disk.
@@ -35,6 +58,7 @@ export const worktreeList: CliCommand = {
35
58
  path: w.path,
36
59
  repoRoot: w.repoRoot,
37
60
  source: w.source,
61
+ tabCount: tabCountsAvailable ? (tabCounts.get(w.id) ?? 0) : null,
38
62
  })),
39
63
  })
40
64
  return EXIT_OK
@@ -1,10 +1,27 @@
1
1
  import type { AssistantId } from '../state/types'
2
2
 
3
+ /**
4
+ * How an assistant renders a vendor-neutral model / reasoning-effort selection
5
+ * into its own CLI args. Vendor flag syntax lives here on the assistant
6
+ * definition — the source of truth — rather than in a separate resolver, so
7
+ * there's exactly one place that knows `claude` takes `--effort` while `codex`
8
+ * takes `-c model_reasoning_effort=…`. An absent builder means the CLI has no
9
+ * control for that dimension, and passing `--model` / `--effort` for it is an
10
+ * error (see `buildAssistantModelArgs`). Values themselves are passed through
11
+ * unvalidated: the worker CLI is the source of truth for its own vocabulary and
12
+ * rejects a bad value with its own clear startup error.
13
+ */
14
+ export interface AssistantModelSpec {
15
+ buildModelArgs?: (model: string) => string[]
16
+ buildEffortArgs?: (effort: string) => string[]
17
+ }
18
+
3
19
  export interface AssistantOption {
4
20
  id: AssistantId
5
21
  label: string
6
22
  command: string
7
23
  description: string
24
+ model?: AssistantModelSpec
8
25
  }
9
26
 
10
27
  const DEFAULT_SHELL =
@@ -17,18 +34,30 @@ export const ASSISTANT_OPTIONS: AssistantOption[] = [
17
34
  description: 'Anthropic Claude CLI',
18
35
  id: 'claude',
19
36
  label: 'Claude',
37
+ model: {
38
+ buildEffortArgs: (effort) => ['--effort', effort],
39
+ buildModelArgs: (model) => ['--model', model],
40
+ },
20
41
  },
21
42
  {
22
43
  command: 'codex',
23
44
  description: 'OpenAI Codex CLI',
24
45
  id: 'codex',
25
46
  label: 'Codex',
47
+ model: {
48
+ buildEffortArgs: (effort) => ['-c', `model_reasoning_effort=${effort}`],
49
+ buildModelArgs: (model) => ['--model', model],
50
+ },
26
51
  },
27
52
  {
28
53
  command: 'opencode',
29
54
  description: 'OpenCode CLI',
30
55
  id: 'opencode',
31
56
  label: 'OpenCode',
57
+ // OpenCode takes a `provider/model` selector but has no reasoning-effort flag.
58
+ model: {
59
+ buildModelArgs: (model) => ['--model', model],
60
+ },
32
61
  },
33
62
  {
34
63
  command: 'agy',
@@ -74,3 +103,34 @@ export function parseCommand(commandString: string): { executable: string; args:
74
103
  const parts = commandString.trim().split(/\s+/).filter(Boolean)
75
104
  return { args: parts.slice(1), executable: parts[0] ?? '' }
76
105
  }
106
+
107
+ /**
108
+ * Translate a vendor-neutral `{ model, effort }` selection into the extra CLI
109
+ * args to append to an assistant's default command. Throws if the assistant has
110
+ * no control for a requested dimension (e.g. `--effort` on OpenCode, anything on
111
+ * `terminal`) — that combination is unrepresentable, so we fail at the CLI
112
+ * boundary rather than spawn a worker that silently ignores the request.
113
+ */
114
+ export function buildAssistantModelArgs(
115
+ option: AssistantOption,
116
+ selection: { model?: string; effort?: string }
117
+ ): string[] {
118
+ const args: string[] = []
119
+ const { effort, model } = selection
120
+
121
+ if (model !== undefined && model !== '') {
122
+ if (!option.model?.buildModelArgs) {
123
+ throw new Error(`assistant '${option.id}' does not support --model`)
124
+ }
125
+ args.push(...option.model.buildModelArgs(model))
126
+ }
127
+
128
+ if (effort !== undefined && effort !== '') {
129
+ if (!option.model?.buildEffortArgs) {
130
+ throw new Error(`assistant '${option.id}' does not support --effort`)
131
+ }
132
+ args.push(...option.model.buildEffortArgs(effort))
133
+ }
134
+
135
+ return args
136
+ }