magus-cli 7.11.8 → 7.12.1

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.
@@ -72,6 +72,19 @@ export interface AliasFlag {
72
72
  * token. See `TEMPLATE_TOKENS` for the supported placeholder set.
73
73
  */
74
74
  templated?: boolean;
75
+ /**
76
+ * Environment assignment (`NAME=VALUE`) the alias sets before `claude`
77
+ * whenever the flag is enabled. For `kind: "boolean"` the assignment is the
78
+ * whole flag, and `flag` carries the same string so lookups by `flag` keep
79
+ * working. For `kind: "select"` the flag also emits `flag <value>` as argv:
80
+ * the assignment and the argv are one setting that only works as a pair.
81
+ */
82
+ env?: string;
83
+ /**
84
+ * Value check for `kind: "text"` flags, run when the user enters a value.
85
+ * Returns null when the value is valid, otherwise the reason it is not.
86
+ */
87
+ validateValue?: (value: string) => string | null;
75
88
  /** UI grouping; rendered as section headers in the list. */
76
89
  group: FlagGroup;
77
90
  }
@@ -100,7 +113,8 @@ export type FlagGroup =
100
113
  | "channels"
101
114
  | "debug"
102
115
  | "integration"
103
- | "worktree";
116
+ | "worktree"
117
+ | "agents";
104
118
 
