tokenmaxxing 1.7.0 → 1.9.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.
Files changed (67) hide show
  1. package/DESIGN.md +2 -2
  2. package/README.md +1 -1
  3. package/agent-plugin/plugin.json +8 -2
  4. package/package.json +1 -1
  5. package/src/cli/add.ts +1 -8
  6. package/src/cli/auth.ts +0 -23
  7. package/src/cli/check.ts +19 -13
  8. package/src/cli/codexadd.ts +0 -17
  9. package/src/cli/codexinit.ts +11 -42
  10. package/src/cli/codexrm.ts +0 -13
  11. package/src/cli/codexswitch.ts +0 -15
  12. package/src/cli/config.ts +0 -30
  13. package/src/cli/doctor.ts +1 -14
  14. package/src/cli/init.ts +8 -34
  15. package/src/cli/ls.ts +0 -2
  16. package/src/cli/onboard.ts +0 -37
  17. package/src/cli/rename.ts +0 -19
  18. package/src/cli/render.ts +0 -23
  19. package/src/cli/rm.ts +0 -19
  20. package/src/cli/status.ts +0 -80
  21. package/src/cli/switch.ts +1 -49
  22. package/src/cli/watch.ts +0 -17
  23. package/src/entries/codexstophook.ts +2 -63
  24. package/src/entries/codexsupervisor.ts +1 -67
  25. package/src/entries/mcp.ts +0 -11
  26. package/src/entries/sessionstart.ts +1 -8
  27. package/src/entries/statusline.ts +0 -66
  28. package/src/entries/stopfailurehook.ts +93 -0
  29. package/src/entries/stophook.ts +3 -23
  30. package/src/entries/subagentstatusline.ts +0 -19
  31. package/src/entries/supervisor.ts +32 -132
  32. package/src/lib/atomic.ts +0 -16
  33. package/src/lib/claudebin.ts +4 -55
  34. package/src/lib/claudejson.ts +0 -10
  35. package/src/lib/claudelock.ts +13 -35
  36. package/src/lib/codexauth.ts +0 -29
  37. package/src/lib/codexbin.ts +0 -10
  38. package/src/lib/codexdecide.ts +1 -112
  39. package/src/lib/codexoauth.ts +0 -16
  40. package/src/lib/codexpick.ts +0 -31
  41. package/src/lib/codexpresence.ts +0 -35
  42. package/src/lib/codexsample.ts +0 -23
  43. package/src/lib/codexstate.ts +0 -7
  44. package/src/lib/codexswap.ts +0 -32
  45. package/src/lib/codexusage.ts +0 -28
  46. package/src/lib/credstore.ts +0 -24
  47. package/src/lib/decide.ts +127 -180
  48. package/src/lib/http.ts +0 -9
  49. package/src/lib/install.ts +57 -124
  50. package/src/lib/keychain.ts +1 -39
  51. package/src/lib/lock.ts +0 -24
  52. package/src/lib/log.ts +0 -14
  53. package/src/lib/oauth.ts +1 -31
  54. package/src/lib/paths.ts +1 -45
  55. package/src/lib/picker.ts +1 -84
  56. package/src/lib/proc.ts +0 -17
  57. package/src/lib/sample.ts +0 -68
  58. package/src/lib/sessions.ts +0 -13
  59. package/src/lib/settings.ts +15 -42
  60. package/src/lib/state.ts +23 -77
  61. package/src/lib/swap.ts +3 -87
  62. package/src/lib/tty.ts +0 -4
  63. package/src/lib/types.ts +13 -140
  64. package/src/lib/usage.ts +108 -196
  65. package/src/lib/worktree.ts +0 -8
  66. package/src/main.ts +5 -35
  67. package/src/sdk.ts +0 -59
