@opencode-cockpit/status 0.3.0 → 0.3.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.
- package/README.md +49 -2
- package/dist/cli/ansi.js +46 -0
- package/dist/cli/preview.js +136 -0
- package/dist/core/config.js +91 -9
- package/dist/core/custom.js +2 -0
- package/dist/core/fixtures.js +157 -0
- package/dist/core/segments.js +76 -41
- package/dist/tui/components/statusline.js +58 -31
- package/dist/tui/index.js +14 -2
- package/examples/README.md +60 -0
- package/examples/bottom.ts +150 -0
- package/examples/gallery.ts +209 -0
- package/examples/sidebar-budget.ts +257 -0
- package/examples/sidebar-full.ts +160 -0
- package/examples/sidebar.ts +82 -0
- package/package.json +11 -2
- package/skills/statusline-design/SKILL.md +105 -0
- package/types/cli/ansi.d.ts +10 -0
- package/types/cli/preview.d.ts +13 -0
- package/types/core/config.d.ts +34 -0
- package/types/core/custom.d.ts +2 -2
- package/types/core/fixtures.d.ts +15 -0
- package/types/core/segments.d.ts +10 -1
- package/types/core/types.d.ts +17 -2
- package/types/tui/components/statusline.d.ts +2 -1
package/README.md
CHANGED
|
@@ -40,6 +40,19 @@ leaves out:
|
|
|
40
40
|
Everything else is one line of config away — including the things the host shows, if you want them
|
|
41
41
|
in both places.
|
|
42
42
|
|
|
43
|
+
## A whole line by name
|
|
44
|
+
|
|
45
|
+
Composing fourteen segments is a design exercise; most people want a good line. A preset is
|
|
46
|
+
built-ins only — nothing to install, nothing to write:
|
|
47
|
+
|
|
48
|
+
```jsonc
|
|
49
|
+
{ "statusline": { "preset": "default" } }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`minimal` · `default` · `detailed` · `sidebar`. Anything you write beside one wins, so it is a
|
|
53
|
+
starting point and not a mode. See [`examples/`](./examples) for the modules to reach for when a
|
|
54
|
+
preset is not enough.
|
|
55
|
+
|
|
43
56
|
## Configuration
|
|
44
57
|
|
|
45
58
|
`~/.config/opencode-cockpit/config.json` for every project, `<project>/.cockpit.json` for one, and
|
|
@@ -140,6 +153,38 @@ rather than reporting `$0.00`; `context` hides itself where nobody declared a wi
|
|
|
140
153
|
inventing a denominator; `diagnostics` is silent while everything is healthy. That rule matters
|
|
141
154
|
behind a proxy — see [Proxies](#proxies-litellm-and-friends).
|
|
142
155
|
|
|
156
|
+
## Replacing OpenCode's own sidebar blocks
|
|
157
|
+
|
|
158
|
+
Each block of OpenCode's sidebar is an internal plugin, and `tui.json` can switch one off:
|
|
159
|
+
|
|
160
|
+
```jsonc
|
|
161
|
+
// ~/.config/opencode/tui.json
|
|
162
|
+
{
|
|
163
|
+
"plugin": ["@opencode-cockpit/status"],
|
|
164
|
+
"plugin_enabled": { "internal:sidebar-context": false }
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
That removes the host's own `Context / tokens / % used / spent` block, leaving the space to a
|
|
169
|
+
`sidebar` line of your own — the honest way to avoid reading the same figure twice. The same works
|
|
170
|
+
for `internal:sidebar-files`, `-todo`, `-lsp`, `-mcp`, `-footer`, and the home screen's
|
|
171
|
+
`internal:home-footer` and `internal:home-tips`.
|
|
172
|
+
|
|
173
|
+
## What you can draw
|
|
174
|
+
|
|
175
|
+
A segment returns styled runs of text, so the design space is finite and worth seeing all at once.
|
|
176
|
+
`examples/gallery.ts` draws every technique in one column — solid, gradient, fine and split bars, a
|
|
177
|
+
bar painted in background colour, steps, a sparkline, rules, dots, chips, dividers, emphasis, every
|
|
178
|
+
tone, and one segment returning several rows:
|
|
179
|
+
|
|
180
|
+
```sh
|
|
181
|
+
bunx @opencode-cockpit/status preview --module examples/gallery.ts --state working
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
It is a terminal, not a browser — no DOM, no images, no borders. What there is: truecolor
|
|
185
|
+
foreground and background, bold, dim, and alignment. See
|
|
186
|
+
[What you can draw](https://codestz.github.io/opencode-cockpit/status/drawing/).
|
|
187
|
+
|
|
143
188
|
## Your own segments, in TypeScript
|
|
144
189
|
|
|
145
190
|
The declarative config covers the usual line and a shell command covers anything with a CLI. Neither
|
|
@@ -188,8 +233,10 @@ Returning `undefined` hides the segment. A segment that throws loses only its ow
|
|
|
188
233
|
A module that will not load raises a toast naming the file, rather than silently dropping segments.
|
|
189
234
|
|
|
190
235
|
**Worked examples** live in [`examples/`](./examples): `bottom.ts` is a complete line for a window
|
|
191
|
-
with no sidebar; `sidebar.ts` is a quiet column beside OpenCode's own Context block
|
|
192
|
-
|
|
236
|
+
with no sidebar; `sidebar.ts` is a quiet column beside OpenCode's own Context block;
|
|
237
|
+
`sidebar-full.ts` replaces that block; `sidebar-budget.ts` is that column drawn as a table, with a
|
|
238
|
+
budget from a proxy and the branch's diff; `gallery.ts` draws every technique at once. All of them
|
|
239
|
+
are loaded and asserted by the test suite, so none of them can rot.
|
|
193
240
|
|
|
194
241
|
## Your Claude Code statusline
|
|
195
242
|
|
package/dist/cli/ansi.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drawing a statusline to a real terminal, outside OpenCode.
|
|
3
|
+
*
|
|
4
|
+
* The TUI paints runs through OpenTUI against the running theme; here the same runs are written as
|
|
5
|
+
* ANSI so a design can be looked at without restarting anything. The colours approximate a dark
|
|
6
|
+
* theme — close enough to judge a design, never the authority on one.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const TONE_RGB = {
|
|
10
|
+
text: [232, 237, 242],
|
|
11
|
+
muted: [138, 148, 160],
|
|
12
|
+
accent: [57, 211, 83],
|
|
13
|
+
success: [57, 211, 83],
|
|
14
|
+
warning: [232, 185, 35],
|
|
15
|
+
error: [248, 81, 73],
|
|
16
|
+
info: [110, 168, 254],
|
|
17
|
+
background: [11, 13, 16],
|
|
18
|
+
panel: [38, 43, 51],
|
|
19
|
+
border: [62, 70, 80]
|
|
20
|
+
};
|
|
21
|
+
function hexRgb(hex) {
|
|
22
|
+
const value = hex.replace("#", "");
|
|
23
|
+
const full = value.length === 3 ? [...value].map(c => c + c).join("") : value;
|
|
24
|
+
if (full.length !== 6) return undefined;
|
|
25
|
+
const n = Number.parseInt(full, 16);
|
|
26
|
+
return Number.isNaN(n) ? undefined : [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
27
|
+
}
|
|
28
|
+
const ESC = String.fromCharCode(27);
|
|
29
|
+
const fg = ([r, g, b]) => `${ESC}[38;2;${r};${g};${b}m`;
|
|
30
|
+
const bg = ([r, g, b]) => `${ESC}[48;2;${r};${g};${b}m`;
|
|
31
|
+
const RESET = `${ESC}[0m`;
|
|
32
|
+
|
|
33
|
+
/** Dim is rendered as a darker colour rather than the SGR attribute, which terminals disagree on. */
|
|
34
|
+
function darken([r, g, b]) {
|
|
35
|
+
return [Math.round(r * 0.62), Math.round(g * 0.62), Math.round(b * 0.62)];
|
|
36
|
+
}
|
|
37
|
+
export function paint(run) {
|
|
38
|
+
const own = run.color ? hexRgb(run.color) : undefined;
|
|
39
|
+
let colour = own ?? TONE_RGB[run.tone ?? "text"];
|
|
40
|
+
if (run.dim) colour = darken(colour);
|
|
41
|
+
const back = run.bg ? hexRgb(run.bg) : run.bgTone ? TONE_RGB[run.bgTone] : undefined;
|
|
42
|
+
return [back ? bg(back) : "", fg(colour), run.bold ? `${ESC}[1m` : "", run.text, RESET].join("");
|
|
43
|
+
}
|
|
44
|
+
export function paintRuns(runs) {
|
|
45
|
+
return runs.map(paint).join("");
|
|
46
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Draw your statusline in this terminal, against sample sessions, without restarting OpenCode.
|
|
3
|
+
*
|
|
4
|
+
* bunx @opencode-cockpit/status preview
|
|
5
|
+
* bunx @opencode-cockpit/status preview --config ~/.config/opencode-cockpit/config.json
|
|
6
|
+
* bunx @opencode-cockpit/status preview --state full --width 60
|
|
7
|
+
*
|
|
8
|
+
* Why this exists: a statusline is a visual thing, and editing TypeScript, restarting OpenCode and
|
|
9
|
+
* squinting is a loop measured in minutes. One sidebar took about twenty restarts to design, and
|
|
10
|
+
* three of the mistakes were glyph choices that read differently in a terminal than they do in a
|
|
11
|
+
* sentence. Nothing here can tell you a design is good; it can tell you what it looks like.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { watch } from "node:fs";
|
|
15
|
+
import { asSegmentConfig, loadStatusConfig, resolveLines } from "../core/config.js";
|
|
16
|
+
import { loadCustomSegments, resolveModulePath } from "../core/custom.js";
|
|
17
|
+
import { FIXTURES } from "../core/fixtures.js";
|
|
18
|
+
import { fit, fitColumn } from "../core/render.js";
|
|
19
|
+
import { buildSegments, segmentWidth } from "../core/segments.js";
|
|
20
|
+
import { paintRuns } from "./ansi.js";
|
|
21
|
+
const args = process.argv.slice(2).filter(arg => arg !== "preview");
|
|
22
|
+
const flag = name => {
|
|
23
|
+
const at = args.indexOf(`--${name}`);
|
|
24
|
+
return at === -1 ? undefined : args[at + 1];
|
|
25
|
+
};
|
|
26
|
+
const has = name => args.includes(`--${name}`);
|
|
27
|
+
if (has("help")) {
|
|
28
|
+
console.log(`
|
|
29
|
+
preview — draw your statusline here, against sample sessions
|
|
30
|
+
|
|
31
|
+
--config <path> a config file (default: your global + project config)
|
|
32
|
+
--module <path> load a module, in addition to any the config names
|
|
33
|
+
--state <name> ${Object.keys(FIXTURES).join(" | ")} (default: every one)
|
|
34
|
+
--width <n> columns available to the line (default: the surface's own)
|
|
35
|
+
--debug mark segments that drew nothing, so silence and typos look different
|
|
36
|
+
--watch redraw whenever the config or a module changes
|
|
37
|
+
`);
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
const directory = process.cwd();
|
|
41
|
+
const configPath = flag("config");
|
|
42
|
+
const config = configPath ? (await Bun.file(configPath).json()).statusline ?? {} : loadStatusConfig(directory);
|
|
43
|
+
const modules = [...(config.modules ?? []), ...(flag("module") ? [flag("module")] : [])];
|
|
44
|
+
let custom = new Map();
|
|
45
|
+
if (modules.length > 0) {
|
|
46
|
+
const loaded = await loadCustomSegments(modules, directory);
|
|
47
|
+
custom = loaded.segments;
|
|
48
|
+
for (const error of loaded.errors) console.error(` module failed: ${error}`);
|
|
49
|
+
}
|
|
50
|
+
const lines = resolveLines(config);
|
|
51
|
+
const states = flag("state") ? [flag("state")] : Object.keys(FIXTURES);
|
|
52
|
+
const debug = has("debug") || config.debug === true;
|
|
53
|
+
|
|
54
|
+
/** The room each surface actually has in OpenCode, so a preview is not wider than the real thing. */
|
|
55
|
+
const roomFor = (line, terminal) => line.surface === "sidebar" ? 34 : terminal - line.paddingLeft - line.paddingRight;
|
|
56
|
+
const width = Number(flag("width") ?? 0) || 0;
|
|
57
|
+
const dim = text => `${String.fromCharCode(27)}[38;2;110;120;132m${text}${String.fromCharCode(27)}[0m`;
|
|
58
|
+
async function draw() {
|
|
59
|
+
const watched = [];
|
|
60
|
+
for (const state of states) {
|
|
61
|
+
const fixture = FIXTURES[state];
|
|
62
|
+
if (!fixture) {
|
|
63
|
+
console.error(` unknown state "${state}" — try ${Object.keys(FIXTURES).join(", ")}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
console.log(`\n${dim(`── ${state} — ${fixture.about}`)}`);
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
const room = width || roomFor(line, process.stdout.columns || 120);
|
|
69
|
+
const ctx = {
|
|
70
|
+
...fixture.ctx,
|
|
71
|
+
width: room
|
|
72
|
+
};
|
|
73
|
+
const built = buildSegments(ctx, line.segments.map(asSegmentConfig), {
|
|
74
|
+
custom,
|
|
75
|
+
icons: line.icons,
|
|
76
|
+
debug
|
|
77
|
+
});
|
|
78
|
+
const fitted = line.stack === "vertical" ? fitColumn(built, room, line.maxRows) : fit(built, room, line.separator);
|
|
79
|
+
if (fitted.segments.length === 0) {
|
|
80
|
+
console.log(` ${dim(`(${line.surface}: nothing to draw)`)}`);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
console.log(` ${dim(`${line.surface}, ${room} cols`)}`);
|
|
84
|
+
if (line.stack === "vertical") {
|
|
85
|
+
for (const segment of fitted.segments) console.log(` ${paintRuns(segment.runs)}`);
|
|
86
|
+
} else {
|
|
87
|
+
const parts = fitted.segments.map(segment => paintRuns(segment.runs));
|
|
88
|
+
console.log(` ${parts.join(dim(line.separator))}`);
|
|
89
|
+
}
|
|
90
|
+
/** Rows a real sidebar would have dropped in silence. */
|
|
91
|
+
if (fitted.dropped > 0) {
|
|
92
|
+
const over = line.stack === "vertical" ? `maxRows is ${line.maxRows}` : `${room} columns`;
|
|
93
|
+
console.log(` ${dim(`↳ ${fitted.dropped} dropped — ${over}`)}`);
|
|
94
|
+
}
|
|
95
|
+
const widest = Math.max(0, ...fitted.segments.map(segmentWidth));
|
|
96
|
+
if (line.stack === "vertical" && widest > room) {
|
|
97
|
+
console.log(` ${dim(`↳ widest row is ${widest} cols, the column has ${room}`)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
console.log();
|
|
102
|
+
return watched;
|
|
103
|
+
}
|
|
104
|
+
await draw();
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Redraw on change, because the point of a preview is the loop and not the picture. A module is
|
|
108
|
+
* re-imported under a fresh query string: Bun caches modules by specifier, so without it an edit
|
|
109
|
+
* would show the version from the first run forever.
|
|
110
|
+
*/
|
|
111
|
+
if (has("watch")) {
|
|
112
|
+
const files = [...modules.map(m => resolveModulePath(m, directory)), ...(configPath ? [configPath] : [])];
|
|
113
|
+
console.log(dim(` watching ${files.length} file${files.length === 1 ? "" : "s"} — ctrl+c to stop\n`));
|
|
114
|
+
let pending;
|
|
115
|
+
for (const file of files) {
|
|
116
|
+
try {
|
|
117
|
+
watch(file, () => {
|
|
118
|
+
clearTimeout(pending);
|
|
119
|
+
// Editors save in bursts; redraw once the burst is over.
|
|
120
|
+
pending = setTimeout(() => {
|
|
121
|
+
void (async () => {
|
|
122
|
+
if (modules.length > 0) {
|
|
123
|
+
const again = await loadCustomSegments(modules, directory, path => import(`${path}?v=${Date.now()}`));
|
|
124
|
+
custom = again.segments;
|
|
125
|
+
for (const error of again.errors) console.error(` module failed: ${error}`);
|
|
126
|
+
}
|
|
127
|
+
console.clear();
|
|
128
|
+
await draw();
|
|
129
|
+
})();
|
|
130
|
+
}, 120);
|
|
131
|
+
});
|
|
132
|
+
} catch {
|
|
133
|
+
// A file that cannot be watched is not a reason to stop previewing.
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
package/dist/core/config.js
CHANGED
|
@@ -78,7 +78,7 @@ export function asStatusConfig(input) {
|
|
|
78
78
|
const section = raw.statusline ?? raw.status;
|
|
79
79
|
if (section && typeof section === "object") return section;
|
|
80
80
|
const own = {};
|
|
81
|
-
for (const key of ["enabled", "surface", "segments", "separator", "stack", "icons", "lines", "commands", "modules"]) {
|
|
81
|
+
for (const key of ["enabled", "preset", "surface", "segments", "separator", "stack", "icons", "debug", "maxRows", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "lines", "commands", "modules"]) {
|
|
82
82
|
if (raw[key] !== undefined) Object.assign(own, {
|
|
83
83
|
[key]: raw[key]
|
|
84
84
|
});
|
|
@@ -132,6 +132,82 @@ export const DEFAULT_SEGMENTS = [{
|
|
|
132
132
|
icon: ""
|
|
133
133
|
}, "todo", "session.status", "diagnostics"];
|
|
134
134
|
export const DEFAULT_SEPARATOR = " │ ";
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Whole lines, by the name of what you want.
|
|
138
|
+
*
|
|
139
|
+
* Composing a good statusline from fourteen segments is a design exercise, and most people want a
|
|
140
|
+
* good line rather than the exercise. Every preset is built-ins only — none needs a module, a
|
|
141
|
+
* command, or anything installed beside it.
|
|
142
|
+
*/
|
|
143
|
+
export const PRESETS = {
|
|
144
|
+
minimal: {
|
|
145
|
+
about: "how full the context is, and what changed",
|
|
146
|
+
surface: "bottom",
|
|
147
|
+
segments: [{
|
|
148
|
+
type: "context",
|
|
149
|
+
style: "bar",
|
|
150
|
+
width: 12,
|
|
151
|
+
icon: ""
|
|
152
|
+
}, {
|
|
153
|
+
type: "session.diff",
|
|
154
|
+
icon: ""
|
|
155
|
+
}, "session.status", "diagnostics"]
|
|
156
|
+
},
|
|
157
|
+
default: {
|
|
158
|
+
about: "the capacity bar, where the tokens went, what changed, how long",
|
|
159
|
+
surface: "bottom",
|
|
160
|
+
segments: DEFAULT_SEGMENTS
|
|
161
|
+
},
|
|
162
|
+
detailed: {
|
|
163
|
+
about: "everything the built-ins know, for a wide window",
|
|
164
|
+
surface: "bottom",
|
|
165
|
+
segments: [{
|
|
166
|
+
type: "context",
|
|
167
|
+
style: "split",
|
|
168
|
+
width: 14,
|
|
169
|
+
icon: ""
|
|
170
|
+
}, {
|
|
171
|
+
type: "tokens",
|
|
172
|
+
style: "parts",
|
|
173
|
+
icon: ""
|
|
174
|
+
}, {
|
|
175
|
+
type: "model",
|
|
176
|
+
icon: ""
|
|
177
|
+
}, "cost", {
|
|
178
|
+
type: "session.diff",
|
|
179
|
+
icon: ""
|
|
180
|
+
}, "todo", {
|
|
181
|
+
type: "session.time",
|
|
182
|
+
icon: ""
|
|
183
|
+
}, "session.status", "diagnostics"]
|
|
184
|
+
},
|
|
185
|
+
sidebar: {
|
|
186
|
+
about: "a quiet column beside OpenCode's own blocks",
|
|
187
|
+
surface: "sidebar",
|
|
188
|
+
segments: [{
|
|
189
|
+
type: "context",
|
|
190
|
+
style: "bar",
|
|
191
|
+
width: 14,
|
|
192
|
+
icon: ""
|
|
193
|
+
}, {
|
|
194
|
+
type: "tokens",
|
|
195
|
+
format: "tk {total}",
|
|
196
|
+
icon: ""
|
|
197
|
+
}, {
|
|
198
|
+
type: "tokens",
|
|
199
|
+
format: "cache {cacheRead}",
|
|
200
|
+
color: "success",
|
|
201
|
+
icon: ""
|
|
202
|
+
}, {
|
|
203
|
+
type: "session.diff",
|
|
204
|
+
icon: ""
|
|
205
|
+
}, {
|
|
206
|
+
type: "session.time",
|
|
207
|
+
icon: ""
|
|
208
|
+
}, "todo", "diagnostics"]
|
|
209
|
+
}
|
|
210
|
+
};
|
|
135
211
|
/** What each surface needs to sit level with the host's own content. */
|
|
136
212
|
const PADDING = {
|
|
137
213
|
// OpenCode's footer indents three columns, and a line hard against the bottom of the window
|
|
@@ -154,27 +230,33 @@ const PADDING = {
|
|
|
154
230
|
/** Normalises whatever the config said into the lines the renderer draws. */
|
|
155
231
|
export function resolveLines(config) {
|
|
156
232
|
const lines = config.lines?.length ? config.lines : [{
|
|
233
|
+
preset: config.preset,
|
|
157
234
|
surface: config.surface,
|
|
158
235
|
segments: config.segments,
|
|
159
236
|
separator: config.separator,
|
|
160
237
|
stack: config.stack,
|
|
161
|
-
icons: config.icons
|
|
238
|
+
icons: config.icons,
|
|
239
|
+
debug: config.debug,
|
|
240
|
+
maxRows: config.maxRows
|
|
162
241
|
}];
|
|
163
242
|
return lines.map(line => {
|
|
164
|
-
|
|
243
|
+
// A preset fills in what was not written; it never overrides what was.
|
|
244
|
+
const preset = PRESETS[line.preset ?? config.preset ?? ""];
|
|
245
|
+
const surface = line.surface ?? config.surface ?? preset?.surface ?? "bottom";
|
|
165
246
|
// The sidebar is a narrow column: across, it would be three truncated words.
|
|
166
247
|
const stack = line.stack ?? config.stack ?? (surface === "sidebar" ? "vertical" : "horizontal");
|
|
167
248
|
return {
|
|
168
249
|
surface,
|
|
169
|
-
segments: line.segments ?? config.segments ?? DEFAULT_SEGMENTS,
|
|
250
|
+
segments: line.segments ?? config.segments ?? preset?.segments ?? DEFAULT_SEGMENTS,
|
|
170
251
|
separator: line.separator ?? config.separator ?? (stack === "vertical" ? "" : DEFAULT_SEPARATOR),
|
|
171
252
|
stack,
|
|
172
|
-
maxRows: line.maxRows ?? 8,
|
|
253
|
+
maxRows: line.maxRows ?? config.maxRows ?? 8,
|
|
173
254
|
icons: line.icons ?? config.icons ?? true,
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
255
|
+
debug: line.debug ?? config.debug ?? false,
|
|
256
|
+
paddingLeft: line.paddingLeft ?? config.paddingLeft ?? PADDING[surface].left,
|
|
257
|
+
paddingRight: line.paddingRight ?? config.paddingRight ?? PADDING[surface].right,
|
|
258
|
+
paddingTop: line.paddingTop ?? config.paddingTop ?? PADDING[surface].top,
|
|
259
|
+
paddingBottom: line.paddingBottom ?? config.paddingBottom ?? PADDING[surface].bottom
|
|
178
260
|
};
|
|
179
261
|
});
|
|
180
262
|
}
|
package/dist/core/custom.js
CHANGED
|
@@ -101,6 +101,8 @@ export async function loadCustomSegments(paths, directory, importer = path => im
|
|
|
101
101
|
render(ctx, config) {
|
|
102
102
|
const value = render(ctx, config);
|
|
103
103
|
if (value === undefined) return undefined;
|
|
104
|
+
// Several rows: each is drawn on its own, and empty ones are left out.
|
|
105
|
+
if (Array.isArray(value)) return value.length > 0 ? value : undefined;
|
|
104
106
|
if (typeof value === "string") return value ? {
|
|
105
107
|
text: value,
|
|
106
108
|
tone: "muted"
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sample sessions to draw a statusline against, without an OpenCode to draw it in.
|
|
3
|
+
*
|
|
4
|
+
* Designing a statusline by editing TypeScript, restarting OpenCode and looking is a loop measured
|
|
5
|
+
* in minutes; one sidebar cost roughly twenty restarts. These are the states worth checking a
|
|
6
|
+
* design against — and the ones a design usually gets wrong, because they are the states you are
|
|
7
|
+
* not in while you are designing.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const session = (over = {}) => ({
|
|
11
|
+
id: "ses_preview",
|
|
12
|
+
title: "A session",
|
|
13
|
+
status: "idle",
|
|
14
|
+
cost: 0.42,
|
|
15
|
+
priced: true,
|
|
16
|
+
messages: 8,
|
|
17
|
+
startedAt: 0,
|
|
18
|
+
model: {
|
|
19
|
+
providerID: "anthropic",
|
|
20
|
+
modelID: "claude-opus-5-20260101",
|
|
21
|
+
contextLimit: 200_000
|
|
22
|
+
},
|
|
23
|
+
tokens: {
|
|
24
|
+
input: 265,
|
|
25
|
+
output: 60,
|
|
26
|
+
reasoning: 0,
|
|
27
|
+
cache: {
|
|
28
|
+
read: 84_900,
|
|
29
|
+
write: 0
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
diff: {
|
|
33
|
+
files: 3,
|
|
34
|
+
additions: 42,
|
|
35
|
+
deletions: 7
|
|
36
|
+
},
|
|
37
|
+
todo: {
|
|
38
|
+
total: 5,
|
|
39
|
+
completed: 2
|
|
40
|
+
},
|
|
41
|
+
...over
|
|
42
|
+
});
|
|
43
|
+
const base = (over = {}) => ({
|
|
44
|
+
now: 2 * 24 * 3600_000 + 15 * 3600_000,
|
|
45
|
+
directory: "/Users/you/code/checkout-service/src",
|
|
46
|
+
worktree: "/Users/you/code/checkout-service",
|
|
47
|
+
home: "/Users/you",
|
|
48
|
+
branch: "feature/checkout",
|
|
49
|
+
defaultBranch: "main",
|
|
50
|
+
version: "0.3.0",
|
|
51
|
+
lsp: [{
|
|
52
|
+
name: "tsserver",
|
|
53
|
+
status: "connected"
|
|
54
|
+
}],
|
|
55
|
+
mcp: [{
|
|
56
|
+
name: "github",
|
|
57
|
+
status: "connected"
|
|
58
|
+
}],
|
|
59
|
+
commands: {},
|
|
60
|
+
width: 120,
|
|
61
|
+
...over
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/** Each one is a state a design has to survive, not merely a different set of numbers. */
|
|
65
|
+
export const FIXTURES = {
|
|
66
|
+
/** Before the first reply: no model, no tokens, no cost. Most designs render a wall of zeroes. */
|
|
67
|
+
fresh: {
|
|
68
|
+
about: "a new session, before the first reply",
|
|
69
|
+
ctx: base({
|
|
70
|
+
session: session({
|
|
71
|
+
messages: 0,
|
|
72
|
+
cost: 0,
|
|
73
|
+
priced: false,
|
|
74
|
+
model: undefined,
|
|
75
|
+
tokens: undefined,
|
|
76
|
+
diff: {
|
|
77
|
+
files: 0,
|
|
78
|
+
additions: 0,
|
|
79
|
+
deletions: 0
|
|
80
|
+
},
|
|
81
|
+
todo: {
|
|
82
|
+
total: 0,
|
|
83
|
+
completed: 0
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
},
|
|
88
|
+
/** The ordinary case: a few turns in, mostly cache. */
|
|
89
|
+
working: {
|
|
90
|
+
about: "a few turns in, mostly served from cache",
|
|
91
|
+
ctx: base({
|
|
92
|
+
session: session()
|
|
93
|
+
})
|
|
94
|
+
},
|
|
95
|
+
/** Nearly out of room, which is when the design has to shout. */
|
|
96
|
+
full: {
|
|
97
|
+
about: "the context nearly full, a long session",
|
|
98
|
+
ctx: base({
|
|
99
|
+
session: session({
|
|
100
|
+
cost: 26.24,
|
|
101
|
+
tokens: {
|
|
102
|
+
input: 4_200,
|
|
103
|
+
output: 2_100,
|
|
104
|
+
reasoning: 900,
|
|
105
|
+
cache: {
|
|
106
|
+
read: 181_000,
|
|
107
|
+
write: 3_400
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
diff: {
|
|
111
|
+
files: 28,
|
|
112
|
+
additions: 1_840,
|
|
113
|
+
deletions: 620
|
|
114
|
+
},
|
|
115
|
+
todo: {
|
|
116
|
+
total: 9,
|
|
117
|
+
completed: 9
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
},
|
|
122
|
+
/** Behind a proxy: tokens flow, but nobody declared prices or a window. */
|
|
123
|
+
unpriced: {
|
|
124
|
+
about: "behind a proxy — no declared prices or context window",
|
|
125
|
+
ctx: base({
|
|
126
|
+
session: session({
|
|
127
|
+
priced: false,
|
|
128
|
+
cost: 0,
|
|
129
|
+
model: {
|
|
130
|
+
providerID: "litellm",
|
|
131
|
+
modelID: "claude-opus-5"
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
},
|
|
136
|
+
/** Stalled, which OpenCode itself shows only as a spinner. */
|
|
137
|
+
retrying: {
|
|
138
|
+
about: "a stalled turn, retrying",
|
|
139
|
+
ctx: base({
|
|
140
|
+
now: 1_000,
|
|
141
|
+
session: session({
|
|
142
|
+
status: "retry",
|
|
143
|
+
startedAt: 0,
|
|
144
|
+
retry: {
|
|
145
|
+
attempt: 2,
|
|
146
|
+
message: "rate limited",
|
|
147
|
+
next: 6_000
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
},
|
|
152
|
+
/** Off a session route entirely: everything session-shaped must stay quiet. */
|
|
153
|
+
empty: {
|
|
154
|
+
about: "no session at all",
|
|
155
|
+
ctx: base()
|
|
156
|
+
}
|
|
157
|
+
};
|