105
119
  export const FLAG_GROUPS: Array<{ id: FlagGroup; label: string }> = [
106
120
  { id: "danger", label: "Permissions" },
@@ -110,8 +124,81 @@ export const FLAG_GROUPS: Array<{ id: FlagGroup; label: string }> = [
110
124
  { id: "debug", label: "Debug" },
111
125
  { id: "integration", label: "Integrations" },
112
126
  { id: "worktree", label: "Worktree" },
127
+ { id: "agents", label: "Agent teams" },
113
128
  ];
114
129
 
130
+ /** Smallest auto-compact window Claude Code accepts, in tokens. */
131
+ export const AUTOCOMPACT_MIN_TOKENS = 100_000;
132
+ /** Largest auto-compact window Claude Code accepts, in tokens. */
133
+ export const AUTOCOMPACT_MAX_TOKENS = 1_000_000;
134
+
135
+ /** Claude Code's own wording when it refuses an `--autocompact` value. */
136
+ export const AUTOCOMPACT_INVALID_REASON =
137
+ "It must be 'auto', or between 100k and 1M (e.g. 500k, 200000, or 200 as shorthand)";
138
+
139
+ const EXPONENT_NUMBER = /^[+-]?(\d+(\.\d*)?|\.\d+)[eE][+-]?\d+$/;
140
+ const GROUPED_DIGITS = /^[+-]?\d{1,3}([_,   ])\d{3}(?:\1\d{3})*$/;
141
+ const DIGIT_SEPARATORS = /[_,   ]/g;
142
+
143
+ /**
144
+ * Read an unsuffixed number the way Claude Code does: exponent notation
145
+ * (`2e5`) when it is an integer, digit groups (`200,000`, `200_000`), and
146
+ * otherwise `parseInt`, which keeps the leading digits of the string.
147
+ */
148
+ function parsePlainNumber(text: string): number {
149
+ if (text.length <= 32) {
150
+ if (EXPONENT_NUMBER.test(text)) {
151
+ const n = Number(text);
152
+ return Number.isInteger(n) ? n : Number.NaN;
153
+ }
154
+ if (GROUPED_DIGITS.test(text)) {
155
+ return Number.parseInt(text.replace(DIGIT_SEPARATORS, ""), 10);
156
+ }
157
+ }
158
+ return Number.parseInt(text, 10);
159
+ }
160
+
161
+ /**
162
+ * Parse an `--autocompact` value exactly as Claude Code's option parser does.
163
+ *
164
+ * `auto` (any case) stays `"auto"`. A trailing `k` or `m` multiplies by a
165
+ * thousand or a million; an unsuffixed number from 100 to 1000 is read as
166
+ * thousands (`200` is 200,000 tokens). The result must fall between
167
+ * {@link AUTOCOMPACT_MIN_TOKENS} and {@link AUTOCOMPACT_MAX_TOKENS}.
168
+ *
169
+ * Returns the token count, `"auto"`, or `undefined` when Claude Code would
170
+ * refuse the value.
171
+ */
172
+ export function parseAutocompactSize(
173
+ value: string,
174
+ ): number | "auto" | undefined {
175
+ if (value.trim().toLowerCase() === "auto") return "auto";
176
+ const text = value.trim();
177
+ const suffix = text.slice(-1).toLowerCase();
178
+ const multiplier =
179
+ suffix === "m" ? 1_000_000 : suffix === "k" ? 1_000 : undefined;
180
+ const parsed =
181
+ multiplier === undefined
182
+ ? parsePlainNumber(text)
183
+ : Number.parseFloat(text) * multiplier;
184
+ const tokens =
185
+ multiplier === undefined && parsed >= 100 && parsed <= 1000
186
+ ? parsed * 1000
187
+ : parsed;
188
+ if (Number.isNaN(tokens) || tokens < 1) return undefined;
189
+ if (tokens < AUTOCOMPACT_MIN_TOKENS || tokens > AUTOCOMPACT_MAX_TOKENS) {
190
+ return undefined;
191
+ }
192
+ return Math.round(tokens);
193
+ }
194
+
195
+ /** `validateValue` for the context window size: Claude Code's reason, or null. */
196
+ export function validateAutocompactSize(value: string): string | null {
197
+ return parseAutocompactSize(value) === undefined
198
+ ? AUTOCOMPACT_INVALID_REASON
199
+ : null;
200
+ }
201
+
115
202
  export const ALIAS_FLAGS: AliasFlag[] = [
116
203
  {
117
204
  id: "dangerously-skip-permissions",
@@ -222,6 +309,16 @@ export const ALIAS_FLAGS: AliasFlag[] = [
222
309
  description:
223
310
  "Path to a file whose contents are appended to the default system prompt.",
224
311
  },
312
+ {
313
+ id: "autocompact",
314
+ label: "Context window size",
315
+ flag: "--autocompact",
316
+ kind: "text",
317
+ group: "context",
318
+ validateValue: validateAutocompactSize,
319
+ description:
320
+ "The window size Claude Code compacts the conversation at: auto, or 100k to 1M tokens (e.g. 200k, 500k, 1m, 200000, or 200 as shorthand for 200k).",
321
+ },
225
322
  {
226
323
  id: "channels",
227
324
  label: "Channels (approved)",
@@ -291,6 +388,22 @@ export const ALIAS_FLAGS: AliasFlag[] = [
291
388
  { label: "classic", value: "classic" },
292
389
  ],
293
390
  },
391
+ {
392
+ id: "agent-teams",
393
+ label: "Agent teams",
394
+ flag: "--teammate-mode",
395
+ env: "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1",
396
+ kind: "select",
397
+ group: "agents",
398
+ description:
399
+ "Turn on agent teams and pick where teammates run. The alias sets CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 and passes --teammate-mode, because one does nothing without the other. tmux opens each teammate in a split pane, but only when the session starts inside tmux. iterm2 uses iTerm2 panes, in-process keeps them in this terminal, auto picks for you.",
400
+ options: [
401
+ { label: "tmux", value: "tmux" },
402
+ { label: "iterm2", value: "iterm2" },
403
+ { label: "in-process", value: "in-process" },
404
+ { label: "auto", value: "auto" },
405
+ ],
406
+ },
294
407
  ];
295
408
 
296
409
  /** Look up a flag by id. Throws if missing — keeps callers honest. */
@@ -125,6 +125,9 @@ export function renderArgs(config: AliasConfig): Segment[] {
125
125
  const seenXor = new Set<string>();
126
126
 
127
127
  for (const flag of ALIAS_FLAGS) {
128
+ // A boolean env flag is only an assignment before `claude`: see renderEnv.
129
+ // A select env flag adds its argv here as well.
130
+ if (isEnvOnly(flag)) continue;
128
131
  if (flag.requires) {
129
132
  const dep = config.flags[flag.requires];
130
133
  if (!isFlagEnabled(dep)) continue;
@@ -154,18 +157,40 @@ export function renderArgs(config: AliasConfig): Segment[] {
154
157
  }
155
158
 
156
159
  /**
157
- * Convenience: render each segment to its literal-token approximation.
158
- * Composite segments are stringified by joining their literal parts with
159
- * `<sub>` placeholders for raw parts. Used by tests and validation summaries
160
- * where the actual shell substitution doesn't matter.
160
+ * The environment assignments (`NAME=VALUE`) the alias sets before `claude`,
161
+ * one per enabled env flag, in catalog order. `requires` applies here as it
162
+ * does to argv flags. Pure function.
163
+ */
164
+ export function renderEnv(config: AliasConfig): string[] {
165
+ const out: string[] = [];
166
+ for (const flag of ALIAS_FLAGS) {
167
+ if (!flag.env) continue;
168
+ if (flag.requires && !isFlagEnabled(config.flags[flag.requires])) continue;
169
+ if (isFlagEnabled(config.flags[flag.id])) out.push(flag.env);
170
+ }
171
+ return out;
172
+ }
173
+
174
+ /**
175
+ * Convenience: the whole command after the alias name as literal tokens, env
176
+ * assignments first, then argv. Composite segments are stringified by joining
177
+ * their literal parts with `<sub>` placeholders for raw parts. Used by the
178
+ * adoption fixpoint check, tests and validation summaries, where the actual
179
+ * shell substitution doesn't matter.
161
180
  */
162
181
  export function renderArgsAsTokens(config: AliasConfig): string[] {
163
- return renderArgs(config).map((seg) => {
182
+ const argv = renderArgs(config).map((seg) => {
164
183
  if (seg.kind === "literal") return seg.text;
165
184
  return seg.parts
166
185
  .map((p) => (p.kind === "literal" ? p.text : `<${p.posix}>`))
167
186
  .join("");
168
187
  });
188
+ return [...renderEnv(config), ...argv];
189
+ }
190
+
191
+ /** A boolean env flag emits its assignment and no argv. */
192
+ function isEnvOnly(flag: AliasFlag): boolean {
193
+ return flag.env !== undefined && flag.kind === "boolean";
169
194
  }
170
195
 
171
196
  function isFlagEnabled(value: FlagValue | undefined): boolean {
@@ -345,24 +370,32 @@ export function renderAlias(
345
370
  shell: ShellKind,
346
371
  ): RenderedAlias {
347
372
  const args = renderArgs(config);
373
+ const env = renderEnv(config);
348
374
  const name = config.aliasName;
349
375
  if (shell === "fish") {
350
- return { shell, block: renderFishBlock(name, args) };
376
+ return { shell, block: renderFishBlock(name, env, args) };
351
377
  }
352
- return { shell, block: renderPosixBlock(name, args) };
378
+ return { shell, block: renderPosixBlock(name, env, args) };
353
379
  }
354
380
 
355
- function renderPosixBlock(name: string, segments: Segment[]): string {
381
+ function renderPosixBlock(
382
+ name: string,
383
+ env: string[],
384
+ segments: Segment[],
385
+ ): string {
356
386
  // The alias body lives inside outer single quotes:
357
- // alias <name>='claude … '
358
- // Each segment becomes one shell-arg inside that body. Composite segments
387
+ // alias <name>='NAME=VALUE claude … '
388
+ // Env assignments lead, so the shell exports them to `claude` alone. Each
389
+ // segment becomes one shell-arg inside that body. Composite segments
359
390
  // (templated values) interleave literal text with double-quoted shell
360
391
  // substitutions via close-outer + double-quote + reopen-outer.
392
+ const prefix =
393
+ env.length === 0 ? "" : `${env.map(quoteInsidePosixAlias).join(" ")} `;
361
394
  const body =
362
395
  segments.length === 0
363
396
  ? ""
364
397
  : ` ${segments.map(renderPosixSegment).join(" ")}`;
365
- const line = `alias ${name}='claude${body}'`;
398
+ const line = `alias ${name}='${prefix}claude${body}'`;
366
399
  return `${BLOCK_BEGIN}\n${line}\n${BLOCK_END}\n`;
367
400
  }
368
401
 
@@ -425,13 +458,22 @@ export function findUnquotableTokens(segments: Segment[]): string[] {
425
458
  return out;
426
459
  }
427
460
 
428
- function renderFishBlock(name: string, segments: Segment[]): string {
461
+ function renderFishBlock(
462
+ name: string,
463
+ env: string[],
464
+ segments: Segment[],
465
+ ): string {
429
466
  // Fish: `alias <name> 'claude --foo bar'` — outer single quotes, no `=`.
467
+ // Env assignments go through `env NAME=VALUE claude …`: the bare
468
+ // `NAME=VALUE cmd` form needs fish 3.1, `env` works on every version. The
469
+ // assignments are catalog constants of `[A-Za-z0-9_=]`, so they need no
470
+ // quoting inside the outer single quotes.
471
+ const prefix = env.length === 0 ? "" : `env ${env.join(" ")} `;
430
472
  const body =
431
473
  segments.length === 0
432
474
  ? ""
433
475
  : ` ${segments.map(renderFishSegment).join(" ")}`;
434
- const line = `alias ${name} 'claude${body}'`;
476
+ const line = `alias ${name} '${prefix}claude${body}'`;
435
477
  return `${BLOCK_BEGIN}\n${line}\n${BLOCK_END}\n`;
436
478
  }
437
479
 
@@ -604,9 +646,33 @@ export function validateConfig(config: AliasConfig): FlagValidationIssue[] {
604
646
 
605
647
  export interface ParseResult {
606
648
  aliasName: string | null;
649
+ /**
650
+ * The env assignments before `claude` (`NAME=VALUE`), then the argv after
651
+ * it — the shape {@link argsToFlagValuesWithLeftovers} reads.
652
+ */
607
653
  args: string[];
608
654
  }
609
655
 
656
+ /** A shell environment assignment token: `NAME=VALUE`. */
657
+ const ENV_ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
658
+
659
+ /**
660
+ * Split an alias command into the tokens {@link argsToFlagValuesWithLeftovers}
661
+ * reads: leading env assignments, then the argv after `claude`. Accepts the
662
+ * POSIX `NAME=VALUE claude …` form and the `env NAME=VALUE claude …` form the
663
+ * fish renderer writes. Returns null when the command is not `claude`.
664
+ */
665
+ function commandTokensAfterEnv(tokens: string[]): string[] | null {
666
+ let i = tokens[0] === "env" ? 1 : 0;
667
+ const env: string[] = [];
668
+ while (i < tokens.length && ENV_ASSIGNMENT_RE.test(tokens[i])) {
669
+ env.push(tokens[i]);
670
+ i += 1;
671
+ }
672
+ if (tokens[i] !== "claude") return null;
673
+ return [...env, ...tokens.slice(i + 1)];
674
+ }
675
+
610
676
  /**
611
677
  * Find the managed block in shell rc text, parse the alias line, and return
612
678
  * the alias name plus the argv tokens after `claude`.
@@ -644,10 +710,11 @@ export function parseAliasLine(line: string): ParseResult | null {
644
710
  const match = ALIAS_LINE_RE.exec(line);
645
711
  if (!match) return null;
646
712
  const [, aliasName, body] = match;
647
- // First token is the command itself (claude). Drop it; we only want argv.
648
- const tokens = tokenizePosixAliasBody(body);
649
- if (tokens.length === 0 || tokens[0] !== "claude") return null;
650
- return { aliasName, args: tokens.slice(1) };
713
+ // The command is `claude`, optionally preceded by env assignments. Drop
714
+ // `claude` itself; keep the assignments ahead of the argv.
715
+ const args = commandTokensAfterEnv(tokenizePosixAliasBody(body));
716
+ if (!args) return null;
717
+ return { aliasName, args };
651
718
  }
652
719
 
653
720
  /**
@@ -793,13 +860,30 @@ export interface ParsedFlagsWithLeftovers {
793
860
  * catalog flag. Unknown `--flag value` pairs contribute BOTH tokens to
794
861
  * `leftovers` (the flag and its consumed value) so the adoption gate sees the
795
862
  * full extent of what wouldn't round-trip.
863
+ *
864
+ * Leading `NAME=VALUE` tokens are the env assignments set before `claude`.
865
+ * Each matches an env flag by its assignment string; an assignment the
866
+ * catalog does not know lands in `leftovers` on its own, so adoption never
867
+ * removes a line it cannot reproduce. Env flags are matched only in that
868
+ * leading run, never among the argv after it.
869
+ *
870
+ * A select env flag is one setting written as an assignment plus
871
+ * `flag <value>`. Its assignment alone reads as the option Claude Code uses
872
+ * when no value is given (`auto`); its argv alone lands in `leftovers`,
873
+ * because the pair is the only form the writer reproduces.
796
874
  */
797
875
  export function argsToFlagValuesWithLeftovers(
798
876
  args: string[],
799
877
  ): ParsedFlagsWithLeftovers {
800
- // Index every flag by its primary `flag` string AND its triStateOff variant.
878
+ // Index argv flags by their primary `flag` string AND their triStateOff
879
+ // variant, and env flags by their assignment string.
801
880
  const byFlag = new Map<string, { flag: AliasFlag; off?: boolean }>();
881
+ const byEnv = new Map<string, AliasFlag>();
802
882
  for (const flag of ALIAS_FLAGS) {
883
+ if (flag.env) {
884
+ byEnv.set(flag.env, flag);
885
+ if (isEnvOnly(flag)) continue;
886
+ }
803
887
  byFlag.set(flag.flag, { flag, off: false });
804
888
  if (flag.triStateOff) {
805
889
  byFlag.set(flag.triStateOff, { flag, off: true });
@@ -808,7 +892,17 @@ export function argsToFlagValuesWithLeftovers(
808
892
 
809
893
  const out: Record<string, FlagValue> = {};
810
894
  const leftovers: string[] = [];
895
+ const envSeen = new Set<string>();
811
896
  let i = 0;
897
+ while (i < args.length && ENV_ASSIGNMENT_RE.test(args[i])) {
898
+ const envFlag = byEnv.get(args[i]);
899
+ if (envFlag) envSeen.add(envFlag.id);
900
+ else leftovers.push(args[i]);
901
+ if (envFlag && isEnvOnly(envFlag)) {
902
+ out[envFlag.id] = { kind: "boolean", enabled: true };
903
+ }
904
+ i += 1;
905
+ }
812
906
  while (i < args.length) {
813
907
  const tok = args[i];
814
908
  const entry = byFlag.get(tok);
@@ -962,6 +1056,20 @@ export function argsToFlagValuesWithLeftovers(
962
1056
  };
963
1057
  }
964
1058
 
1059
+ // Pair each select env flag's assignment with its argv.
1060
+ for (const flag of ALIAS_FLAGS) {
1061
+ if (!flag.env || isEnvOnly(flag)) continue;
1062
+ const parsed = out[flag.id];
1063
+ const hasArgv = parsed !== undefined;
1064
+ if (envSeen.has(flag.id) && !hasArgv) {
1065
+ out[flag.id] = { kind: "select", enabled: true, value: "auto" };
1066
+ } else if (!envSeen.has(flag.id) && hasArgv) {
1067
+ delete out[flag.id];
1068
+ const value = parsed.kind === "select" ? parsed.value : "";
1069
+ leftovers.push(flag.flag, ...(value ? [value] : []));
1070
+ }
1071
+ }
1072
+
965
1073
  return { flags: out, leftovers };
966
1074
  }
967
1075
 
@@ -1122,9 +1230,9 @@ function parseAdoptableLine(
1122
1230
  if (!match) return null;
1123
1231
  const [, name, , body] = match;
1124
1232
  const tokens = tokenizeGeneralAliasBody(body);
1125
- if (!tokens || tokens.length === 0 || tokens[0] !== "claude") return null;
1126
-
1127
- const args = tokens.slice(1);
1233
+ if (!tokens) return null;
1234
+ const args = commandTokensAfterEnv(tokens);
1235
+ if (!args) return null;
1128
1236
  const { flags, leftovers } = argsToFlagValuesWithLeftovers(args);
1129
1237
 
1130
1238
  // Fixpoint: render the parsed flags back to tokens and re-parse. If the
@@ -61,6 +61,11 @@ export function InputModal({
61
61
  if (typeof value === "string") onSubmit(value);
62
62
  }}
63
63
  focused
64
+ // OpenTUI paints input text white by default, which vanishes on a
65
+ // light terminal. The terminal's own foreground reads on both.
66
+ textColor={theme.colors.text}
67
+ focusedTextColor={theme.colors.text}
68
+ placeholderColor={theme.colors.muted}
64
69
  width={50}
65
70
  />
66
71
  </box>
@@ -7,6 +7,7 @@ import {
7
7
  ALIAS_FLAGS,
8
8
  type AliasFlag,
9
9
  FLAG_GROUPS,
10
+ getFlagById,
10
11
  } from "../../data/alias-flags.js";
11
12
  import { loadAliasName, saveAliasName } from "../../services/alias-settings.js";
12
13
  import {
@@ -18,6 +19,7 @@ import {
18
19
  findAdoptableAlias,
19
20
  parseAliasFromRc,
20
21
  renderArgs,
22
+ renderEnv,
21
23
  validateConfig,
22
24
  writeAliasToShell,
23
25
  } from "../../services/alias-shell-writer.js";
@@ -521,9 +523,10 @@ export function AliasScreen() {
521
523
  selectedRow.flag,
522
524
  selectedRow.value,
523
525
  modal,
526
+ showFailures,
524
527
  );
525
528
  if (next) updateFlag(selectedRow.flag.id, next);
526
- }, [selectedRow, modal, updateFlag, editAliasName]);
529
+ }, [selectedRow, modal, showFailures, updateFlag, editAliasName]);
527
530
 
528
531
  const toggleSelected = useCallback(async () => {
529
532
  if (!selectedRow) return;
@@ -680,6 +683,7 @@ export function AliasScreen() {
680
683
 
681
684
  // ── Render ────────────────────────────────────────────────────────────
682
685
  const previewSegments = config ? renderArgs(config) : [];
686
+ const previewEnv = config ? renderEnv(config) : [];
683
687
  const previewAliasName = config?.aliasName ?? "";
684
688
 
685
689
  // Native OpenTUI scrollboxes own the scroll math. We render every item
@@ -767,6 +771,7 @@ export function AliasScreen() {
767
771
  selectedRow,
768
772
  isGated,
769
773
  previewAliasName,
774
+ previewEnv,
770
775
  previewSegments,
771
776
  );
772
777
  // When editing --channels and there are derived rows, the right pane carries
@@ -795,6 +800,7 @@ export function AliasScreen() {
795
800
  detailFlag,
796
801
  detailValue,
797
802
  previewAliasName,
803
+ previewEnv,
798
804
  previewSegments,
799
805
  derivedNote,
800
806
  stateOverride,
@@ -1081,6 +1087,7 @@ async function editFlagValue(
1081
1087
  flag: AliasFlag,
1082
1088
  value: FlagValue,
1083
1089
  modal: ReturnType<typeof useModal>,
1090
+ showFailures: ReturnType<typeof useFailurePopup>,
1084
1091
  ): Promise<FlagValue | null> {
1085
1092
  switch (value.kind) {
1086
1093
  case "boolean":
@@ -1154,6 +1161,17 @@ async function editFlagValue(
1154
1161
  }
1155
1162
  const v = await modal.input(flag.label, flag.description, value.value);
1156
1163
  if (v === null) return null;
1164
+ // An empty value disables the flag, so only a non-empty one is checked.
1165
+ // A refused value leaves the old one in place.
1166
+ const reason = v.length > 0 ? (flag.validateValue?.(v) ?? null) : null;
1167
+ if (reason) {
1168
+ await showFailures(
1169
+ failureReport(`The ${flag.label.toLowerCase()} was not changed`, [
1170
+ { subject: v, reason },
1171
+ ]),
1172
+ );
1173
+ return null;
1174
+ }
1157
1175
  return { kind: "text", enabled: v.length > 0, value: v };
1158
1176
  }
1159
1177
 
@@ -1336,6 +1354,7 @@ function renderDetail(
1336
1354
  row: Row | undefined,
1337
1355
  isGated: (f: AliasFlag) => boolean,
1338
1356
  aliasName: string,
1357
+ env: string[],
1339
1358
  segments: Segment[],
1340
1359
  ): React.ReactNode {
1341
1360
  if (!row) {
@@ -1357,7 +1376,7 @@ function renderDetail(
1357
1376
  {headerAndBody}
1358
1377
  <text fg={theme.colors.text}> </text>
1359
1378
  <text fg={theme.colors.muted}>Preview:</text>
1360
- {renderPreviewLines(aliasName, segments)}
1379
+ {renderPreviewLines(aliasName, env, segments)}
1361
1380
  </box>
1362
1381
  );
1363
1382
  }
@@ -1407,7 +1426,7 @@ function renderFlagDetail(
1407
1426
  </text>
1408
1427
  {gated && (
1409
1428
  <text fg={theme.colors.muted}>
1410
- {`Depends on --${row.flag.requires}. Enabling this will turn that on too.`}
1429
+ {`Depends on ${getFlagById(row.flag.requires ?? "").flag}. Enabling this will turn that on too.`}
1411
1430
  </text>
1412
1431
  )}
1413
1432
  {row.issue && (
@@ -1438,18 +1457,23 @@ function renderFlagDetail(
1438
1457
  */
1439
1458
  function renderPreviewLines(
1440
1459
  aliasName: string,
1460
+ env: string[],
1441
1461
  segments: Segment[],
1442
1462
  ): React.ReactNode {
1463
+ // Env assignments run before `claude`, so they sit on the head line.
1464
+ const command = [...env, "claude"].join(" ");
1443
1465
  if (segments.length === 0) {
1444
1466
  return (
1445
- <text fg={theme.colors.text}>{`alias ${aliasName || "?"}='claude'`}</text>
1467
+ <text
1468
+ fg={theme.colors.text}
1469
+ >{`alias ${aliasName || "?"}='${command}'`}</text>
1446
1470
  );
1447
1471
  }
1448
1472
  const lines: React.ReactNode[] = [
1449
1473
  <text
1450
1474
  key="head"
1451
1475
  fg={theme.colors.text}
1452
- >{`alias ${aliasName || "?"}='claude`}</text>,
1476
+ >{`alias ${aliasName || "?"}='${command}`}</text>,
1453
1477
  ];
1454
1478
  segments.forEach((seg, i) => {
1455
1479
  lines.push(
@@ -1530,6 +1554,7 @@ function renderFlagDetailRightPane(
1530
1554
  flag: AliasFlag,
1531
1555
  value: FlagValue,
1532
1556
  aliasName: string,
1557
+ env: string[],
1533
1558
  segments: Segment[],
1534
1559
  derivedNote?: string,
1535
1560
  stateOverride?: string,
@@ -1555,7 +1580,7 @@ function renderFlagDetailRightPane(
1555
1580
  </text>
1556
1581
  <text fg={theme.colors.text}> </text>
1557
1582
  <text fg={theme.colors.muted}>Preview:</text>
1558
- {renderPreviewLines(aliasName, segments)}
1583
+ {renderPreviewLines(aliasName, env, segments)}
1559
1584
  <text fg={theme.colors.text}> </text>
1560
1585
  <text fg={theme.colors.muted}>Keys:</text>
1561
1586
  <text fg={theme.colors.text}>