@opencode-cockpit/status 0.3.0

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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +254 -0
  3. package/dist/core/ansi.js +145 -0
  4. package/dist/core/authoring.js +13 -0
  5. package/dist/core/builtins/index.js +10 -0
  6. package/dist/core/builtins/model.js +204 -0
  7. package/dist/core/builtins/place.js +66 -0
  8. package/dist/core/builtins/session.js +72 -0
  9. package/dist/core/builtins/settings.js +28 -0
  10. package/dist/core/builtins/system.js +58 -0
  11. package/dist/core/claude-code.js +79 -0
  12. package/dist/core/command.js +77 -0
  13. package/dist/core/config.js +187 -0
  14. package/dist/core/context.js +24 -0
  15. package/dist/core/custom.js +125 -0
  16. package/dist/core/format.js +156 -0
  17. package/dist/core/render.js +88 -0
  18. package/dist/core/segments.js +148 -0
  19. package/dist/core/types.js +1 -0
  20. package/dist/tui/components/statusline.js +135 -0
  21. package/dist/tui/index.js +110 -0
  22. package/dist/tui/state/snapshot.js +144 -0
  23. package/dist/tui/state/store.js +51 -0
  24. package/package.json +63 -0
  25. package/types/core/ansi.d.ts +8 -0
  26. package/types/core/authoring.d.ts +17 -0
  27. package/types/core/builtins/index.d.ts +6 -0
  28. package/types/core/builtins/model.d.ts +3 -0
  29. package/types/core/builtins/place.d.ts +3 -0
  30. package/types/core/builtins/session.d.ts +3 -0
  31. package/types/core/builtins/settings.d.ts +13 -0
  32. package/types/core/builtins/system.d.ts +3 -0
  33. package/types/core/claude-code.d.ts +61 -0
  34. package/types/core/command.d.ts +35 -0
  35. package/types/core/config.d.ts +141 -0
  36. package/types/core/context.d.ts +78 -0
  37. package/types/core/custom.d.ts +49 -0
  38. package/types/core/format.d.ts +56 -0
  39. package/types/core/render.d.ts +22 -0
  40. package/types/core/segments.d.ts +26 -0
  41. package/types/core/types.d.ts +52 -0
  42. package/types/tui/components/statusline.d.ts +26 -0
  43. package/types/tui/index.d.ts +10 -0
  44. package/types/tui/state/snapshot.d.ts +11 -0
  45. package/types/tui/state/store.d.ts +28 -0
