Skip to content

Segments and layout

Settings live in the status section of ~/.config/opencode-cockpit/config.json for every project, and of <project>/.cockpit.json for one. Comments and trailing commas are fine. The keys every bay shares, the sidebar order and the names from before 0.9 are on Configuration.

A whole line by name, built-ins only — nothing to install, nothing to write:

{ "status": { "preset": "default" } }
PresetSurfaceWhat you get
sidebarsidebarthe default: a table — the window as one bar, the tokens in named rows, a proxy’s budget, the branch’s diff
minimalbottomhow full the context is, and what changed
defaultbottomthe bar, where the tokens went, what changed, how long
detailedbottomeverything the built-ins know, for a wide window
Status
████████████████
tokens 85.2k · 43%
in 265 · 0%
out 60 · 0%
cache 84.9k · 100%
──────────────
spend $26.24
avail $173.76 · 87% left
──────────────
git 5f +312 -48

That is the sidebar preset, and what you get with no configuration at all. A row with nothing to say is not drawn — spend and avail with no proxy writing a budget, a hairline with nothing on one side of it. { "status": { "sidebar": false } }, or "surface": "bottom", draws the default line under the prompt instead. Anything you write beside a preset wins, so it is a starting point and not a mode; a name that is not a preset draws the surface’s own line, with a ! row naming the presets there are.

override changes the preset’s segments by name and keeps every other row, so the table goes on following the preset:

{
"status": {
"override": {
"git": { "against": "branch" }, // an object merges into that segment's settings
"write": false, // false drops it
"session.status": { "working": true }, // the turn's clock as well as a retry
"spend": "cost" // a name swaps it, in the same place
}
}
}

A change applies to every segment of that name — "sep": false drops every hairline. With segments written too, the override applies to those; a line in lines takes its own. A project’s override adds to the global one, key by key. A name that matches no segment is not silent:

! settings: override "gti" matches no segment in the sidebar preset — did you mean "git"?

Look at it before restarting: bunx @opencode-cockpit/status preview --config <file> reads the file as OpenCode will — preset, sidebarRows, override and the ! rows — and stops on a file it cannot read or a flag it does not know. --config - reads it from stdin, as the file it will become (--as global or --as project), so a change can be seen before it is written anywhere. --surface sidebar|bottom draws there whatever the file says; the sidebar is 34 columns unless --width says otherwise. --debug names every row — ✓git drew, ✗spend drew nothing, ?gti is no segment at all.

Terminal window
cat <<'EOF' | bunx @opencode-cockpit/status preview --config - --debug
{ "status": { "override": { "git": { "against": "branch" } } } }
EOF

The status-setup skill previews the same way, with the preview that came with your install — cockpit_settings names it under Previews — never bunx, which would fetch another release.

{
"status": {
"surface": "bottom",
"separator": " │ ",
"segments": [
"git.diff",
{ "type": "context", "style": "gradient", "width": 16 },
{ "type": "cost", "color": "#e8b923" },
"diagnostics"
]
}
}

A segment is a built-in’s name, or that name with settings. An unknown name is skipped rather than fatal: a config written against a newer version costs you a segment, not the line. segments is the whole list and replaces the preset’s; to change one row of a preset, override keeps the rest.

NameShowsSettings
cwdfolder, relative to the worktreemaxWidth
git.branchcurrent branch, dimmed on the default branch
git.diff+150 / -30 — what is uncommitted in the working tree
modelclaude-opus-5full
contexthow full the window isstyle: percent | bar | solid | gradient | split, width, warnAt, dangerAt
tokens78.5k tok; tokens 85.2k · 43% as a table rowformat, style: parts | row
titleStatus, bold: a column’s headingtext
in · out · cache · writecache 84.9k · 100% — one part of the window and its share; nothing when zero
sepa hairline between groups, drawn only with a row on either sidewidth
spend · availspend $26.24, avail $173.76 · 87% left — a proxy’s budget; nothing without onefile
gitgit 5f +312 -48 — what is uncommitted; "against": "branch" counts the branch against where it forked (… vs main)
costsession spendcurrency, showZero
todo3/7 todoshowComplete
session.statusworking 1m02s since your prompt, or a retry and its countdown; "working": false (the sidebar preset) keeps only the retry
session.timethe conversation’s age, or with of: "turn" how long the last answer tookof: session | turn, coarse
diagnostics! name for an unhealthy MCP or language server (OpenCode 2: MCP only); nothing while all are healthy
versionthe bay’s version
textliteral textvalue
commanda shell command’s outputname, row

Every one also takes prefix, suffix, priority, color and icon.

session.time has two clocks, and of picks one:

ofShows
"session", or left out2d 15hhow old the conversation is, from its creation
"turn"took 3m42show long the last answer took, from your prompt to the last reply after it; nothing while one is running, and nothing before the first
{ "type": "session.time", "of": "turn" }

Every built-in line and preset that shows the time uses the turn, so a conversation reopened two days later no longer reads 2d 15h with no word beside it. A line you wrote yourself keeps the session’s age until you add of. While a turn runs, session.status counts it from the same prompt — working 1m02s — so the two never show the same clock twice.

