omp-statusliney 0.0.0-stage → 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jakub Bordáš
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,169 @@
1
- # Temporary Holding Version
1
+ # omp-statusliney
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A fully customizable statusline for [Oh My Pi](https://omp.sh) (`omp`). It replaces the built-in statusline completely, including the startup prepaint, and draws a multi-line bar like ccstatusline. It keeps all 27 built-in segments and adds 25 new widgets.
4
+
5
+ ![A two-line statusliney bar: model, path, git branch and diff stats on line one; context bar, token counts, cache hit rate and cost on line two](docs/statusline.png)
6
+
7
+ ## Requirements
8
+
9
+ - omp `>=18.12.2 <19`
10
+ - Bun `>=1.3.14` (omp already runs on Bun)
11
+
12
+ ## Installation
13
+
14
+ omp-statusliney is an omp plugin. It does not work in upstream pi, because it hooks into omp's own statusline.
15
+
16
+ ### From npm
17
+
18
+ ```sh
19
+ omp plugin install omp-statusliney
20
+ ```
21
+
22
+ ### From the omp marketplace
23
+
24
+ This repository is also an omp marketplace catalog (`.omp-plugin/marketplace.json`):
25
+
26
+ ```sh
27
+ omp plugin marketplace add ignissak/omp-statusliney
28
+ omp plugin install omp-statusliney@ignissak
29
+ ```
30
+
31
+ Marketplace installs track the `main` branch. Use `omp plugin upgrade omp-statusliney@ignissak` to update.
32
+
33
+ ### From a local checkout
34
+
35
+ ```sh
36
+ git clone https://github.com/ignissak/omp-statusliney.git
37
+ cd omp-statusliney
38
+ bun install
39
+ omp plugin link "$PWD"
40
+ ```
41
+
42
+ `omp plugin link` points omp at the checkout, so later source changes take effect the next time omp starts.
43
+
44
+ Install omp-statusliney only one way at a time. omp refuses a second copy of a package it already has.
45
+
46
+ ### Check the install
47
+
48
+ ```sh
49
+ omp plugin doctor # should list "plugin:omp-statusliney"
50
+ omp # the new bar replaces the built-in one
51
+ ```
52
+
53
+ On first launch the plugin writes `~/.omp/agent/statusliney.json` with a layout built from your current built-in statusline segments. On the band and box composers it looks the same as the built-in bar. On shapes where the bar sits below the prompt, it uses your configured separator instead of the built-in plain dots.
54
+
55
+ ### Uninstall
56
+
57
+ ```sh
58
+ omp plugin uninstall omp-statusliney
59
+ ```
60
+
61
+ The built-in statusline returns on the next launch. The config file stays in place; delete `~/.omp/agent/statusliney.json` to remove it.
62
+
63
+ ## Configuration
64
+
65
+ Run `/statusliney-settings` in the TUI. Each change applies to the live bar right away and is saved automatically.
66
+
67
+ - **Lines & widgets**: 1–6 lines. Add (`a`), insert (`i`), delete (`d` twice), move (`[` `]`) and clone (`c`) widgets. Set each widget's color (theme tokens only), bold, label and options.
68
+ - **Appearance**: placement (`auto` puts the bar in the composer frame when it has one, otherwise below the prompt; or force `top`/`bottom`), separator (`inherit` follows `statusLine.separator`), background, hook status rows, refresh intervals.
69
+ - **Presets**: the built-in presets, `Two-line dev` and `Focused`. Moving through the list previews each preset on the live bar. Enter applies it; Esc restores your layout.
70
+ - **Reset to built-in layout**: copy your current built-in statusline settings again.
71
+
72
+ You can also edit the JSON by hand; the plugin picks up external edits while omp is running. Set `OMP_STATUSLINEY_CONFIG` to use a different file.
73
+
74
+ The bar follows omp's theme and symbol preset (all glyphs and colors come from the theme). It also honors your `statusLine.*` preferences (segment options, transparent, session accent, compact thinking level, context line, hook status) and the composer shape.
75
+
76
+ ### Widgets
77
+
78
+ | Category | Widgets |
79
+ |---|---|
80
+ | omp built-in | every built-in segment (`pi`, `model`, `path`, `git`, `pr`, `cost`, `context_pct`, `session_name`, `usage`, `vim`, …) |
81
+ | Git | `git_ahead_behind`, `git_sha`, `git_changes`, `git_stash`, `git_clean` |
82
+ | Context | `context_gauge` (the built-in context rule), `context_bar`, `context_remaining`, `quota_bar` |
83
+ | Session | `cost_rate`, `turns`, `compactions`, `session_clock`, `tool` |
84
+ | Environment | `jobs`, `thinking`, `provider`, `omp_version`, `terminal_width`, `memory` |
85
+ | Custom | `text`, `custom_command` (first output line, cached), `link` (OSC 8) |
86
+ | Layout | `separator`, `flex` |
87
+
88
+ ## Local development
89
+
90
+ ### Setup
91
+
92
+ ```sh
93
+ bun install
94
+ ```
95
+
96
+ This installs the `@oh-my-pi/*` packages as dev dependencies for types and tests. At runtime omp maps every `@oh-my-pi/*` import to its own bundled modules, so the plugin always runs against the live host.
97
+
98
+ The plugin is TypeScript that Bun runs directly; there is no build step. The entry point is `src/index.ts`, declared under `omp.extensions` in `package.json`.
99
+
100
+ ### Run omp with your working copy
101
+
102
+ Load the checkout as an extension without installing it, and use a scratch config so your real one is untouched:
103
+
104
+ ```sh
105
+ export OMP_STATUSLINEY_CONFIG=/tmp/sly/statusliney.json
106
+ omp --no-session -e "$PWD"
107
+ ```
108
+
109
+ - `--no-session` skips writing a session file.
110
+ - Add `--no-extensions` to load only this plugin, without your other plugins.
111
+ - Restart omp after each source change.
112
+ - Delete the scratch config to start again from the mirrored built-in layout.
113
+
114
+ ### Tests and type checks
115
+
116
+ ```sh
117
+ bun test # config normalization, store round-trip, layout engine, presets
118
+ bun run typecheck # tsc --noEmit
119
+ ```
120
+
121
+ ### Project layout
122
+
123
+ | Path | Purpose |
124
+ |---|---|
125
+ | `src/index.ts` | Plugin entry: events, ticker, `/statusliney-settings` command |
126
+ | `src/takeover.ts` | Patches omp's `StatusLineComponent`, captures the live segment context, scrubs the startup cache |
127
+ | `src/frame.ts` | Placement, surfaces, hook rows, per-frame rendering |
128
+ | `src/layout.ts` | Pure line layout: groups, separators, flex fill, overflow |
129
+ | `src/config.ts`, `src/store.ts` | Config schema, validation, atomic save, file watch |
130
+ | `src/presets.ts` | Built-in mirror and named presets |
131
+ | `src/widgets/` | Widget contract, registry and widget implementations |
132
+ | `src/ui/` | Settings overlay and its views |
133
+ | `test/` | Bun tests |
134
+
135
+ ### Debugging
136
+
137
+ - Plugin messages go to omp's log file, `~/.omp/logs/omp.<DATE>.<PID>.log`, which records every level by default. Filter with `grep statusliney`. Set `OMP_LOG_LEVEL` (`error|warn|info|debug`) to limit what is written.
138
+ - The plugin patches omp internals. If an omp release removes a member it patches, the plugin leaves the built-in bar in place and shows a `statusliney: unsupported omp internals` warning. If rendering keeps failing at runtime (three frames in a row), it restores the built-in bar and warns. Fix the patch targets in `src/takeover.ts` (`canTakeOver`).
139
+ - On native TSP terminals the plugin passes your real settings through, so the terminal's own statusline is unchanged.
140
+
141
+ ## Releasing
142
+
143
+ Releases are automated by `.github/workflows/release.yml`. To release, bump `version` in `package.json` and push it to `main`:
144
+
145
+ ```sh
146
+ npm version minor --no-git-tag-version # or patch / major, or edit package.json by hand
147
+ git commit -am "chore(release): 1.1.0"
148
+ git push
149
+ ```
150
+
151
+ The workflow then:
152
+
153
+ 1. Runs the tests and the type check.
154
+ 2. Publishes the version to npm with provenance, unless it is already there.
155
+ 3. Creates the tag `v<version>` and a GitHub release. The release notes come from the commits since the previous tag, grouped by their Conventional Commit type (breaking changes, features, fixes, other). Version-bump commits are left out.
156
+
157
+ Each step skips itself if it is already done, so a failed run can simply be re-run. Preview the notes locally with `bun scripts/release-notes.ts v1.1.0`.
158
+
159
+ The npm package carries the `pi-package` keyword and a `pi.image` preview, so it also appears in the [pi package gallery](https://pi.dev/packages) once it is on npm.
160
+
161
+ ### One-time setup
162
+
163
+ npm only allows trusted publishing for a package that already exists, so the first version is published by hand:
164
+
165
+ 1. `npm login`, then `npm publish --access public` from a clean checkout of `main`.
166
+ 2. On npmjs.com, open the package's **Settings → Trusted publishing** and add GitHub Actions with repository `ignissak/omp-statusliney` and workflow `release.yml`.
167
+ 3. Push `main`, or run the **Release** workflow by hand. It sees the version already on npm, so it only creates the tag and the release.
168
+
169
+ After that, no npm token is needed: the workflow authenticates through GitHub's OIDC token.
package/package.json CHANGED
@@ -1,6 +1,65 @@
1
1
  {
2
2
  "name": "omp-statusliney",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.1",
4
+ "description": "Fully customizable statusline for Oh My Pi (omp)",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "omp",
9
+ "oh-my-pi",
10
+ "omp-plugin",
11
+ "statusline",
12
+ "pi-package"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/ignissak/omp-statusliney.git"
17
+ },
18
+ "homepage": "https://github.com/ignissak/omp-statusliney#readme",
19
+ "bugs": {
20
+ "url": "https://github.com/ignissak/omp-statusliney/issues"
21
+ },
22
+ "omp": {
23
+ "description": "Replaces the omp statusline with a configurable multi-line bar",
24
+ "extensions": [
25
+ "./src/index.ts"
26
+ ]
27
+ },
28
+ "pi": {
29
+ "image": "https://raw.githubusercontent.com/ignissak/omp-statusliney/main/docs/statusline.png"
30
+ },
31
+ "files": [
32
+ "src",
33
+ "README.md"
34
+ ],
35
+ "engines": {
36
+ "bun": ">=1.3.14"
37
+ },
38
+ "peerDependencies": {
39
+ "@oh-my-pi/pi-coding-agent": ">=18.12.2 <19",
40
+ "@oh-my-pi/pi-tui": ">=18.12.2 <19",
41
+ "@oh-my-pi/pi-utils": ">=18.12.2 <19"
42
+ },
43
+ "peerDependenciesMeta": {
44
+ "@oh-my-pi/pi-coding-agent": {
45
+ "optional": true
46
+ },
47
+ "@oh-my-pi/pi-tui": {
48
+ "optional": true
49
+ },
50
+ "@oh-my-pi/pi-utils": {
51
+ "optional": true
52
+ }
53
+ },
54
+ "devDependencies": {
55
+ "@oh-my-pi/pi-coding-agent": "18.12.2",
56
+ "@oh-my-pi/pi-tui": "18.12.2",
57
+ "@oh-my-pi/pi-utils": "18.12.2",
58
+ "@types/bun": "latest",
59
+ "typescript": "^5.9.0"
60
+ },
61
+ "scripts": {
62
+ "test": "bun test",
63
+ "typecheck": "tsc --noEmit"
64
+ }
65
+ }
package/src/config.ts ADDED
@@ -0,0 +1,163 @@
1
+ import * as path from "node:path";
2
+ import type { StatusLineSeparatorStyle } from "@oh-my-pi/pi-tui/status-line";
3
+ import { STATUS_LINE_SEPARATOR_VALUES } from "@oh-my-pi/pi-tui/status-line/schema";
4
+ import type { ThemeColor } from "@oh-my-pi/pi-tui/theme";
5
+ import { getAgentDir, isRecord } from "@oh-my-pi/pi-utils";
6
+ import { getWidget, WIDGET_COLOR_TOKENS } from "./widgets/registry";
7
+
8
+ export type Placement = "auto" | "top" | "bottom";
9
+ export type SeparatorChoice = "inherit" | StatusLineSeparatorStyle;
10
+ export type TriState = "inherit" | "on" | "off";
11
+ export type BackgroundChoice = "auto" | "on" | "off";
12
+ export type OptionValue = string | number | boolean;
13
+
14
+ export interface WidgetItem {
15
+ id: string;
16
+ type: string;
17
+ color?: ThemeColor;
18
+ bold?: boolean;
19
+ label?: string;
20
+ options?: Record<string, OptionValue>;
21
+ }
22
+
23
+ export interface StatuslineyConfig {
24
+ version: 1;
25
+ placement: Placement;
26
+ separator: SeparatorChoice;
27
+ background: BackgroundChoice;
28
+ hookStatusRows: TriState;
29
+ /** Repaint interval for clock-driven widgets, 250..10000 ms. */
30
+ refreshMs: number;
31
+ /** Git extras refresh interval, 1000..60000 ms. */
32
+ gitRefreshMs: number;
33
+ /** 1..6 lines; a line may be empty. */
34
+ lines: WidgetItem[][];
35
+ }
36
+
37
+ export const PLACEMENTS: readonly Placement[] = ["auto", "top", "bottom"];
38
+ export const SEPARATOR_CHOICES: readonly SeparatorChoice[] = ["inherit", ...STATUS_LINE_SEPARATOR_VALUES];
39
+ export const BACKGROUND_CHOICES: readonly BackgroundChoice[] = ["auto", "on", "off"];
40
+ export const TRI_STATES: readonly TriState[] = ["inherit", "on", "off"];
41
+ export const MAX_LINES = 6;
42
+ export const REFRESH_RANGE = { min: 250, max: 10_000, default: 1_000 } as const;
43
+ export const GIT_REFRESH_RANGE = { min: 1_000, max: 60_000, default: 5_000 } as const;
44
+
45
+ export function defaultConfigPath(): string {
46
+ return process.env.OMP_STATUSLINEY_CONFIG ?? path.join(getAgentDir(), "statusliney.json");
47
+ }
48
+
49
+ function pick<T extends string>(values: readonly T[], value: unknown, fallback: T): T {
50
+ return values.find(candidate => candidate === value) ?? fallback;
51
+ }
52
+
53
+ function clampInt(value: unknown, range: { min: number; max: number; default: number }): number {
54
+ if (typeof value !== "number" || !Number.isFinite(value)) return range.default;
55
+ return Math.min(range.max, Math.max(range.min, Math.round(value)));
56
+ }
57
+
58
+ /** Validate one option value against its definition; undefined drops it. */
59
+ export function normalizeOptionValue(
60
+ def: { kind: string; values?: readonly string[]; min?: number; max?: number },
61
+ value: unknown,
62
+ ): OptionValue | undefined {
63
+ switch (def.kind) {
64
+ case "bool":
65
+ return typeof value === "boolean" ? value : undefined;
66
+ case "enum":
67
+ return typeof value === "string" && value !== "inherit" && def.values?.includes(value) ? value : undefined;
68
+ case "number": {
69
+ if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
70
+ const rounded = Math.round(value);
71
+ return Math.min(def.max ?? rounded, Math.max(def.min ?? rounded, rounded));
72
+ }
73
+ case "string":
74
+ return typeof value === "string" ? value : undefined;
75
+ default:
76
+ return undefined;
77
+ }
78
+ }
79
+
80
+ function normalizeItem(raw: unknown, warnings: string[]): WidgetItem | undefined {
81
+ if (!isRecord(raw) || typeof raw.type !== "string") {
82
+ warnings.push("statusliney: dropped a widget without a type");
83
+ return undefined;
84
+ }
85
+ const def = getWidget(raw.type);
86
+ if (!def) {
87
+ warnings.push(`statusliney: unknown widget type "${raw.type}" dropped`);
88
+ return undefined;
89
+ }
90
+ const item: WidgetItem = {
91
+ id: typeof raw.id === "string" && raw.id ? raw.id : crypto.randomUUID(),
92
+ type: raw.type,
93
+ };
94
+ if (raw.color !== undefined) {
95
+ const color = WIDGET_COLOR_TOKENS.find(token => token === raw.color);
96
+ if (color) item.color = color;
97
+ else warnings.push(`statusliney: unknown color "${String(raw.color)}" dropped from ${raw.type}`);
98
+ }
99
+ if (typeof raw.bold === "boolean") item.bold = raw.bold;
100
+ if (typeof raw.label === "string" && raw.label) item.label = raw.label;
101
+ if (isRecord(raw.options)) {
102
+ const options: Record<string, OptionValue> = {};
103
+ for (const [key, value] of Object.entries(raw.options)) {
104
+ const optionDef = def.options.find(option => option.key === key);
105
+ const normalized = optionDef ? normalizeOptionValue(optionDef, value) : undefined;
106
+ if (normalized === undefined) {
107
+ warnings.push(`statusliney: invalid option "${key}" dropped from ${raw.type}`);
108
+ continue;
109
+ }
110
+ options[key] = normalized;
111
+ }
112
+ if (Object.keys(options).length > 0) item.options = options;
113
+ }
114
+ return item;
115
+ }
116
+
117
+ /** Type-guarded validation with defaults; never throws. */
118
+ export function normalizeConfig(
119
+ raw: unknown,
120
+ fallbackLines: () => WidgetItem[][],
121
+ ): { config: StatuslineyConfig; warnings: string[]; linesFallback: boolean } {
122
+ const warnings: string[] = [];
123
+ const source = isRecord(raw) ? raw : {};
124
+ let lines: WidgetItem[][] = [];
125
+ const linesFallback = !Array.isArray(source.lines);
126
+ if (Array.isArray(source.lines)) {
127
+ const rawLines: unknown[] = source.lines;
128
+ for (const [index, line] of rawLines.entries()) {
129
+ if (!Array.isArray(line)) {
130
+ warnings.push(`statusliney: line ${index + 1} is not a list of widgets; dropped`);
131
+ continue;
132
+ }
133
+ if (lines.length === MAX_LINES) {
134
+ warnings.push(`statusliney: only the first ${MAX_LINES} lines are used`);
135
+ break;
136
+ }
137
+ const items: WidgetItem[] = [];
138
+ for (const entry of line) {
139
+ const item = normalizeItem(entry, warnings);
140
+ if (item) items.push(item);
141
+ }
142
+ lines.push(items);
143
+ }
144
+ if (lines.length === 0) lines = [[]];
145
+ } else {
146
+ if (source.lines !== undefined) warnings.push("statusliney: invalid lines; using the built-in layout");
147
+ lines = fallbackLines();
148
+ }
149
+ return {
150
+ config: {
151
+ version: 1,
152
+ placement: pick(PLACEMENTS, source.placement, "auto"),
153
+ separator: pick(SEPARATOR_CHOICES, source.separator, "inherit"),
154
+ background: pick(BACKGROUND_CHOICES, source.background, "auto"),
155
+ hookStatusRows: pick(TRI_STATES, source.hookStatusRows, "inherit"),
156
+ refreshMs: clampInt(source.refreshMs, REFRESH_RANGE),
157
+ gitRefreshMs: clampInt(source.gitRefreshMs, GIT_REFRESH_RANGE),
158
+ lines,
159
+ },
160
+ warnings,
161
+ linesFallback,
162
+ };
163
+ }