@@ -1,22 +1,3 @@
1
- // Native statusLine. Reads Claude's statusLine stdin, tees the rate-limit data
2
- // to usage.json (write-on-change, O(ms)), then renders ONE line:
3
- // worktree name (linked worktrees only), model name painted by context fill
4
- // (effort in parens), +added/-removed, then quota as COLOR (user decisions
5
- // 2026-07-18): each window is its time-to-reset painted on a continuous
6
- // green->yellow->red ramp by used% - "◆ 𝒇 2h 1d ◇ 2d ◇ full" - the
7
- // active account's windows after a green ◆ (per-model by bare initial
8
- // first, fable as 𝒇, other families uppercased; "𝒇?" unpainted when the
9
- // cap applies but is unmeasured, then session/5h, then week), then EVERY
10
- // parked account's week after its own cyan ◇ (red ✗ if needs-reauth),
11
- // sorted by earliest upcoming reset (needs-reauth last). A window with no
12
- // upcoming reset renders "0" (empty again); measured usage with an unknown
13
- // reset clock renders a painted "?". With color disabled the numeric format
14
- // returns ("2h5" = resets in 2h, 5 used; "ctx 42"): without color a drained
15
- // window must not look fresh. Blocks are joined by TWO spaces, tokens
16
- // within a block by one. Per-model resets are omitted (they match the
17
- // weekly reset).
18
- // Must NEVER break the status line: render what parses, skip what doesn't.
19
-
20
1
  import { sortBy } from "es-toolkit";
21
2
  import { z } from "zod";
22
3
  import { readOAuthAccount } from "../lib/claudejson.ts";
@@ -36,27 +17,16 @@ import {
36
17
  type UsageWindow,
37
18
  } from "../lib/types.ts";
38
19
 
39
- /** A session that hasn't adopted a fresh swap yet (<=30s keychain cache) pushes
40
- * the OLD account's windows while ~/.claude.json already names the NEW org.
41
- * Suppress the tee for this long after a swap so that mislabel never lands. */
42
20
  const ADOPTION_GRACE_MS = 45_000;
43
21
 
44
22
  const RenderCtxSchema = z.object({
45
23
  accounts: AccountsIndexSchema,
46
- /** active account's per-model weekly windows ({} unless fresh for the live org). */
47
24
  perModel: z.record(z.string(), UsageWindowSchema),
48
- /** families (lowercased) whose per-model weekly cap gates a switch. */
49
25
  switchModels: z.array(z.string()),
50
- /** linked-worktree basename, null in a main checkout. */
51
26
  worktree: z.string().nullable(),
52
- /** the LIVE login's org from claude.json, the seat's identity - the
53
- * activeAccountUuid label drifts after a manual /login (the same rule as
54
- * decide.ts's seatOf; closing-review catch: the label-keyed split rendered
55
- * the live account twice and hid the stale-labeled one). */
56
27
  liveOrg: z.string().nullable(),
57
28
  now: z.number(),
58
29
  color: z.boolean(),
59
- /** terminal advertises 24-bit color (COLORTERM); false steps the ramp to the 256-color cube. */
60
30
  truecolor: z.boolean(),
61
31
  });
62
32
  export type RenderCtx = z.infer<typeof RenderCtxSchema>;
@@ -67,22 +37,17 @@ export async function readStdin(): Promise<string> {
67
37
  return Buffer.concat(chunks).toString("utf8");
68
38
  }
69
39
 
70
- /** Pure renderer: statusLine stdin + tokenmaxxing state → the emitted line. */
71
40
  export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
72
41
  const col = makeColors(ctx.color);
73
42
  const paint = makeUsagePaint({ enabled: ctx.color, truecolor: ctx.truecolor });
74
- // Used quota in a window; one whose reset has passed is empty again.
75
43
  const used = (w: UsageWindow) => (w.resetsAt != null && w.resetsAt <= ctx.now ? 0 : w.usedPercentage);
76
44
  const reset = (epochMs: number | null) => fmtResetShort(epochMs, ctx.now);
77
45
 
78
46
  const parsed = StatusLineStdinSchema.safeParse(stdinObj);