What is uncommitted: git diff --shortstat HEAD, so staged and unstaged changes together, against the last commit. Untracked files are left out, because git cannot count lines in a file it has never seen and a file count that moves without the line counts moving reads as a bug.

It used to report what this session changed, read from OpenCode’s own file list. That number could not be checked against anything, it counted nothing you edited by hand — and when the list came back empty, which it did, the segment simply vanished, which looks exactly like a segment you never configured. Git answers a slightly different question honestly, and you can always run the command yourself to see the same number.

The command only runs when a line actually carries this segment, at most once every two seconds, on the same schedule as any other command segment — a line without it spawns nothing.

Both questions are worth asking, and they are different questions — “what have I changed here” is not “what has the agent changed this turn”. The working tree needs a command, because a built-in that shelled out would stop being a pure function of the snapshot, which is what makes every one of them testable without a filesystem:

{
"status": {
"modules": ["<examples/bottom.ts>"],
"commands": { "tree": { "run": "git diff --shortstat", "intervalMs": 5000 } },
"segments": [
{ "type": "git.diff", "prefix": "uncommitted " },
{ "type": "worktree", "prefix": "tree " }
]
}
}

git diff --shortstat prints 3 files changed, 12 insertions(+), 4 deletions(-), far too long for a line — the worktree segment in examples/bottom.ts reads that and draws 3f +12 -4.

git.diff also answers to session.diff, the name it had while the numbers came from the host.

The sidebar you see is not one panel — each block is a plugin of OpenCode’s own, which its config can switch off. The names differ by version:

OpenCode 1 — ~/.config/opencode/tui.json
{
"plugin": ["opencode-cockpit"],
"plugin_enabled": { "internal:sidebar-context": false }
}
OpenCode 2 — ~/.config/opencode/cli.json
{ "plugins": ["opencode-cockpit", "-opencode.sidebar.context"] }

That removes OpenCode’s own Context / tokens / % used / spent block, leaving the space to the table. It is the honest way to avoid the same figure twice: rather than this bay staying quiet about what the host says, you turn off the half you would rather not read. /cockpit-setup offers it when Status draws in the sidebar.

On OpenCode 1 the other blocks can go the same way:

Plugin (OpenCode 1)What it draws
internal:sidebar-contexttokens, context percentage, spend
internal:sidebar-filesfiles this session changed
internal:sidebar-lsplanguage-server status
internal:sidebar-mcpMCP server status
internal:sidebar-footerthe path and version at the bottom
internal:sidebar-todothe todo list — leave it on, nothing in Cockpit replaces it
internal:home-footer, internal:home-tipsthe home screen’s furniture

On OpenCode 2, -internal:sidebar-context does nothing — the block is opencode.sidebar.context — and its sidebar has three blocks of its own: opencode.sidebar.context, opencode.sidebar.mcp and opencode.sidebar.footer, each off with a - before it in cli.json’s plugins. It has no LSP or Todo block. Leave OpenCode 1’s Todo block on: nothing in Cockpit replaces it. api.plugins.list() prints the current set, so the list above can be checked rather than trusted.

Five styles, because a context meter is the segment people care most about.

styleDraws
percent39% ctx
bara plain bar with end caps
solidone solid bar on a dark track, no figure — the sidebar table’s
gradienta bar whose every cell is coloured by the level it stands for, green through amber to red
splitone bar coloured by what fills it — cache, fresh input, output

split is the one worth knowing about: a session that is mostly re-reading its own cache looks different from one that is mostly new input, and that difference is invisible in a percentage.

{ "type": "context", "style": "split", "width": 12, "warnAt": 0.7, "dangerAt": 0.9 }

Both hide themselves where no context window was declared. A percentage needs a denominator.

{
"status": {
"lines": [
{ "surface": "bottom", "segments": ["git.diff", "todo", "session.time"] },
{ "surface": "sidebar", "segments": ["context", "cost"] }
]
}
}

Two lines on the same surface stack, which is how a two-row statusline is written.

Each line takes its own settings:

SettingDefault
overridethe section’s override
separator" · " across, nothing down
stackvertical in the sidebar, horizontal elsewhere
maxRowsthe bay’s sidebarRows (8; the sidebar preset 14), vertical only
iconson
paddingLeft / Right / Top / Bottomper surface, to line up with OpenCode’s own content

color takes a tone name or a literal. A tone follows whatever theme you run; a literal does not.

Tones: text, muted, accent, success, warning, error, info, and background, panel, border for drawing against the window’s own surfaces.

Prefer a tone. A statusline in someone else’s palette is the first thing that makes a plugin look bolted on.

On by default, in single-width glyphs — an emoji is two cells wide in most terminals and one in a few, which is exactly what shears a fixed-width line. Turn them off with "icons": false, globally or per line, or set your own per segment with "icon": "»".

The top-level "sidebar" list says, for every bay at once — Status first by default:

~/.config/opencode-cockpit/config.json
{ "sidebar": ["subagents", "shell", "status", "trail", "trust"] }

That puts the table under the shells. It has no effect on the bottom surface, where there is nothing to share the row with. A sidebarOrder in the status section is no longer read; it is a ! row pointing here. See Configuration.