@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
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small, quiet sidebar: a coloured context bar and the two figures behind it.
|
|
3
|
+
*
|
|
4
|
+
* The sidebar sits beside OpenCode's own Context block, which already gives you the token count,
|
|
5
|
+
* the percentage and the spend. So this one does not repeat them -- it draws the bar those numbers
|
|
6
|
+
* describe, and adds the two things the host leaves out: how the window is being used, and what
|
|
7
|
+
* the session has changed.
|
|
8
|
+
*
|
|
9
|
+
* {
|
|
10
|
+
* "statusline": {
|
|
11
|
+
* "modules": ["<this file>"],
|
|
12
|
+
* "surface": "sidebar",
|
|
13
|
+
* "segments": ["bar", "split", "changes"]
|
|
14
|
+
* }
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* `stack` defaults to vertical here, so it does not need to be written.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { CustomModule, Run, StatusContext } from "@opencode-cockpit/status/segment"
|
|
21
|
+
import { compact, contextRatio, contextUsed, gradient } from "@opencode-cockpit/status/segment"
|
|
22
|
+
|
|
23
|
+
/** A row with a quiet label, so a column of them lines up as a table would. */
|
|
24
|
+
function row(label: string, value: Run[]): { runs: Run[] } {
|
|
25
|
+
return { runs: [{ text: `${label} `, tone: "muted", dim: true }, ...value] }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default {
|
|
29
|
+
segments: {
|
|
30
|
+
/**
|
|
31
|
+
* The context window, coloured cell by cell. The figure beside it is the host's own, so this
|
|
32
|
+
* is the one place the bay repeats something -- a bar with no number is hard to read at a
|
|
33
|
+
* glance, and the percentage is two characters.
|
|
34
|
+
*/
|
|
35
|
+
bar(ctx: StatusContext, config) {
|
|
36
|
+
const ratio = contextRatio(ctx.session)
|
|
37
|
+
if (ratio === undefined) return undefined
|
|
38
|
+
const width = typeof config.width === "number" ? config.width : 14
|
|
39
|
+
const filled = Math.round(ratio * width)
|
|
40
|
+
const runs: Run[] = []
|
|
41
|
+
for (let cell = 0; cell < width; cell++) {
|
|
42
|
+
runs.push(
|
|
43
|
+
cell < filled
|
|
44
|
+
? { text: "█", color: gradient((cell + 1) / width) }
|
|
45
|
+
: { text: "░", tone: "border" as const },
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
runs.push({ text: ` ${Math.round(ratio * 100)}%`, color: gradient(ratio) })
|
|
49
|
+
return { runs }
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* What the window is made of: cache, fresh input, output. A coloured rule per part rather
|
|
54
|
+
* than a filled chip, so a share of nothing is a mark rather than an empty box.
|
|
55
|
+
*/
|
|
56
|
+
split(ctx: StatusContext) {
|
|
57
|
+
const tokens = ctx.session?.tokens
|
|
58
|
+
const total = contextUsed(tokens)
|
|
59
|
+
if (!tokens || total === 0) return undefined
|
|
60
|
+
const share = (n: number) => `${Math.round((n / total) * 100)}%`
|
|
61
|
+
return row("split", [
|
|
62
|
+
{ text: "▌", tone: "success" },
|
|
63
|
+
{ text: share(tokens.cache.read + tokens.cache.write), tone: "muted" },
|
|
64
|
+
{ text: " ▌", tone: "info" },
|
|
65
|
+
{ text: share(tokens.input), tone: "muted" },
|
|
66
|
+
{ text: " ▌", tone: "accent" },
|
|
67
|
+
{ text: share(tokens.output + tokens.reasoning), tone: "muted" },
|
|
68
|
+
])
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
/** What the session has done to the working tree, which the host never mentions. */
|
|
72
|
+
changes(ctx: StatusContext) {
|
|
73
|
+
const diff = ctx.session?.diff
|
|
74
|
+
if (!diff || diff.files === 0) return undefined
|
|
75
|
+
return row("diff", [
|
|
76
|
+
{ text: `${diff.files}f`, tone: "muted" },
|
|
77
|
+
{ text: ` +${compact(diff.additions)}`, tone: "success" },
|
|
78
|
+
{ text: ` -${compact(diff.deletions)}`, tone: "error" },
|
|
79
|
+
])
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
} satisfies CustomModule
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencode-cockpit/status",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "A statusline for OpenCode you can actually configure: declarative segments, a typed module, or your existing Claude Code statusline command",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,6 +35,10 @@
|
|
|
35
35
|
"./segment": {
|
|
36
36
|
"types": "./types/core/authoring.d.ts",
|
|
37
37
|
"default": "./dist/core/authoring.js"
|
|
38
|
+
},
|
|
39
|
+
"./fixtures": {
|
|
40
|
+
"types": "./types/core/fixtures.d.ts",
|
|
41
|
+
"default": "./dist/core/fixtures.js"
|
|
38
42
|
}
|
|
39
43
|
},
|
|
40
44
|
"engines": {
|
|
@@ -44,6 +48,8 @@
|
|
|
44
48
|
"files": [
|
|
45
49
|
"dist",
|
|
46
50
|
"types",
|
|
51
|
+
"examples",
|
|
52
|
+
"skills",
|
|
47
53
|
"README.md",
|
|
48
54
|
"LICENSE"
|
|
49
55
|
],
|
|
@@ -51,7 +57,7 @@
|
|
|
51
57
|
"access": "public"
|
|
52
58
|
},
|
|
53
59
|
"dependencies": {
|
|
54
|
-
"@opencode-cockpit/client": "0.3.
|
|
60
|
+
"@opencode-cockpit/client": "0.3.1",
|
|
55
61
|
"@opencode-ai/plugin": "1.18.31"
|
|
56
62
|
},
|
|
57
63
|
"devDependencies": {
|
|
@@ -59,5 +65,8 @@
|
|
|
59
65
|
"@opentui/keymap": "0.4.5",
|
|
60
66
|
"@opentui/solid": "0.4.5",
|
|
61
67
|
"solid-js": "1.9.12"
|
|
68
|
+
},
|
|
69
|
+
"bin": {
|
|
70
|
+
"opencode-statusline": "./dist/cli/preview.js"
|
|
62
71
|
}
|
|
63
72
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: statusline-design
|
|
3
|
+
description: Designing or editing an opencode-cockpit statusline — a bottom line or a sidebar column, its config, or a TypeScript segment module. Use when a request mentions the statusline, a segment, .cockpit.json's statusline section, or a module importing @opencode-cockpit/status/segment.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Designing a statusline
|
|
7
|
+
|
|
8
|
+
A statusline is a **visual artifact judged in a terminal**. The failure mode this skill exists to
|
|
9
|
+
prevent is designing it blind: editing TypeScript, restarting OpenCode, and scoring the result from
|
|
10
|
+
a sentence. One sidebar took about twenty restarts and five rejected iterations that way, and three
|
|
11
|
+
of the rejections were glyph choices that read completely differently on screen than they do in
|
|
12
|
+
prose.
|
|
13
|
+
|
|
14
|
+
## Look at it before you ship it
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
bunx @opencode-cockpit/status preview --watch # redraws on every save
|
|
18
|
+
bunx @opencode-cockpit/status preview --state full # one state
|
|
19
|
+
bunx @opencode-cockpit/status preview --debug # mark segments that drew nothing
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The preview draws the real segments against sample sessions, in this terminal, with no OpenCode
|
|
23
|
+
involved. **Use it after every change.** If a design decision cannot be checked in the preview, it
|
|
24
|
+
has not been checked.
|
|
25
|
+
|
|
26
|
+
Before writing code for anything visual, **paste an ASCII mock and ask**. A mock costs a line; a
|
|
27
|
+
wrong reading of one ambiguous word costs two iterations. "Make it taller" once meant "make the bar
|
|
28
|
+
look solid", and the two interpretations share no code.
|
|
29
|
+
|
|
30
|
+
## Rules with reasons
|
|
31
|
+
|
|
32
|
+
| Rule | Why |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| **Never repeat what OpenCode already shows.** Its footer has path, branch, token total, spend; its prompt has agent and model. | Configured carelessly the same percentage lands on screen five times. The exception: a *better instrument* for the same fact — a bar you read without looking is not a second copy of `78.5K (39%)`. |
|
|
35
|
+
| **A segment with nothing to say says nothing.** | `cost` hides where no prices are declared rather than printing `$0.00`; `context` hides with no declared window rather than inventing a denominator. A confident wrong number is worse than an absent one. |
|
|
36
|
+
| **No walls of zeroes on a fresh session.** Check the `fresh` and `empty` fixtures. | Most designs look right mid-session and read as broken before the first reply. |
|
|
37
|
+
| **Every number gets a word.** Colour may repeat the meaning, never carry it alone. | A row distinguished only by colour is unreadable: "I read `mix` and I don't understand the colours." |
|
|
38
|
+
| **Labels in a fixed-width column, values after.** | Alignment is what makes a column read as designed rather than as output. |
|
|
39
|
+
| **Bars are solid.** Filled cells `█` coloured by level; the empty track is `█` in `panel` tone. | `░` reads as floating gaps and `─` reads as `-----`. Both were rejected on sight. Swapping one rejected glyph for another is not iteration. |
|
|
40
|
+
| **Single-width glyphs only.** | An emoji is two cells in most terminals and one in a few — exactly what shears a fixed-width line. |
|
|
41
|
+
| **Prefer a coloured rule `▌` to a filled pill.** | A filled block must be as wide as its text, so a short label leaves a slab of colour and an empty one leaves an empty box. |
|
|
42
|
+
| **Prefer a figure to a moving picture.** | A sparkline redraws its shape every second; movement in peripheral vision is the one thing a statusline must not do. `+1.2%/min · 48m left` changes digits and nothing else. |
|
|
43
|
+
| **No section headings above optional rows.** | A heading cannot know whether the rows under it will draw, so `SPEND` strands itself above nothing on an unpriced model. Self-label the rows instead. |
|
|
44
|
+
| **Emphasis is a bonus, never the meaning.** Bold, italic and underline are `<b>`, `<i>`, `<u>` markup — and a terminal with no bold face draws bold identically to plain. | Colour and background always render; weight may not. There is no strikethrough or inverse at all. |
|
|
45
|
+
| **Colours come from tones, not hexes.** `text muted accent success warning error info background panel border` | A literal ignores the user's theme, which is the first thing that makes a plugin look bolted on. Use a hex only where the exact colour *is* the meaning. |
|
|
46
|
+
| **Say what a number means in the word, not the docs.** `cache` is cache reads, `write` is cache writes, `in` is fresh prompt tokens, `out` is output plus reasoning. | Read and write are not in and out; a reader who has to learn your mapping will misread it. |
|
|
47
|
+
|
|
48
|
+
## The renderer's contract
|
|
49
|
+
|
|
50
|
+
- One segment draws **one row**, unless it returns an **array** of pieces — then each element is a
|
|
51
|
+
row of its own. (Arrays used to be silently dropped; they work now.)
|
|
52
|
+
- A segment returns a string, `{ text, tone, color }`, `{ runs: [...] }`, an array of those, or
|
|
53
|
+
`undefined` to say nothing.
|
|
54
|
+
- A run takes `tone`, `color`, `bg`, `bgTone`, `bold`, `dim`, `italic`, `underline`.
|
|
55
|
+
- **A track drawn in `panel` tone is invisible** on most themes — it is the panel's own colour.
|
|
56
|
+
Use `border`.
|
|
57
|
+
- A **column** keeps at most `maxRows` rows (default 8) — **count your rows and raise it**, or the
|
|
58
|
+
extras vanish. The preview prints `↳ N dropped` when this happens.
|
|
59
|
+
- A **line** drops the lowest-priority segments until it fits the width.
|
|
60
|
+
- A segment that throws loses only its own row. A module that fails to load raises a toast naming
|
|
61
|
+
the file.
|
|
62
|
+
|
|
63
|
+
## Checking what actually reached the terminal
|
|
64
|
+
|
|
65
|
+
`preview` paints with its own ANSI and the smoke harness serialises the screen as text, so both are
|
|
66
|
+
blind to colour and emphasis. When a design looks wrong and the code looks right:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
bun run capture --sidebar --find "40%" --find bold
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
It drives a real OpenCode and reports the escape codes written around the text you name. That is
|
|
73
|
+
how three separate "is this even working" questions were settled in minutes rather than rounds:
|
|
74
|
+
bold *was* being emitted and the font had no bold face; italic *was* being emitted and the word was
|
|
75
|
+
truncated; a track *was* being drawn and `panel` was the panel's own colour.
|
|
76
|
+
|
|
77
|
+
## Where everything lives
|
|
78
|
+
|
|
79
|
+
| What | Where |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| Settings, every project | `~/.config/opencode-cockpit/config.json` |
|
|
82
|
+
| Settings, one project | `<project>/.cockpit.json` |
|
|
83
|
+
| Modules | anywhere — `~/.config/opencode-cockpit/modules/` needs no `node_modules` beside it |
|
|
84
|
+
| Which plugins load | `~/.config/opencode/tui.json` |
|
|
85
|
+
|
|
86
|
+
## Turning OpenCode's own blocks off
|
|
87
|
+
|
|
88
|
+
Each block of the host's sidebar is an internal plugin, and `tui.json` disables any of them:
|
|
89
|
+
|
|
90
|
+
```jsonc
|
|
91
|
+
{ "plugin": ["opencode-cockpit"], "plugin_enabled": { "internal:sidebar-context": false } }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`internal:sidebar-{context,files,todo,lsp,mcp,footer}`, `internal:home-{footer,tips}`,
|
|
95
|
+
`internal:notifications`. **A sidebar meant to replace the Context block must carry what that block
|
|
96
|
+
carried** — percentage, token total, spend — or the user ends up with less than before.
|
|
97
|
+
|
|
98
|
+
The footer under the prompt is core UI: it cannot be hidden. Design around it.
|
|
99
|
+
|
|
100
|
+
## Start simple
|
|
101
|
+
|
|
102
|
+
Most people want a good line, not a composition exercise. Begin with the built-ins and a `format`
|
|
103
|
+
string; reach for a module only when the answer needs the session read, decided on, or remembered
|
|
104
|
+
across ticks — a rate, a trend, a budget from a file. Reach for a shell `command` for anything a CLI
|
|
105
|
+
already prints; do not reimplement the shell as a segment.
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
import type { Run } from "../core/types.ts";
|
|
9
|
+
export declare function paint(run: Run): string;
|
|
10
|
+
export declare function paintRuns(runs: readonly Run[]): string;
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
export {};
|
package/types/core/config.d.ts
CHANGED
|
@@ -52,6 +52,8 @@ export interface CommandConfig {
|
|
|
52
52
|
*/
|
|
53
53
|
export type Stack = "horizontal" | "vertical";
|
|
54
54
|
export interface LineConfig {
|
|
55
|
+
/** A whole line by name; anything written beside it wins. */
|
|
56
|
+
preset?: string;
|
|
55
57
|
surface?: Surface;
|
|
56
58
|
segments?: (string | SegmentConfig)[];
|
|
57
59
|
/** Drawn between segments. Defaults to " · " across, and nothing down. */
|
|
@@ -60,6 +62,8 @@ export interface LineConfig {
|
|
|
60
62
|
stack?: Stack;
|
|
61
63
|
/** Built-in icons. On by default; switch off for a terminal missing the glyphs. */
|
|
62
64
|
icons?: boolean;
|
|
65
|
+
/** Draw a placeholder where a segment said nothing, so a typo and missing data look different. */
|
|
66
|
+
debug?: boolean;
|
|
63
67
|
/** Vertical only: rows to draw at most. Lowest priority goes first. Defaults to 8. */
|
|
64
68
|
maxRows?: number;
|
|
65
69
|
/**
|
|
@@ -74,6 +78,11 @@ export interface LineConfig {
|
|
|
74
78
|
}
|
|
75
79
|
export interface StatusConfig {
|
|
76
80
|
enabled?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* A whole line by name: `minimal`, `default`, `detailed`, `sidebar`. Anything you write
|
|
83
|
+
* alongside it wins, so a preset is a starting point rather than a mode.
|
|
84
|
+
*/
|
|
85
|
+
preset?: string;
|
|
77
86
|
/** One line, for the common case. Use `lines` for more than one surface. */
|
|
78
87
|
surface?: Surface;
|
|
79
88
|
segments?: (string | SegmentConfig)[];
|
|
@@ -81,6 +90,18 @@ export interface StatusConfig {
|
|
|
81
90
|
stack?: Stack;
|
|
82
91
|
/** Built-in icons. On by default; switch off for a terminal missing the glyphs. */
|
|
83
92
|
icons?: boolean;
|
|
93
|
+
/** Draw a placeholder where a segment said nothing, so a typo and missing data look different. */
|
|
94
|
+
debug?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Vertical lines: rows to draw at most. Settable here as well as per line, because writing it
|
|
97
|
+
* here is the natural guess and having it quietly ignored costs exactly the rows it was meant
|
|
98
|
+
* to keep.
|
|
99
|
+
*/
|
|
100
|
+
maxRows?: number;
|
|
101
|
+
paddingLeft?: number;
|
|
102
|
+
paddingRight?: number;
|
|
103
|
+
paddingTop?: number;
|
|
104
|
+
paddingBottom?: number;
|
|
84
105
|
lines?: LineConfig[];
|
|
85
106
|
/** Named commands usable as segments: `{"type": "command", "name": "budget"}`. */
|
|
86
107
|
commands?: Record<string, CommandConfig>;
|
|
@@ -123,6 +144,18 @@ export declare function asStatusConfig(input: unknown): StatusConfig;
|
|
|
123
144
|
*/
|
|
124
145
|
export declare const DEFAULT_SEGMENTS: (string | SegmentConfig)[];
|
|
125
146
|
export declare const DEFAULT_SEPARATOR = " \u2502 ";
|
|
147
|
+
/**
|
|
148
|
+
* Whole lines, by the name of what you want.
|
|
149
|
+
*
|
|
150
|
+
* Composing a good statusline from fourteen segments is a design exercise, and most people want a
|
|
151
|
+
* good line rather than the exercise. Every preset is built-ins only — none needs a module, a
|
|
152
|
+
* command, or anything installed beside it.
|
|
153
|
+
*/
|
|
154
|
+
export declare const PRESETS: Record<string, {
|
|
155
|
+
about: string;
|
|
156
|
+
surface: Surface;
|
|
157
|
+
segments: (string | SegmentConfig)[];
|
|
158
|
+
}>;
|
|
126
159
|
export interface ResolvedLine {
|
|
127
160
|
surface: Surface;
|
|
128
161
|
segments: (string | SegmentConfig)[];
|
|
@@ -130,6 +163,7 @@ export interface ResolvedLine {
|
|
|
130
163
|
stack: Stack;
|
|
131
164
|
maxRows: number;
|
|
132
165
|
icons: boolean;
|
|
166
|
+
debug: boolean;
|
|
133
167
|
paddingLeft: number;
|
|
134
168
|
paddingRight: number;
|
|
135
169
|
paddingTop: number;
|
package/types/core/custom.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SegmentConfig } from "./config.ts";
|
|
2
2
|
import type { StatusContext } from "./context.ts";
|
|
3
|
-
import type { Run, SegmentDef, Tone } from "./segments.ts";
|
|
3
|
+
import type { Piece, Run, SegmentDef, Tone } from "./segments.ts";
|
|
4
4
|
/**
|
|
5
5
|
* Your own segments, written in TypeScript.
|
|
6
6
|
*
|
|
@@ -32,7 +32,7 @@ export type CustomRender = (ctx: StatusContext, config: SegmentConfig) => {
|
|
|
32
32
|
color?: string;
|
|
33
33
|
} | {
|
|
34
34
|
runs: Run[];
|
|
35
|
-
} | string | undefined;
|
|
35
|
+
} | Piece[] | string | undefined;
|
|
36
36
|
export interface CustomModule {
|
|
37
37
|
segments?: Record<string, CustomRender | {
|
|
38
38
|
render: CustomRender;
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
import type { StatusContext } from "./context.ts";
|
|
10
|
+
export type FixtureName = "fresh" | "working" | "full" | "unpriced" | "retrying" | "empty";
|
|
11
|
+
/** Each one is a state a design has to survive, not merely a different set of numbers. */
|
|
12
|
+
export declare const FIXTURES: Record<FixtureName, {
|
|
13
|
+
about: string;
|
|
14
|
+
ctx: StatusContext;
|
|
15
|
+
}>;
|
package/types/core/segments.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { Piece, Run, Segment, SegmentDef } from "./types.ts";
|
|
|
6
6
|
* Turning a line's configuration into the segments a surface draws. The segment model itself lives
|
|
7
7
|
* in `types.ts` and the built-ins in `builtins/`; this file is only the assembly.
|
|
8
8
|
*/
|
|
9
|
-
export type { Piece, Run, Segment, SegmentDef, Tone } from "./types.ts";
|
|
9
|
+
export type { Piece, Pieces, Run, Segment, SegmentDef, Tone } from "./types.ts";
|
|
10
10
|
export { BUILTINS };
|
|
11
11
|
export declare function runsOf(piece: Piece): Run[];
|
|
12
12
|
export declare function segmentText(segment: Segment): string;
|
|
@@ -22,5 +22,14 @@ export interface BuildOptions {
|
|
|
22
22
|
custom?: ReadonlyMap<string, SegmentDef>;
|
|
23
23
|
/** Icons are on by default; a terminal without the glyphs can switch them off. */
|
|
24
24
|
icons?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Draw a placeholder where a segment chose to say nothing, and mark names nothing answers to.
|
|
27
|
+
*
|
|
28
|
+
* The rule that a segment with nothing to say says nothing is right while you are working and
|
|
29
|
+
* miserable while you are configuring: a typo, a proxy that declared no context window, and a
|
|
30
|
+
* model with no prices all look identical, because all three look like absence. This makes the
|
|
31
|
+
* three distinguishable for as long as it is on.
|
|
32
|
+
*/
|
|
33
|
+
debug?: boolean;
|
|
25
34
|
}
|
|
26
35
|
export declare function buildSegments(ctx: StatusContext, configs: SegmentConfig[], options?: BuildOptions | ReadonlyMap<string, SegmentDef>): Segment[];
|
package/types/core/types.d.ts
CHANGED
|
@@ -22,14 +22,21 @@ export interface Run {
|
|
|
22
22
|
bgTone?: Tone;
|
|
23
23
|
bold?: boolean;
|
|
24
24
|
dim?: boolean;
|
|
25
|
+
italic?: boolean;
|
|
26
|
+
underline?: boolean;
|
|
25
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Emphasis stops here on purpose. OpenTUI draws bold, italic and underline as markup (`<b>`,
|
|
30
|
+
* `<i>`, `<u>`) and offers no element for strikethrough or inverse, so neither was ever going to
|
|
31
|
+
* reach the screen. To make a run shout, give it a background: `bgTone` always renders.
|
|
32
|
+
*/
|
|
26
33
|
export interface Segment {
|
|
27
34
|
id: string;
|
|
28
35
|
runs: Run[];
|
|
29
36
|
/** Higher survives when the line is too long for the terminal. */
|
|
30
37
|
priority: number;
|
|
31
38
|
}
|
|
32
|
-
/** What a segment may return: one styled string, or several. */
|
|
39
|
+
/** What a segment may return: one styled string, or several runs. */
|
|
33
40
|
export type Piece = {
|
|
34
41
|
text: string;
|
|
35
42
|
tone?: Tone;
|
|
@@ -37,6 +44,14 @@ export type Piece = {
|
|
|
37
44
|
} | {
|
|
38
45
|
runs: Run[];
|
|
39
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* A segment may also answer with several rows — a gauge with bands, a row per service, a table.
|
|
49
|
+
* Down a column each becomes its own row; across a line they sit next to each other.
|
|
50
|
+
*
|
|
51
|
+
* Returning an array used to be a silent no-op, which is an expensive thing to debug: the segment
|
|
52
|
+
* ran, returned something reasonable, and drew nothing at all.
|
|
53
|
+
*/
|
|
54
|
+
export type Pieces = Piece | Piece[];
|
|
40
55
|
/**
|
|
41
56
|
* A built-in. Returning `undefined` hides it, and that is the important half of the contract:
|
|
42
57
|
* a segment whose input is missing must say nothing. A cost of "$0.00" on a provider nobody
|
|
@@ -48,5 +63,5 @@ export interface SegmentDef {
|
|
|
48
63
|
priority: number;
|
|
49
64
|
/** Shown before the text when icons are on. */
|
|
50
65
|
icon?: string;
|
|
51
|
-
render(ctx: StatusContext, config: SegmentConfig):
|
|
66
|
+
render(ctx: StatusContext, config: SegmentConfig): Pieces | undefined;
|
|
52
67
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
import type { TuiPluginApi, TuiThemeCurrent } from "@opencode-ai/plugin/tui";
|
|
3
|
+
import type { JSX } from "solid-js";
|
|
3
4
|
import type { Segment, Tone } from "../../core/segments.ts";
|
|
4
5
|
/**
|
|
5
6
|
* One line of segments, each segment a list of styled runs. Colour comes from the running theme
|
|
@@ -23,4 +24,4 @@ export interface StatusLineProps {
|
|
|
23
24
|
paddingTop?: number;
|
|
24
25
|
paddingBottom?: number;
|
|
25
26
|
}
|
|
26
|
-
export declare function StatusLine(props: StatusLineProps):
|
|
27
|
+
export declare function StatusLine(props: StatusLineProps): JSX.Element;
|