79
47
  const d = parsed.success ? parsed.data : null;
80
48
 
81
- // ---- info block: worktree, model, ctx, diff
82
49
  const info: string[] = [];
83
50
  if (ctx.worktree != null) info.push(ctx.worktree);
84
- // The model name carries the context-fill color (user rule 2026-07-18, no
85
- // ctx token); colorless mode keeps the numeric ctx token instead.
86
51
  const modelName = d?.model?.display_name ?? d?.model?.id;
87
52
  const ctxUsed = d?.context_window?.used_percentage;
88
53
  if (modelName) {
@@ -93,26 +58,17 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
93
58
  if (!ctx.color && ctxUsed != null) info.push(`ctx ${Math.round(ctxUsed)}`);
94
59
  const added = d?.cost?.total_lines_added ?? 0;
95
60
  const removed = d?.cost?.total_lines_removed ?? 0;
96
- // -removed stays unpainted: red means quota alarm and nothing else.
97
61
  if (added > 0 || removed > 0) info.push(`${col.green(`+${added}`)}/-${removed}`);
98
62
 
99
- // ---- active account block
100
- // A window token: color carries the used%, the text is the reset countdown
101
- // (or the bare per-model initial - per-model resets are omitted). "0" = no
102
- // upcoming reset (the window is empty again); a painted "?" = measured usage
103
- // whose reset clock is unknown. Colorless mode glues the number back on.
104
63
  const seg = (label: string, w: UsageWindow, resetAt: number | null) => {
105
64
  const u = used(w);
106
65
  if (!ctx.color) return `${label}${reset(resetAt)}${Math.round(u)}`;
107
66
  const body = label !== "" ? label : reset(resetAt);
108
67
  return col.bold(paint(u)(body !== "" ? body : u > 0 ? "?" : "0"));
109
68
  };
110
- // Per-model initial: fable renders 𝒇 (user rule 2026-07-18), family-matched
111
- // as always; other families keep their uppercased first letter.
112
69
  const initial = (name: string) => (familyTokens(name).includes("fable") ? "𝒇" : name.slice(0, 1).toUpperCase());
113
70
  const wins = parseStatusLineStdin(stdinObj);
114
71
  const windows: string[] = [];
115
- // A capacity-constrained model whose cap is unmeasured must not look safe.
116
72
  const family = matchedFamily(parseStatusLineModel(stdinObj), ctx.switchModels);
117
73
  if (family && !Object.keys(ctx.perModel).some((k) => familyTokens(k).includes(family))) {
118
74
  windows.push(`${initial(family)}?`);
@@ -122,7 +78,6 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
122
78
  windows.push(seg("", wins.fiveHour, wins.fiveHour.resetsAt));
123
79
  windows.push(seg("", wins.sevenDay, wins.sevenDay.resetsAt));
124
80
  }
125
- // The seat: live-org first, stored label fallback (unknown live identity).
126
81
  const seatUuid =
127
82
  (ctx.liveOrg != null ? ctx.accounts.accounts.find((a) => a.organizationUuid === ctx.liveOrg)?.accountUuid : undefined) ??
128
83
  ctx.accounts.activeAccountUuid;
@@ -133,13 +88,10 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
133
88
  ? `${col.green("◆")} ?`
134
89
  : "";
135
90
 
136
- // ---- parked accounts, earliest upcoming reset first (needs-reauth last)
137
91
  const parked = sortBy(
138
92
  ctx.accounts.accounts.filter((a) => a.accountUuid !== seatUuid),
139
93
  [(a) => (a.needsReauth ? 1 : 0), (a) => earliestReset(a, ctx.now)],
140
94
  );
141
- // Every parked account renders its own marker (user rule 2026-07-18: the
142
- // old counted collapse "◇ 3 full" hid the pool size and read as confusing).
143
95
  const poolSeg = (a: Account): string => {
144
96
  const marker = a.needsReauth ? col.red("✗") : col.cyan("◇");
145
97
  const week = a.lastUsage?.sevenDay;
@@ -148,7 +100,6 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
148
100
  if (Math.round(weekUsed) <= 0) return `${marker} ${paint(0)("full")}`;
149
101
 
150
102
  const parts: string[] = [];
151
- // A per-model weekly cap with more used than the aggregate is the binding constraint - surface it.
152
103
  for (const [name, w] of Object.entries(a.lastPerModel ?? {})) {
153
104
  if (used(w) > weekUsed) parts.push(seg(initial(name), w, null));
154
105
  }
@@ -166,25 +117,11 @@ export async function runStatusline(): Promise<number> {
166
117
  try {
167
118
  obj = JSON.parse(raw);
168
119
  } catch {
169
- // malformed stdin - render from state alone
170
120
  }
171
121
  const now = Date.now();
172
122
 
173
- // The stdin payload's own org label: those rate_limits and that
174
- // organizationUuid ride the SAME API response, so the label can never lie
175
- // about whose windows these are. claude.json's org is only the fallback -
176
- // it flips at the swap while a session that has made no post-swap request
177
- // keeps rendering the OLD account's windows indefinitely, and the 45s
178
- // ADOPTION_GRACE_MS bounds nothing for such a session (closing-review
179
- // catch: a stale-window tee labeled with the new org could hard-swap a
180
- // healthy account and stamp foreign usage into it). The render below uses
181
- // the same preference so the active ◆ seat matches the windows painted
182
- // beside it (PR #36 review catch). Trusted ONLY when the payload's windows
183
- // parsed too: an org label without windows would re-label state the payload
184
- // did not carry (second-round catch).
185
123
  let stdinOrg: string | null = null;
186
124
 
187
- // tee usage for the Stop hook / status - best effort, never blocks rendering.
188
125
  let org: string | null = null;
189
126
  try {
190
127
  org = readOAuthAccount()?.organizationUuid ?? null;
@@ -197,7 +134,6 @@ export async function runStatusline(): Promise<number> {
197
134
  writeUsage(state);
198
135
  }
199
136
  } catch {
200
- // skip the tee, still render below
201
137
  }
202
138
 
203
139
  let line: string;
@@ -219,8 +155,6 @@ export async function runStatusline(): Promise<number> {
219
155
  };
220
156
  line = renderStatusline(obj, ctx);
221
157
  } catch (e) {
222
- // Corrupt local state (the loaders throw on it) must stay VISIBLE: render
223
- // the failure as the statusline itself, never abort into a blank line.
224
158
  line = `tokenmaxxing: ${e instanceof Error ? e.message : String(e)}`;
225
159
  }
226
160
  process.stdout.write(line + "\n");
@@ -0,0 +1,93 @@
1
+ import { join } from "node:path";
2
+ import { z } from "zod";
3
+ import { paths } from "../lib/paths.ts";
4
+ import { writeFileAtomic } from "../lib/atomic.ts";
5
+ import { readOAuthAccount } from "../lib/claudejson.ts";
6
+ import { enforcedWindowMs, evaluateAndMaybeSwap, postSwapProof, recordEnforcedLimit } from "../lib/decide.ts";
7
+ import { loadConfig, loadLastSwapAt } from "../lib/state.ts";
8
+ import { classifyEnforcedLimit, findEnforcedRow, parseErrorBody, readTranscriptTail } from "../lib/usage.ts";
9
+ import { RespawnMarkerSchema, type EnforcedLimit } from "../lib/types.ts";
10
+ import { log } from "../lib/log.ts";
11
+
12
+ export const RETRIGGER_PROMPT = "Continue where the previous turn left off; it was interrupted by a usage limit and tokenmaxxing switched accounts.";
13
+
14
+ const StopFailureStdin = z.looseObject({
15
+ session_id: z.uuid().optional().catch(undefined),
16
+ transcript_path: z.string().optional().catch(undefined),
17
+ error: z.string().optional().catch(undefined),
18
+ agent_id: z.string().optional().catch(undefined),
19
+ last_assistant_message: z.string().optional().catch(undefined),
20
+ });
21
+
22
+ const LaunchedAtSchema = z.coerce.number().finite().optional().catch(undefined);
23
+
24
+ async function readStdin(): Promise<string> {
25
+ const chunks: Uint8Array[] = [];
26
+ for await (const c of Bun.stdin.stream()) chunks.push(c);
27
+ return Buffer.concat(chunks).toString("utf8");
28
+ }
29
+
30
+ export async function runStopFailureHook(): Promise<number> {
31
+ if (process.env.TOKENMAXXING_PROBE) return 0;
32
+
33
+ const org = readOAuthAccount()?.organizationUuid ?? null;
34
+ const now = Date.now();
35
+ const raw = await readStdin();
36
+ const parsed = StopFailureStdin.safeParse((() => { try { return JSON.parse(raw); } catch { return {}; } })());
37
+ const stdin = parsed.success ? parsed.data : {};
38
+ if (stdin.error !== undefined && stdin.error !== "rate_limit") return 0;
39
+
40
+ const stdinSid = stdin.session_id;
41
+ const pinnedSid = process.env.TOKENMAXXING_SESSION_ID;
42
+ const launchedAt = LaunchedAtSchema.parse(process.env.TOKENMAXXING_LAUNCHED_AT) ?? null;
43
+ const mainLoop = stdin.agent_id === undefined;
44
+ const canPause = process.env.TOKENMAXXING_SUPERVISED === "1" && pinnedSid != null && mainLoop;
45
+
46
+ try {
47
+ const cfg = loadConfig();
48
+ const found = stdin.transcript_path
49
+ ? findEnforcedRow({ rows: readTranscriptTail(stdin.transcript_path), lastAssistantMessage: stdin.last_assistant_message, now })
50
+ : null;
51
+ const limit = found ? classifyEnforcedLimit(found.row, cfg.policy.switchModels) : null;
52
+
53
+ let enforced: EnforcedLimit | null = null;
54
+ if (limit && found && org) {
55
+ if (postSwapProof({ swapAt: loadLastSwapAt(), launchedAt, errorAt: found.errorAt, now })) {
56
+ const stamp = await recordEnforcedLimit({ limit, org, now });
57
+ log("stopfailure.enforced", { kind: limit.kind, family: limit.kind === "model" ? limit.family : undefined, outcome: stamp.outcome, resetsAt: stamp.resetsAt, subagent: !mainLoop });
58
+ if (stamp.outcome !== "org-moved") {
59
+ enforced = { org, family: limit.kind === "model" ? limit.family : null, resetsAt: stamp.resetsAt, windowMs: enforcedWindowMs(limit) };
60
+ }
61
+ } else {
62
+ log("stopfailure.unproven", { kind: limit.kind });
63
+ }
64
+ } else {
65
+ log("stopfailure.unclassified", {
66
+ row: found != null,
67
+ type: found?.row.quotaLimits?.rateLimitType,
68
+ transient: found?.row.apiErrorIsTransient,
69
+ body: found ? parseErrorBody(found.row.errorDetails)?.error?.type : undefined,
70
+ });
71
+ }
72
+
73
+ const decision = await evaluateAndMaybeSwap(now, canPause && enforced != null, enforced);
74
+ if (enforced && canPause && pinnedSid && decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
75
+ const marker = join(paths.respawnDir, pinnedSid);
76
+ const payload = RespawnMarkerSchema.parse({
77
+ account: decision.account.label,
78
+ ts: Date.now(),
79
+ waitUntil: decision.waitUntil ?? now,
80
+ sessionId: stdinSid ?? pinnedSid,
81
+ prompt: RETRIGGER_PROMPT,
82
+ ...(launchedAt != null ? { launchedAt } : {}),
83
+ });
84
+ writeFileAtomic(marker, JSON.stringify(payload));
85
+ log("stopfailure.marker", { session: (stdinSid ?? pinnedSid).slice(0, 8), account: decision.account.accountUuid.slice(0, 8), waitUntil: payload.waitUntil });
86
+ } else {
87
+ log("stopfailure.decision", { reason: decision.reason, swapped: decision.swapped, account: decision.account?.accountUuid.slice(0, 8), waitUntil: decision.waitUntil });
88
+ }
89
+ } catch (e) {
90
+ log("stopfailure.error", { err: e instanceof Error ? e.message : String(e) });
91
+ }
92
+ return 0;
93
+ }
@@ -1,11 +1,3 @@
1
- // Stop hook. Fires when claude finishes a turn (transcript already committed).
2
- // A plain swap needs no respawn: the running session adopts the swapped
3
- // credential on its own (<=30s on macOS, next request on Linux). Only a
4
- // depleted-pool wait - when running under the supervisor - drops a respawn
5
- // marker keyed by this session id: the supervisor SIGTERMs its child at this
6
- // clean boundary, counts down to the reset, then relaunches `--resume`. We
7
- // never kill claude ourselves.
8
-
9
1
  import { join } from "node:path";
10
2
  import { z } from "zod";
11
3
  import { paths } from "../lib/paths.ts";
@@ -14,10 +6,6 @@ import { evaluateAndMaybeSwap } from "../lib/decide.ts";
14
6
  import { RespawnMarkerSchema } from "../lib/types.ts";
15
7
  import { log } from "../lib/log.ts";
16
8
 
17
- // session_id must be a real transcript UUID: a malformed value would ride the
18
- // respawn marker into `--resume <garbage>`, which claude treats as a picker
19
- // search term (PR #36 review catch); non-UUID input drops to undefined and the
20
- // marker falls back to the pinned sid.
21
9
  const StopStdin = z.looseObject({ session_id: z.uuid().optional().catch(undefined) });
22
10
 
23
11
  async function readStdin(): Promise<string> {
@@ -27,35 +15,27 @@ async function readStdin(): Promise<string> {
27
15
  }
28
16
 
29
17
  export async function runStopHook(): Promise<number> {
30
- // recursion guard - our own `-p '/usage'` probe re-enters hooks.
31
18
  if (process.env.TOKENMAXXING_PROBE) return 0;
32
19
 
33
20
  const raw = await readStdin();
34
21
  const parsed = StopStdin.safeParse((() => { try { return JSON.parse(raw); } catch { return {}; } })());
35
- // TWO session ids with different jobs (closing-review HIGH catch): the
36
- // PINNED id (env, set once by the supervisor) names the marker file the
37
- // supervisor actually watches and survives /clear; the STDIN id names the
38
- // CURRENT transcript to resume and drifts to a new value after /clear.
39
- // Keying the file by the stdin id orphaned every post-/clear marker.
40
22
  const stdinSid = parsed.success ? parsed.data.session_id : undefined;
41
23
  const pinnedSid = process.env.TOKENMAXXING_SESSION_ID;
42
24
 
43
25
  try {
44
- // Anticipatory depleted swaps are only sane when the respawn marker below
45
- // will actually pause the session until the reset.
46
26
  const canPause = process.env.TOKENMAXXING_SUPERVISED === "1" && pinnedSid != null;
47
27
  const decision = await evaluateAndMaybeSwap(Date.now(), canPause);
48
28
  if (decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
49
29
  log(decision.swapped ? "stop.swapped" : "stop.wait", { account: decision.account.accountUuid.slice(0, 8), waitUntil: decision.waitUntil });
50
- // Respawn only for a depleted-pool wait: pausing until the reset requires
51
- // killing the child. A plain swap leaves the session running to adopt.
52
30
  if (decision.waitUntil !== undefined && canPause && pinnedSid) {
53
31
  const marker = join(paths.respawnDir, pinnedSid);
32
+ const launchedAt = z.coerce.number().finite().optional().catch(undefined).parse(process.env.TOKENMAXXING_LAUNCHED_AT);
54
33
  const payload = RespawnMarkerSchema.parse({
55
34
  account: decision.account.label,
56
35
  ts: Date.now(),
57
36
  waitUntil: decision.waitUntil,
58
37
  sessionId: stdinSid ?? pinnedSid,
38
+ ...(launchedAt != null ? { launchedAt } : {}),
59
39
  });
60
40
  writeFileAtomic(marker, JSON.stringify(payload));
61
41
  log("stop.marker", { session: (stdinSid ?? pinnedSid).slice(0, 8) });
@@ -64,5 +44,5 @@ export async function runStopHook(): Promise<number> {
64
44
  } catch (e) {
65
45
  log("stop.error", { err: e instanceof Error ? e.message : String(e) });
66
46
  }
67
- return 0; // never block the stop
47
+ return 0;
68
48
  }
@@ -1,16 +1,3 @@
1
- // Renders claude's subagentStatusLine: one {id, content} JSON line per active
2
- // subagent task, so the agents panel shows each subagent's model, effort, and
3
- // ctx fill the way the main statusline shows the session's (user ask
4
- // 2026-07-18). This per-row panel surface is the only place subagent info can
5
- // appear: the main statusLine command never learns which subagent the UI is
6
- // viewing (verified against the 2.1.214 bundle). Row shape mirrors the main
7
- // info block, info first so claude's end-truncation eats the label instead:
8
- // "fable (high) <task label>" - the family name painted by the task's
9
- // context fill (colorless mode keeps a numeric ctx token).
10
- // A task we emit nothing for keeps claude's default row (emitting empty
11
- // content would HIDE it). Must NEVER break the panel: render what parses,
12
- // skip what doesn't.
13
-
14
1
  import { z } from "zod";
15
2
  import { makeColors, makeUsagePaint } from "../cli/render.ts";
16
3
  import { readStdin } from "./statusline.ts";
@@ -19,14 +6,11 @@ import { SubagentStatusLineStdinSchema } from "../lib/types.ts";
19
6
  const RowCtxSchema = z.object({ color: z.boolean(), truecolor: z.boolean() });
20
7
  export type RowCtx = z.infer<typeof RowCtxSchema>;
21
8
 
22
- /** "claude-fable-5" -> "fable" (lowercase family, matching the chart-label
23
- * convention); an id without the claude- prefix passes through whole. */
24
9
  function modelFamily(id: string): string {
25
10
  const [head, family] = id.split("-");
26
11
  return head === "claude" && family ? family : id;
27
12
  }
28
13
 
29
- /** Pure renderer: subagentStatusLine stdin -> {id, content} JSON lines. */
30
14
  export function renderSubagentRows(stdinObj: unknown, ctx: RowCtx): string[] {
31
15
  const parsed = SubagentStatusLineStdinSchema.safeParse(stdinObj);
32
16
  if (!parsed.success) return [];
@@ -41,8 +25,6 @@ export function renderSubagentRows(stdinObj: unknown, ctx: RowCtx): string[] {
41
25
  ? Math.round((t.tokenCount / t.contextWindowSize) * 100)
42
26
  : null;
43
27
  const info: string[] = [];
44
- // The model name carries the task's context-fill color (same rule as the
45
- // main line); colorless mode keeps the numeric ctx token instead.
46
28
  if (t.model != null) {
47
29
  const family = modelFamily(t.model);
48
30
  const body = pct != null ? col.bold(paint(pct)(family)) : col.bold(family);
@@ -66,7 +48,6 @@ export async function runSubagentStatusline(): Promise<number> {
66
48
  try {
67
49
  obj = JSON.parse(raw);
68
50
  } catch {
69
- // malformed stdin - emit nothing, claude keeps its default rows
70
51
  }
71
52
  const colorterm = z.string().optional().parse(process.env.COLORTERM);
72
53
  const rows = renderSubagentRows(obj, {