Shows the main loop's model and its context window fill under the prompt

Configurations managed with Nix.
Follow the fresh Darwin installation guide. The flake has a private SSH input, so a new machine must commission 1Password and verify access to dotfiles-private before the first nix develop invocation.
After installing the prerequisites and initializing the colocated Jujutsu workspace, the activation sequence is:
nix develop --accept-flake-config --command ./x.sh nix-blueprints
nix develop --accept-flake-config --command ./x.sh nix-switch-system --blueprint <blueprint-id>
nix develop --accept-flake-config --command ./x.sh nix-switch-home --blueprint <blueprint-id>
Home Manager creates dotfiles/blueprint-id under the XDG config root (normally ~/.config), so later commands can invoke ./x.sh directly without --blueprint.
./x.sh nix-<cmd>-home [--blueprint <blueprint-id>] - Home Manager: eval/build/diff/activate/switch./x.sh nix-<cmd>-system [--blueprint <blueprint-id>] - system: eval/build/diff/activate/switch./x.sh nix-eval-all - evaluate every canonical blueprint without a marker./x.sh nix-blueprints - list configured blueprint IDs./x.sh fmt - format all files./x.sh fmt-check - check formatting without modifying files./x.sh lint - check Nix, tracked .sh and .bash files, and PythonBlueprint-dependent evaluation, build, and switch commands use an explicit --blueprint first, then the Home Manager-managed blueprint marker. Builds use blueprint-specific result-home-<blueprint-id> and result-system-<blueprint-id> links so activation cannot consume another blueprint's result.
hooks/register.tsx 68 lines1import { atom, read, update } from "claude-code";
2import type {
3 EngineInterface,
4 Register,
5 SessionContextUsage,
6} from "claude-code";
7
8import type { StatuslineLine } from "../types";
9
10// The session's model and context window fill, for example
11// `claude-opus-5-5 │ 42% 84000/200000`, kept for the hint row to draw.
12const line = atom(
13 { plugin: "statusline", key: "line" } as const,
14 null as StatuslineLine,
15);
16
17export const register: Register = (on) => {
18 on("session.start", async ($, e, next) => {
19 const started = await next(e);
20 await refreshLine($, (await $.session.usage()).context);
21 return started;
22 });
23
24 // Refreshes the model after a `/model` switch, which lands between turns.
25 on("turn.start", async ($, e, next) => {
26 const turn = await next(e);
27 await refreshLine($, (await $.session.usage()).context);
28 return turn;
29 });
30
31 on("session.measure", async ($, e, next) => {
32 const measured = await next(e);
33 await refreshLine($, e.context);
34 return measured;
35 });
36
37 // A row of its own under the engine's hint row: a tail on that row is cut at
38 // its end, and dropped entirely on a narrow terminal.
39 on("ui.render", { component: "PromptHint" }, async ($, e, next) => {
40 const hint = await next(e);
41 const text = await read($, line);
42 if (text === null) return hint;
43
44 const { Box, Text } = $.ui.resolve(e);
45 return (
46 <Box flexDirection="column">
47 {hint}
48 <Text dimColor>{text}</Text>
49 </Box>
50 );
51 });
52};
53
54async function refreshLine(
55 $: EngineInterface,
56 context: SessionContextUsage,
57): Promise<void> {
58 const text = `${await $.session.model()} │ ${contextFill(context)}`;
59 await update($, line, () => text);
60}
61
62// `tokens` and `percent` stay absent until the live window's first response.
63function contextFill(context: SessionContextUsage): string {
64 if (context.tokens === undefined || context.percent === undefined)
65 return "0%";
66 return `${context.percent}% ${context.tokens}/${context.window}`;
67}
68types/index.d.ts 9 lines1// The hint line text, or null until the session has measured itself.
2export type StatuslineLine = string | null;
3
4declare module "claude-code" {
5 interface PluginState {
6 statusline: { line: StatuslineLine };
7 }
8}
9