@@ -0,0 +1,110 @@
1
+ import { memo as _$memo } from "@opentui/solid";
2
+ import { createComponent as _$createComponent } from "@opentui/solid";
3
+ /** @jsxImportSource @opentui/solid */
4
+
5
+ import { claimFeature, duplicateFeatureMessage } from "@opencode-cockpit/client/feature";
6
+ import { createMemo } from "solid-js";
7
+ import pkg from "../../package.json" with { type: "json" };
8
+ import { asSegmentConfig, loadStatusConfig, resolveLines } from "../core/config.js";
9
+ import { loadCustomSegments } from "../core/custom.js";
10
+ import { fit, fitColumn } from "../core/render.js";
11
+ import { buildSegments } from "../core/segments.js";
12
+ import { StatusLine } from "./components/statusline.js";
13
+ import { buildContext } from "./state/snapshot.js";
14
+ import { createStatusStore } from "./state/store.js";
15
+ const STATUS_PACKAGE = "@opencode-cockpit/status";
16
+
17
+ /** Status' TUI half as a factory, so bundles such as `opencode-cockpit` can include it. */
18
+ export function createStatusTui({
19
+ source = STATUS_PACKAGE
20
+ } = {}) {
21
+ return async (api, rawOptions) => {
22
+ // The renderer is shared by every TUI plugin in this OpenCode window.
23
+ const claim = claimFeature(api.renderer, "status", source);
24
+ if (!claim.active) {
25
+ api.ui.toast({
26
+ variant: "warning",
27
+ title: "opencode-cockpit",
28
+ message: duplicateFeatureMessage("Statusline", claim.owner, source),
29
+ duration: 10_000
30
+ });
31
+ return;
32
+ }
33
+ api.lifecycle.onDispose(() => claim.release());
34
+ const directory = api.state.path.directory;
35
+ const config = loadStatusConfig(directory, rawOptions);
36
+ if (config.enabled === false) return;
37
+
38
+ // Your own segments, loaded before the first draw so they are never missing from frame one.
39
+ let custom = new Map();
40
+ if (config.modules?.length) {
41
+ const loaded = await loadCustomSegments(config.modules, directory);
42
+ custom = loaded.segments;
43
+ // A module that will not load is worth saying out loud: its segments silently vanish.
44
+ for (const error of loaded.errors) {
45
+ api.ui.toast({
46
+ variant: "error",
47
+ title: "Statusline",
48
+ message: error,
49
+ duration: 10_000
50
+ });
51
+ }
52
+ }
53
+ const lines = resolveLines(config);
54
+ const store = createStatusStore(api, config, {
55
+ version: pkg.version,
56
+ build: buildContext
57
+ });
58
+ api.lifecycle.onDispose(() => store.dispose());
59
+
60
+ /** A line, fitted to the room its surface actually has. */
61
+ const line = (spec, width) => {
62
+ const segments = createMemo(() => {
63
+ const built = buildSegments(store.context(), spec.segments.map(asSegmentConfig), {
64
+ custom,
65
+ icons: spec.icons
66
+ });
67
+ return spec.stack === "vertical" ? fitColumn(built, width(), spec.maxRows).segments : fit(built, width(), spec.separator).segments;
68
+ });
69
+ return _$createComponent(StatusLine, {
70
+ api: api,
71
+ segments: segments,
72
+ get separator() {
73
+ return spec.separator;
74
+ },
75
+ get stack() {
76
+ return spec.stack;
77
+ },
78
+ get paddingLeft() {
79
+ return spec.paddingLeft;
80
+ },
81
+ get paddingRight() {
82
+ return spec.paddingRight;
83
+ },
84
+ get paddingTop() {
85
+ return spec.paddingTop;
86
+ },
87
+ get paddingBottom() {
88
+ return spec.paddingBottom;
89
+ }
90
+ });
91
+ };
92
+ const on = surface => lines.filter(spec => spec.surface === surface);
93
+ const bottom = on("bottom");
94
+ const sidebar = on("sidebar");
95
+ api.slots.register({
96
+ // After the shell dock (150), so the line sits at the very bottom of the window.
97
+ order: 200,
98
+ slots: {
99
+ app_bottom: () => _$memo(() => bottom.map(spec => line(spec, () => api.renderer.width - spec.paddingLeft - spec.paddingRight))),
100
+ // sidebar_content, not sidebar_footer: the host does not draw plugin content in the footer.
101
+ sidebar_content: () => _$memo(() => sidebar.map(spec => line(spec, () => Math.max(10, Math.floor(api.renderer.width / 4)))))
102
+ }
103
+ });
104
+ };
105
+ }
106
+ const plugin = {
107
+ id: STATUS_PACKAGE,
108
+ tui: createStatusTui()
109
+ };
110
+ export default plugin;
@@ -0,0 +1,144 @@
1
+ import { homedir } from "node:os";
2
+ /**
3
+ * Turns OpenCode's live state into the plain snapshot the segments read. Everything that touches
4
+ * the plugin api lives here, so every built-in stays a pure function of its input.
5
+ */
6
+
7
+ /** Everything a usage reading accounts for; zero means the message has not reported yet. */
8
+ function counted(tokens) {
9
+ return tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write;
10
+ }
11
+
12
+ /** The session the interface is showing, when it is showing one. */
13
+ export function currentSession(api) {
14
+ const route = api.route.current;
15
+ return route.name === "session" ? route.params.sessionID : undefined;
16
+ }
17
+ export function sessionSnapshot(api, id, now) {
18
+ const messages = api.state.session.messages(id);
19
+ const status = api.state.session.status(id);
20
+ const session = api.state.session.get(id);
21
+ let summed = 0;
22
+ let tokens;
23
+ let modelID;
24
+ let providerID;
25
+ for (const message of messages) {
26
+ if (message.role !== "assistant") continue;
27
+ summed += message.cost ?? 0;
28
+ /**
29
+ * The newest assistant message is what currently occupies the window -- but only once it has
30
+ * reported its usage. A message that is still streaming carries zeroes, and taking those would
31
+ * blank every token-based segment for the length of the turn, which reads as the line breaking
32
+ * exactly when you are watching it.
33
+ */
34
+ if (message.tokens && counted(message.tokens) > 0) tokens = message.tokens;
35
+ modelID = message.modelID;
36
+ providerID = message.providerID;
37
+ }
38
+
39
+ /**
40
+ * OpenCode keeps a running total on the session record, and its own sidebar reads that. Summing
41
+ * the messages we can see under-reports it twice over: a turn still streaming has not booked its
42
+ * cost yet, and revert or compaction takes spent history out of the list entirely. Observed live
43
+ * against a proxy as $0.30 here against $0.56 in the sidebar.
44
+ *
45
+ * The field is not in the published `Session` type, so it is read defensively and the sum stands
46
+ * in when it is absent.
47
+ */
48
+ const accumulated = session?.cost;
49
+ const cost = typeof accumulated === "number" && Number.isFinite(accumulated) ? accumulated : summed;
50
+ const model = modelID && providerID ? describeModel(api, providerID, modelID) : undefined;
51
+ let additions = 0;
52
+ let deletions = 0;
53
+ const files = api.state.session.diff(id);
54
+ for (const file of files) {
55
+ additions += file.additions;
56
+ deletions += file.deletions;
57
+ }
58
+ const todos = api.state.session.todo(id);
59
+ const completed = todos.filter(todo => todo.status === "completed").length;
60
+ return {
61
+ id,
62
+ title: session?.title,
63
+ status: status?.type === "busy" ? "busy" : status?.type === "retry" ? "retry" : "idle",
64
+ ...(status?.type === "retry" ? {
65
+ retry: {
66
+ attempt: status.attempt,
67
+ message: status.message,
68
+ next: status.next
69
+ }
70
+ } : {}),
71
+ ...(model ? {
72
+ model
73
+ } : {}),
74
+ ...(tokens ? {
75
+ tokens
76
+ } : {}),
77
+ cost,
78
+ priced: model?.priced ?? false,
79
+ messages: messages.length,
80
+ ...(session?.time.created ? {
81
+ startedAt: session.time.created
82
+ } : {
83
+ startedAt: now
84
+ }),
85
+ diff: {
86
+ files: files.length,
87
+ additions,
88
+ deletions
89
+ },
90
+ todo: {
91
+ total: todos.length,
92
+ completed
93
+ }
94
+ };
95
+ }
96
+
97
+ /**
98
+ * A model's context window and whether anyone declared prices for it. Both come from the provider
99
+ * catalogue or from the user's own `provider.<id>.models` config — which is the only place they
100
+ * come from behind a proxy such as LiteLLM, where the catalogue knows nothing.
101
+ */
102
+ function describeModel(api, providerID, modelID) {
103
+ const provider = api.state.provider.find(p => p.id === providerID);
104
+ const model = provider?.models[modelID];
105
+ const limit = model?.limit?.context;
106
+ const cost = model?.cost;
107
+ return {
108
+ providerID,
109
+ modelID,
110
+ ...(limit && limit > 0 ? {
111
+ contextLimit: limit
112
+ } : {}),
113
+ priced: Boolean(cost && (cost.input > 0 || cost.output > 0))
114
+ };
115
+ }
116
+ export function buildContext(api, options) {
117
+ const sessionID = currentSession(api);
118
+ return {
119
+ now: options.now,
120
+ directory: api.state.path.directory,
121
+ worktree: api.state.path.worktree,
122
+ home: process.env.HOME ?? homedir(),
123
+ ...(api.state.vcs?.branch ? {
124
+ branch: api.state.vcs.branch
125
+ } : {}),
126
+ ...(api.state.vcs?.default_branch ? {
127
+ defaultBranch: api.state.vcs.default_branch
128
+ } : {}),
129
+ version: options.version,
130
+ ...(sessionID ? {
131
+ session: sessionSnapshot(api, sessionID, options.now)
132
+ } : {}),
133
+ lsp: api.state.lsp().map(item => ({
134
+ name: item.id,
135
+ status: String(item.status)
136
+ })),
137
+ mcp: api.state.mcp().map(item => ({
138
+ name: item.name,
139
+ status: String(item.status)
140
+ })),
141
+ commands: options.commands,
142
+ width: options.width
143
+ };
144
+ }
@@ -0,0 +1,51 @@
1
+ import { createMemo, createRoot, createSignal } from "solid-js";
2
+ import { createRunner, execShell } from "../../core/command.js";
3
+
4
+ /**
5
+ * Keeps one snapshot of OpenCode's state for every line to read. One memo rather than one per
6
+ * segment: the whole snapshot is a handful of reads of state already in memory, and recomputing it
7
+ * once a second costs less than the bookkeeping to avoid it.
8
+ */
9
+
10
+ export function createStatusStore(api, config, options) {
11
+ return createRoot(dispose => {
12
+ const clock = options.now ?? (() => Date.now());
13
+ const [now, setNow] = createSignal(clock());
14
+ const tick = setInterval(() => setNow(clock()), options.tickMs ?? 1000);
15
+
16
+ // A finished command run is a repaint: `equals: false` so an identical count still notifies.
17
+ const [commandTick, bump] = createSignal(0, {
18
+ equals: false
19
+ });
20
+ const runners = new Map();
21
+ for (const [name, command] of Object.entries(config.commands ?? {})) {
22
+ runners.set(name, createRunner(command, {
23
+ exec: options.exec ?? execShell,
24
+ now: clock,
25
+ onValue: () => bump(n => n + 1)
26
+ }));
27
+ }
28
+ const context = createMemo(() => {
29
+ commandTick();
30
+ const commands = {};
31
+ for (const [name, runner] of runners) commands[name] = runner.value();
32
+ const ctx = options.build(api, {
33
+ now: now(),
34
+ width: api.renderer.width,
35
+ version: options.version,
36
+ commands
37
+ });
38
+ // Asking here keeps the schedule tied to what is actually drawn: a hidden line costs nothing.
39
+ for (const runner of runners.values()) runner.maybeRun(ctx);
40
+ return ctx;
41
+ });
42
+ return {
43
+ context,
44
+ dispose() {
45
+ clearInterval(tick);
46
+ for (const runner of runners.values()) runner.dispose();
47
+ dispose();
48
+ }
49
+ };
50
+ });
51
+ }
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@opencode-cockpit/status",
3
+ "version": "0.3.0",
4
+ "description": "A statusline for OpenCode you can actually configure: declarative segments, a typed module, or your existing Claude Code statusline command",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Codestz",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Codestz/opencode-cockpit.git",
11
+ "directory": "packages/status"
12
+ },
13
+ "homepage": "https://github.com/Codestz/opencode-cockpit#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/Codestz/opencode-cockpit/issues"
16
+ },
17
+ "keywords": [
18
+ "opencode",
19
+ "opencode-plugin",
20
+ "statusline",
21
+ "status-line",
22
+ "tui",
23
+ "terminal",
24
+ "prompt"
25
+ ],
26
+ "exports": {
27
+ "./tui": {
28
+ "types": "./types/tui/index.d.ts",
29
+ "default": "./dist/tui/index.js"
30
+ },
31
+ "./config": {
32
+ "types": "./types/core/config.d.ts",
33
+ "default": "./dist/core/config.js"
34
+ },
35
+ "./segment": {
36
+ "types": "./types/core/authoring.d.ts",
37
+ "default": "./dist/core/authoring.js"
38
+ }
39
+ },
40
+ "engines": {
41
+ "opencode": ">=1.18.0",
42
+ "bun": ">=1.3.5"
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "types",
47
+ "README.md",
48
+ "LICENSE"
49
+ ],
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "dependencies": {
54
+ "@opencode-cockpit/client": "0.3.0",
55
+ "@opencode-ai/plugin": "1.18.31"
56
+ },
57
+ "devDependencies": {
58
+ "@opentui/core": "0.4.5",
59
+ "@opentui/keymap": "0.4.5",
60
+ "@opentui/solid": "0.4.5",
61
+ "solid-js": "1.9.12"
62
+ }
63
+ }
@@ -0,0 +1,8 @@
1
+ import type { Run } from "./segments.ts";
2
+ /**
3
+ * Parses one line of a script's output into runs. Text with no escapes comes back as a single
4
+ * muted run, which is what a plain script should look like.
5
+ */
6
+ export declare function parseAnsi(line: string): Run[];
7
+ /** Every line of a script's output, parsed. Claude Code statuslines may print several rows. */
8
+ export declare function parseAnsiLines(stdout: string): Run[][];
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The public surface a statusline module writes against, published as
3
+ * `@opencode-cockpit/status/segment`.
4
+ *
5
+ * import type { StatusContext, CustomModule } from "@opencode-cockpit/status/segment"
6
+ *
7
+ * Everything here is a type or a pure helper: a module never touches OpenCode's plugin api, only
8
+ * the snapshot it is handed. That is what makes a custom segment as testable as a built-in.
9
+ */
10
+ export type { ClaudeCodeStatusInput } from "./claude-code.ts";
11
+ export type { SegmentConfig } from "./config.ts";
12
+ export type { ServiceSnapshot, SessionSnapshot, StatusContext, TokenCounts, } from "./context.ts";
13
+ export { contextRatio, contextUsed, todoRemaining, unhealthy } from "./context.ts";
14
+ export type { CustomModule, CustomRender } from "./custom.ts";
15
+ export { bar, basename, compact, duration, gradient, money, percent, preciseDuration, shortModel, shortPath, truncate, truncateStart, } from "./format.ts";
16
+ export type { Piece, Run, Segment, SegmentDef, Tone } from "./segments.ts";
17
+ export { cutSegment, runsOf, segmentText, segmentWidth } from "./segments.ts";
@@ -0,0 +1,6 @@
1
+ import type { SegmentDef } from "../types.ts";
2
+ /**
3
+ * Every built-in, grouped by what it talks about rather than listed in one file: adding a segment
4
+ * should mean opening the twenty lines it belongs with, not four hundred.
5
+ */
6
+ export declare const BUILTINS: SegmentDef[];
@@ -0,0 +1,3 @@
1
+ /** The model in play, how full its context window is, and what the session is spending. */
2
+ import type { SegmentDef } from "../types.ts";
3
+ export declare const SEGMENTS: SegmentDef[];
@@ -0,0 +1,3 @@
1
+ /** Where you are: the folder, the branch, and what has changed in it. */
2
+ import type { SegmentDef } from "../types.ts";
3
+ export declare const SEGMENTS: SegmentDef[];
@@ -0,0 +1,3 @@
1
+ /** How the session is going: work outstanding, work in progress, time spent. */
2
+ import type { SegmentDef } from "../types.ts";
3
+ export declare const SEGMENTS: SegmentDef[];
@@ -0,0 +1,13 @@
1
+ /** Reading a segment's own settings out of its config entry, safely. */
2
+ import type { SegmentConfig } from "../config.ts";
3
+ import type { Piece, Tone } from "../types.ts";
4
+ export declare function num(config: SegmentConfig, key: string, fallback: number): number;
5
+ export declare function str(config: SegmentConfig, key: string): string | undefined;
6
+ /**
7
+ * A segment's own shape, when the config asked for one.
8
+ *
9
+ * Returns `undefined` when no `format` was written, so the segment draws its default — which is
10
+ * usually several runs in several colours. A format is one run in one tone: full control of the
11
+ * words, at the cost of the colouring, which is the honest trade and worth saying out loud.
12
+ */
13
+ export declare function formatted(config: SegmentConfig, values: Record<string, string | number>, tone?: Tone): Piece | undefined;
@@ -0,0 +1,3 @@
1
+ /** Everything outside the conversation: service health, versions, and your own commands. */
2
+ import type { SegmentDef } from "../types.ts";
3
+ export declare const SEGMENTS: SegmentDef[];
@@ -0,0 +1,61 @@
1
+ import { type StatusContext } from "./context.ts";
2
+ /**
3
+ * The escape hatch: a shell command whose stdout becomes a segment.
4
+ *
5
+ * It is fed the same JSON on stdin that Claude Code's statusLine hook sends, so a statusline
6
+ * script someone already wrote works here unchanged. That matters more than elegance — nobody
7
+ * rewrites a working statusline to try a new editor.
8
+ *
9
+ * Unlike Claude Code's, this is not on the draw path: the command runs on its own interval and the
10
+ * line renders whatever it last returned, so a slow script makes the value stale rather than making
11
+ * the interface stutter.
12
+ */
13
+ /**
14
+ * Claude Code's statusLine stdin payload, as close as our data allows.
15
+ *
16
+ * The fields real statuslines actually read are the context-window ones — a script that draws a
17
+ * capacity bar wants `context_window.used_percentage`, not a token total it has to divide itself.
18
+ * `rate_limits` is deliberately absent: it describes an Anthropic plan's quota, which has no
19
+ * meaning behind a proxy or another provider, and inventing a number there would be worse than
20
+ * the field being missing.
21
+ */
22
+ export interface ClaudeCodeStatusInput {
23
+ hook_event_name: "Status";
24
+ session_id: string;
25
+ session_name?: string;
26
+ cwd: string;
27
+ model: {
28
+ id: string;
29
+ display_name: string;
30
+ };
31
+ workspace: {
32
+ current_dir: string;
33
+ project_dir: string;
34
+ git_worktree?: string;
35
+ };
36
+ version: string;
37
+ output_style: {
38
+ name: string;
39
+ };
40
+ cost: {
41
+ total_cost_usd: number;
42
+ total_duration_ms: number;
43
+ total_lines_added: number;
44
+ total_lines_removed: number;
45
+ };
46
+ context_window?: {
47
+ used_percentage: number;
48
+ remaining_percentage: number;
49
+ context_window_size: number;
50
+ total_input_tokens: number;
51
+ total_output_tokens: number;
52
+ };
53
+ current_usage?: {
54
+ input_tokens: number;
55
+ output_tokens: number;
56
+ cache_creation_tokens: number;
57
+ cache_read_tokens: number;
58
+ };
59
+ exceeds_200k_tokens: boolean;
60
+ }
61
+ export declare function claudeCodeInput(ctx: StatusContext): ClaudeCodeStatusInput;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Running a shell command for a segment.
3
+ *
4
+ * Unlike Claude Code's statusline, this is not on the draw path: the command runs on its own
5
+ * interval and the line renders whatever it last returned, so a slow script makes the value stale
6
+ * rather than making the interface stutter.
7
+ */
8
+ import type { CommandConfig } from "./config.ts";
9
+ import type { StatusContext } from "./context.ts";
10
+ /**
11
+ * What a command's output is worth keeping: every row, escapes and all.
12
+ *
13
+ * The colour escapes are deliberately *not* stripped — they are parsed into styled runs when the
14
+ * segment draws, so a script someone already tuned for Claude Code looks the same here. Claude
15
+ * Code statuslines may also print several rows, so the rows are kept rather than the first one.
16
+ */
17
+ export declare function cleanOutput(stdout: string): string;
18
+ /** The rows of a command's last output. */
19
+ export declare function outputRows(value: string): string[];
20
+ export interface CommandRunner {
21
+ /** The last successful output, or "" until one arrives. */
22
+ value(): string;
23
+ /** Runs if the interval has elapsed and no run is in flight. */
24
+ maybeRun(ctx: StatusContext): void;
25
+ dispose(): void;
26
+ }
27
+ export interface RunnerHost {
28
+ /** Injected so tests do not need a shell. Resolves to stdout, or rejects. */
29
+ exec(command: string, stdin: string, timeoutMs: number): Promise<string>;
30
+ now(): number;
31
+ onValue(): void;
32
+ }
33
+ export declare function createRunner(config: CommandConfig, host: RunnerHost): CommandRunner;
34
+ /** The real shell, used outside tests. */
35
+ export declare function execShell(command: string, stdin: string, timeoutMs: number): Promise<string>;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Statusline settings, read from the same two files every cockpit bay uses:
3
+ *
4
+ * ~/.config/opencode-cockpit/config.json → <project>/.cockpit.json → plugin-entry options
5
+ *
6
+ * Only the `statusline` section is read here. An unreadable or invalid file is ignored rather than
7
+ * fatal — a typo in a config should never cost you the interface.
8
+ */
9
+ export declare const CONFIG_FILE = "config.json";
10
+ export declare const PROJECT_FILE = ".cockpit.json";
11
+ /**
12
+ * Where a line is drawn.
13
+ *
14
+ * Two surfaces, deliberately. A third sat inside the prompt box, which is both the narrowest place
15
+ * in the window and the one OpenCode already fills with the agent, the model and the elapsed time:
16
+ * a line there had almost no room and almost nothing left to say.
17
+ */
18
+ export type Surface = "bottom" | "sidebar";
19
+ /**
20
+ * A segment is either a built-in named by string ("cwd"), or that name with settings. `when` and
21
+ * `priority` are what make a line survive a narrow terminal instead of wrapping into noise.
22
+ */
23
+ export interface SegmentConfig {
24
+ type: string;
25
+ /** Text placed before the value, e.g. "on ". */
26
+ prefix?: string;
27
+ suffix?: string;
28
+ /** Higher survives when the line has to be shortened. Defaults per built-in. */
29
+ priority?: number;
30
+ /** Theme colour name (`success`, `warning`, `textMuted`…) or a literal `#rrggbb`. */
31
+ color?: string;
32
+ /** Built-in specific settings, e.g. `{ "style": "bar" }` for context. */
33
+ [key: string]: unknown;
34
+ }
35
+ export interface CommandConfig {
36
+ /** Shell command whose stdout becomes the segment's text. */
37
+ run: string;
38
+ /** How often it may run. Defaults to 2000ms; it never runs more than once at a time. */
39
+ intervalMs?: number;
40
+ /** Kill and ignore the output after this long. Defaults to 1000ms. */
41
+ timeoutMs?: number;
42
+ /**
43
+ * Feed the command Claude Code's statusline JSON on stdin, so an existing statusline script
44
+ * works unchanged. On by default.
45
+ */
46
+ claudeCodeCompat?: boolean;
47
+ priority?: number;
48
+ }
49
+ /**
50
+ * How a line lays its segments out. A wide line under the prompt reads across; a sidebar four
51
+ * columns wide reads down.
52
+ */
53
+ export type Stack = "horizontal" | "vertical";
54
+ export interface LineConfig {
55
+ surface?: Surface;
56
+ segments?: (string | SegmentConfig)[];
57
+ /** Drawn between segments. Defaults to " · " across, and nothing down. */
58
+ separator?: string;
59
+ /** Defaults to vertical in the sidebar, horizontal everywhere else. */
60
+ stack?: Stack;
61
+ /** Built-in icons. On by default; switch off for a terminal missing the glyphs. */
62
+ icons?: boolean;
63
+ /** Vertical only: rows to draw at most. Lowest priority goes first. Defaults to 8. */
64
+ maxRows?: number;
65
+ /**
66
+ * Columns of space either side. The defaults line each surface up with OpenCode's own
67
+ * furniture -- its footer indents three, its prompt keeps two clear on the right -- so the line
68
+ * reads as part of the interface rather than as something bolted underneath it.
69
+ */
70
+ paddingLeft?: number;
71
+ paddingRight?: number;
72
+ paddingTop?: number;
73
+ paddingBottom?: number;
74
+ }
75
+ export interface StatusConfig {
76
+ enabled?: boolean;
77
+ /** One line, for the common case. Use `lines` for more than one surface. */
78
+ surface?: Surface;
79
+ segments?: (string | SegmentConfig)[];
80
+ separator?: string;
81
+ stack?: Stack;
82
+ /** Built-in icons. On by default; switch off for a terminal missing the glyphs. */
83
+ icons?: boolean;
84
+ lines?: LineConfig[];
85
+ /** Named commands usable as segments: `{"type": "command", "name": "budget"}`. */
86
+ commands?: Record<string, CommandConfig>;
87
+ /**
88
+ * Your own segments: paths to modules that export them by name, usable in `segments` exactly
89
+ * like the built-ins. `~` and a path relative to the project both work.
90
+ */
91
+ modules?: string[];
92
+ }
93
+ export interface CockpitStatusConfig {
94
+ statusline?: StatusConfig;
95
+ }
96
+ export declare function globalConfigPath(env?: Record<string, string | undefined>): string;
97
+ /** Reads and merges every source. `options` is the plugin entry's own options object. */
98
+ export declare function loadStatusConfig(directory: string, options?: unknown, env?: Record<string, string | undefined>): StatusConfig;
99
+ export declare function readStatusFile(path: string): StatusConfig;
100
+ /**
101
+ * Section-wise merge. `segments` is replaced rather than concatenated: a project that lists its
102
+ * own segments means "this line", not "these as well as the global ones".
103
+ */
104
+ export declare function mergeStatus(base: StatusConfig, over: StatusConfig): StatusConfig;
105
+ /**
106
+ * Accepts either a whole cockpit config (`{ statusline: {...} }`) or the statusline section on its
107
+ * own, because plugin-entry options are written straight onto the `tui.json` entry.
108
+ */
109
+ export declare function asStatusConfig(input: unknown): StatusConfig;
110
+ /**
111
+ * The default line: what someone who writes nothing at all should see.
112
+ *
113
+ * It took a long walk to arrive here, and the shape is the point. A capacity bar that means
114
+ * something at a glance, the total beside the three quantities that make it up, what changed, how
115
+ * long it has been. Colour carries which is which; the separators carry the grouping.
116
+ *
117
+ * It does repeat one thing OpenCode already shows -- the token count and the percentage, which its
118
+ * footer carries in a corner. That is deliberate. The rule is not to avoid every fact the host
119
+ * mentions, it is to avoid saying it no better than the host does: a bar you can read without
120
+ * looking, with the breakdown beside it, is a different instrument from "78.5K (39%)" in the
121
+ * corner. What stays out are the facts a second copy adds nothing to -- the path, the branch, the
122
+ * model, the spend.
123
+ */
124
+ export declare const DEFAULT_SEGMENTS: (string | SegmentConfig)[];
125
+ export declare const DEFAULT_SEPARATOR = " \u2502 ";
126
+ export interface ResolvedLine {
127
+ surface: Surface;
128
+ segments: (string | SegmentConfig)[];
129
+ separator: string;
130
+ stack: Stack;
131
+ maxRows: number;
132
+ icons: boolean;
133
+ paddingLeft: number;
134
+ paddingRight: number;
135
+ paddingTop: number;
136
+ paddingBottom: number;
137
+ }
138
+ /** Normalises whatever the config said into the lines the renderer draws. */
139
+ export declare function resolveLines(config: StatusConfig): ResolvedLine[];
140
+ /** A segment written as a bare string is that built-in with no settings. */
141
+ export declare function asSegmentConfig(entry: string | SegmentConfig): SegmentConfig;