
Customizing Statusline
- 8 installs
- 2.9k repo stars
- Updated August 3, 2026
- letta-ai/letta-code
Helps with ai & agent building tasks.
About
customizing-statusline is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- customizing-statusline
- AI & Agent Building
- AI-coding skill
Customizing Statusline by the numbers
- 8 all-time installs (skills.sh)
- Ranked #12,269 of 16,556 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/letta-ai/letta-code --skill customizing-statuslineAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 8 |
|---|---|
| repo stars | ★ 2.9k |
| Last updated | August 3, 2026 |
| Repository | letta-ai/letta-code ↗ |
What it does
Helps with ai & agent building tasks.
Files
Customizing Statusline
Use this skill to create or update the global Letta Code statusline mod:
~/.letta/mods/statusline.tsxThe statusline is a full-row idle renderer. Host UI can still temporarily preempt it for safety confirmations and transient hints.
Statusline ownership model
safety preemption
else transient host hint
else custom statusline mod
else built-in default statuslineA custom statusline owns the whole idle row. Do not preserve legacy left/right split semantics in the new API.
Workflow
1. Check whether ~/.letta/mods/statusline.tsx exists. 2. If it exists, read it before editing and preserve unrelated code. 3. If it does not exist, start from the built-in default template or synthesize a focused starter for the user's request. 4. If the user asks to migrate, import a .sh file, or match a shell prompt, read references/migration.md. 5. If API details or concrete patterns are needed, read references/api.md and references/examples.md. 6. If the request combines statusline work with commands, tools, events, panels, or stateful mod behavior, also use creating-mods and its references/architecture.md. 7. Guard statusline-specific behavior with letta.capabilities.ui.customStatuslineRenderer when writing new files. 8. Edit ~/.letta/mods/statusline.tsx. 9. Summarize the absolute file path changed and tell the user to run /reload unless the command can reload automatically.
Bare /statusline behavior
If the user ran /statusline without a specific request:
- If a custom statusline file exists, summarize what it appears to do and ask what they want to change.
- If no custom file exists, explain that Letta is using the built-in default statusline and offer focused next steps:
1. start from the default Letta statusline 2. add project info like git branch, worktree, or PR 3. migrate an existing legacy statusline .sh file 4. match shell prompt / PS1 5. describe a custom statusline in their own words
Keep this conversational. Do not build a menu UI unless the product command explicitly asks for one.
Rules
- Global-only for now. Do not create project mods.
- Keep the mod single-file for MVP.
- Do not assume extra npm packages are available.
- Do not use relative multi-file imports yet.
- Keep renderers synchronous. Do not shell, fetch, or await inside render.
- Do async work in setup code, intervals, subscriptions, or status providers.
- Use
letta.ui.setStatusfor data andsetStatuslineRendererfor drawing that data. - Guard optional APIs with
letta.capabilities.ui.statusValuesandletta.capabilities.ui.customStatuslineRendererin new files. - Return a disposer that clears timers/subscriptions.
- Preserve existing mod code unless the user asks to reset.
- Do not delete legacy command statusline files or settings unless the user explicitly asks.
Useful references
references/api.md- mod API, render context, lifecycle rulesreferences/examples.md- common statusline patternsreferences/migration.md- legacy command.shand PS1 migration
Statusline Mod API
Use this reference when creating or editing ~/.letta/mods/statusline.tsx.
Location
~/.letta/mods/statusline.tsxThis is a trusted, user-owned global mod file. Project mods are intentionally unsupported for now.
Activation
Export a default function or named activate function:
export default function activate(letta) {
if (!letta.capabilities.ui.customStatuslineRenderer) return;
letta.ui.setStatuslineRenderer((context) => {
const { Text } = context.components;
return <Text>{context.agent.name} · {context.model.displayName}</Text>;
});
}API
letta.capabilities.ui.statusValues: boolean
letta.capabilities.ui.customStatuslineRenderer: boolean
letta.ui.setStatus(key: string, value: string | null | undefined | ((context) => string | null)): void
letta.ui.clearStatus(key: string): void
letta.ui.setStatuslineRenderer(renderer: StatuslineRenderer | ((context) => ReactNode | null)): voidsetStatus stores named string values. Renderers read evaluated values from context.statuses.
letta.ui.setStatus("branch", "main");
letta.ui.setStatuslineRenderer((context) => {
const { Text } = context.components;
return <Text>{context.statuses.branch}</Text>;
});Renderer rules
- Renderer owns the entire idle bottom row.
- Renderer must be synchronous.
- Do not run shell commands, network requests, file reads, or awaits inside render.
- Do async work in setup code or intervals, store results with
setStatus, then rendercontext.statuses. - Return
nullonly when intentionally rendering nothing.
Async state pattern
Use Node/Bun APIs directly from the trusted mod file. Do not assume helper methods like letta.shell exist.
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
export default function activate(letta) {
if (!letta.capabilities.ui.customStatuslineRenderer) return;
const update = async () => {
try {
const { stdout } = await execFileAsync("git", ["branch", "--show-current"], {
cwd: process.cwd(),
});
if (letta.capabilities.ui.statusValues) {
letta.ui.setStatus("branch", stdout.trim());
}
} catch {
if (letta.capabilities.ui.statusValues) {
letta.ui.clearStatus("branch");
}
}
};
letta.ui.setStatuslineRenderer((context) => {
const { Text } = context.components;
const branch = context.statuses.branch;
return <Text>{branch ? `branch ${branch}` : context.agent.name}</Text>;
});
void update();
const timer = setInterval(update, 30_000);
return () => {
clearInterval(timer);
if (letta.capabilities.ui.statusValues) {
letta.ui.clearStatus("branch");
}
};
}Context fields
The app statusline render context source types live near:
src/cli/display/statusline/types.ts
src/cli/display/statusline/context.tsCommon fields:
context.components // Display components such as Text, Box, Spacer
context.statuses // evaluated mod status strings
context.app.version
context.workspace.cwd
context.workspace.currentDir
context.workspace.projectDir
context.agent.name
context.agent.id
context.model.id
context.model.displayName
context.model.provider
context.model.reasoningEffort
context.permissionMode
context.terminalWidth
context.contextWindow.usedPercentage
context.contextWindow.remainingPercentage
context.cost.totalDurationMs
context.cost.totalCostUsd
context.reflection
context.memfs
context.backgroundAgents
context.rawPayload // compatibility payload for advanced casesPrefer semantic fields over rawPayload unless migrating old command statuslines.
Full-row layout
New statuslines do not have a host left/right API. To create left/right visual alignment, do it inside the renderer:
return (
<Box flexDirection="row">
<Box flexGrow={1}>
<Text>left content</Text>
</Box>
<Text>right content</Text>
</Box>
);Reload behavior
After editing ~/.letta/mods/statusline.tsx, tell the user to run:
/reloadThe runtime tracks mod loading separately from “no custom statusline,” so a custom statusline should not flash back to the built-in default during reload.
Statusline Examples
Use these as patterns, not mandatory templates. Keep the final mod focused on the user's request.
Agent and model
export default function activate(letta) {
if (!letta.capabilities.ui.customStatuslineRenderer) return;
letta.ui.setStatuslineRenderer((context) => {
const { Text } = context.components;
return <Text>{context.agent.name ?? "Letta"} · {context.model.displayName ?? "no model"}</Text>;
});
}Git branch with fallback
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
export default function activate(letta) {
if (!letta.capabilities.ui.customStatuslineRenderer) return;
const update = async () => {
try {
const { stdout } = await execFileAsync("git", ["branch", "--show-current"], {
cwd: process.cwd(),
});
letta.ui.setStatus("branch", stdout.trim());
} catch {
letta.ui.clearStatus("branch");
}
};
letta.ui.setStatuslineRenderer((context) => {
const { Text } = context.components;
const branch = context.statuses.branch;
return <Text>{branch ? `git ${branch}` : context.agent.name}</Text>;
});
void update();
const timer = setInterval(update, 30_000);
return () => clearInterval(timer);
}Full row with internal right alignment
export default function activate(letta) {
if (!letta.capabilities.ui.customStatuslineRenderer) return;
letta.ui.setStatuslineRenderer((context) => {
const { Box, Text } = context.components;
const model = context.model.displayName ?? "no model";
return (
<Box flexDirection="row">
<Box flexGrow={1}>
<Text dimColor>Press / for commands</Text>
</Box>
<Text>{context.agent.name ?? "Letta"} · {model}</Text>
</Box>
);
});
}GitHub PR number via gh
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
export default function activate(letta) {
if (!letta.capabilities.ui.customStatuslineRenderer) return;
const update = async () => {
try {
const { stdout } = await execFileAsync(
"gh",
["pr", "view", "--json", "number,title", "--jq", "\"#\\(.number) \\(.title)\""],
{ cwd: process.cwd() },
);
const pr = stdout.trim();
pr ? letta.ui.setStatus("pr", pr) : letta.ui.clearStatus("pr");
} catch {
letta.ui.clearStatus("pr");
}
};
letta.ui.setStatuslineRenderer((context) => {
const { Text } = context.components;
return <Text>{context.statuses.pr ?? context.model.displayName}</Text>;
});
void update();
const timer = setInterval(update, 60_000);
return () => clearInterval(timer);
}macOS currently playing track
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
export default function activate(letta) {
if (!letta.capabilities.ui.customStatuslineRenderer) return;
const update = async () => {
try {
const script = 'tell application "Music" to if it is running then artist of current track & " - " & name of current track';
const { stdout } = await execFileAsync("osascript", ["-e", script]);
const music = stdout.trim();
music ? letta.ui.setStatus("music", music) : letta.ui.clearStatus("music");
} catch {
letta.ui.clearStatus("music");
}
};
letta.ui.setStatuslineRenderer((context) => {
const { Text } = context.components;
return <Text>{context.statuses.music ?? context.agent.name}</Text>;
});
void update();
const timer = setInterval(update, 15_000);
return () => clearInterval(timer);
}Statusline Migration
Use this reference when migrating legacy command statuslines, standalone .sh statusline scripts, or shell PS1 prompts.
Legacy Letta command statusline
Inspect these files for old config:
~/.letta/settings.json
<project>/.letta/settings.json
<project>/.letta/settings.local.jsonLook for either shape:
{
"statusLine": {
"type": "command",
"command": "..."
}
}{
"statusLine": {
"command": "...",
"refreshIntervalMs": 30000,
"timeout": 5000,
"debounceMs": 300,
"padding": 0,
"prompt": ">"
}
}When migrating:
- Preserve old config and referenced files unless the user explicitly asks to delete them.
- If
commandreferences a.shfile, read it before writing the new mod. - Translate polling (
refreshIntervalMs) tosetInterval. - Translate direct command output into cached status plus synchronous rendering.
- If the command output used
\x1eto split left/right output, convert it to internal full-row layout withBox; do not create a new left/right API. - Treat old prompt customization separately. The new statusline controls the bottom row, not necessarily the input prompt.
Old model:
echo "$(git branch --show-current)"New model:
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
const update = async () => {
const { stdout } = await execFileAsync("git", ["branch", "--show-current"], {
cwd: process.cwd(),
});
letta.ui.setStatus("branch", stdout.trim());
};
letta.ui.setStatuslineRenderer((context) => {
const { Text } = context.components;
return <Text>{context.statuses.branch ?? ""}</Text>;
});Standalone .sh file migration
If the user provides a .sh path:
1. Read the script. 2. Identify commands, expected stdin JSON, environment variables, and output shape. 3. Port shell commands to async setup/update code. 4. Store results with letta.ui.setStatus(key, value). 5. Render cached status synchronously. 6. Preserve graceful fallbacks for missing tools, not-a-git-repo, no PR, etc.
If a script depends heavily on stdin JSON, use context.rawPayload as a temporary migration aid, but prefer semantic context fields for new code.
Shell PS1 import
If the user asks to match their shell prompt, inspect shell config files in this order:
~/.zshrc
~/.bashrc
~/.bash_profile
~/.profileExtract PS1 with:
/(?:^|\n)\s*(?:export\s+)?PS1\s*=\s*["']([^"']+)["']/mMap common escapes:
\u -> username
\h -> short hostname
\H -> hostname
\w -> current working directory
\W -> basename(current working directory)
\$ -> prompt character, usually remove if trailing
\n -> newline
\t -> HH:MM:SS
\d -> date like Tue May 23
\@ -> 12-hour time
\# -> command number, usually omit unless requested
\! -> history number, usually omit unless requestedIf the imported prompt ends with $, >, or similar prompt chars, remove that trailing prompt marker. The statusline is not the input prompt.
If no PS1 is found and the user did not provide other instructions, ask for one of:
1. the output of echo $PS1 2. a description of what their prompt shows 3. the current prompt output as it appears in their terminal
Preserve colors where practical using display components. If the PS1 is too dynamic to port exactly, ask whether to approximate it or port specific commands.