opencode-usage-coach 0.8.2 → 0.8.4

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 (3) hide show
  1. package/README.md +62 -238
  2. package/dist/tui.js +83 -70
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,269 +1,93 @@
1
1
  # opencode-usage-coach
2
2
 
3
- A closed-loop usage coach and harness for [OpenCode](https://opencode.ai). Built for
4
- flat-rate / quota-metered coding plans where USD cost is meaningless, so it tracks
5
- **quota windows (5h / weekly / monthly)** and turns them into coaching + loop control.
6
- Provider-agnostic — configure via `harness.config.json`.
7
-
8
- Existing plugins only *display* usage. This one **senses quota → coaches → stops/advances
9
- the loop** — and ships a harness agent mode.
10
-
11
- ## What it does
12
-
13
- **Always-on guardian (plugin):**
14
- - Senses provider quota windows via the `codexbar` CLI (config-driven any provider codexbar knows).
15
- - On STOP threshold: blocks tool calls (`tool.execute.before` throws) the agent self-stops.
16
- - Injects coaching (how to use right now) into the system prompt (double defense).
17
- - Surfaces a sidebar panel: per-provider quota meters + harness task states + coaching.
18
- - Defensive: any plugin error never breaks opencode.
19
-
20
- **Harness (agent mode, 1 terminal):**
21
- - The `usage-coach-harness` agent triages each request — trivial → do directly;
22
- unclear clarify; substantive generate→grade→revise→advance.
23
- - Multi-model via plugin tools `generate`/`grade`: run the configured generator/grader model
24
- (from `harness.config.json`) in a new session on the same server — no second terminal.
25
- - Reports progress via `task_update` → the sidebar panel shows live task states.
26
- - **Triggering the harness:** the agent auto-triages — trivial work is done directly,
27
- substantive multi-step work enters the generate→grade loop. For reliable triggering,
28
- explicitly ask "run this through the harness" or "use the harness for this". The harness
29
- tools are only available when the harness agent mode is active (see Install).
30
-
31
- **Learning from failures (learning loop):**
32
- - When `grade` returns FAIL, the harness enters a learning cycle: `record_failure` → `investigate` (root-cause analysis) → `verify_diagnosis` → `generalize` (extract a reusable rule).
33
- - Rules accumulate in `rules.md` → the next `generate` call automatically includes them → the harness avoids repeating the same mistake.
34
- - Tools: `record_failure`, `investigate`, `verify_diagnosis`, `generalize`.
35
-
36
- **Domain knowledge base:**
37
- - `investigate` and `generate` query a local domain DB before running — known facts are injected into the prompt ("Known facts from domain DB: ...").
38
- - Unknown domains are investigated (webfetch/docs) and stored as graph nodes/edges → accumulates over time → evidence-based judgments instead of speculation.
39
- - Storage: `nodes.ndjson` + `edges.ndjson` under the project state dir.
40
-
41
- ## Requirements
42
- - opencode (tested on 1.17.13) with a quota-metered provider configured.
43
- - `codexbar` CLI with your provider key wired (e.g. `codexbar config set-api-key --provider zai --stdin`).
44
-
45
- ## Install (from npm)
3
+ A closed-loop usage coach and harness for [OpenCode](https://opencode.ai). Built for flat-rate / quota-metered coding plans — it **senses quota → coaches → stops/advances the loop**. Provider-agnostic, configurable via `harness.config.json`.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/opencode-usage-coach)](https://www.npmjs.com/package/opencode-usage-coach) [![license](https://img.shields.io/npm/l/opencode-usage-coach)](./LICENSE) [![coverage](https://img.shields.io/badge/coverage-planned-yellow)](#) [![Ko-fi](https://img.shields.io/badge/Ko--fi-sponsor-FF5E5B)](https://ko-fi.com/lhjnano) [![GitHub Sponsors](https://img.shields.io/badge/GitHub-Sponsors-ea4aaa)](https://github.com/sponsors/lhjnano)
6
+
7
+ ## Features
8
+
9
+ - **Quota guardian** senses provider quota windows (5h / weekly / monthly) via the `codexbar` CLI.
10
+ - **Automatic loop control** — STOP threshold blocks tool calls so the agent self-stops; throttle advice downshifts models.
11
+ - **Coaching injection** — real-time guidance on how to use remaining quota, injected into the system prompt.
12
+ - **Sidebar panel** — per-provider quota meters + live harness task states (SolidJS, `Alt+H` to toggle).
13
+ - **Harness agent mode** — triages requests and runs a generate → grade → revise loop, multi-model in one terminal.
14
+ - **Learning loop** failures are investigated, verified, and generalized into reusable rules (`rules.md`).
15
+ - **Domain knowledge base** a local graph store that injects known facts into prompts, reducing speculation.
16
+ - **Pre-flight gap analysis** inspired by Anthropic's Unknowns Matrix; surfaces blind spots before generation starts.
17
+ - **Provider-agnostic** any provider `codexbar` knows; configure once in `harness.config.json`.
18
+ - **Session isolation** per-session harness state; no cross-session leakage.
19
+
20
+ ## How it works
21
+
22
+ The plugin runs in two parts inside a single opencode terminal. The **server module** senses quota, decides GO/THROTTLE/STOP, and exposes custom harness tools (`generate`, `grade`, `record_failure`, …). The **TUI module** renders quota meters and live task states into the sidebar. The **harness agent** triages each request — trivial work is done directly; substantive work enters the generate→grade→revise loop.
23
+
24
+ See **[docs/architecture.md](docs/architecture.md)** for the full design.
25
+
26
+ ## Quick Start
27
+
46
28
  ```jsonc
47
- // ~/.config/opencode/opencode.json (server plugin)
29
+ // ~/.config/opencode/opencode.json server plugin
48
30
  { "plugin": ["opencode-usage-coach"] }
49
31
 
50
- // ~/.config/opencode/tui.json (TUI panel)
32
+ // ~/.config/opencode/tui.json — sidebar panel (point at built dist/tui.js)
51
33
  { "$schema": "https://opencode.ai/tui.json", "plugin": ["opencode-usage-coach/tui"] }
52
34
  ```
53
- Drop `agents/usage-coach-harness.md` into `~/.config/opencode/agents/` for the agent mode.
54
35
 
55
- ## Local dev / install (without npm)
56
- ```bash
57
- bun install
58
- bun run build # -> dist/index.js, dist/tui.js (solid external)
59
- # server plugin
60
- cp dist/index.js ~/.config/opencode/plugins/opencode-usage-coach.js
61
- # TUI plugin — point tui.json at the built file (NOT the plugins/ dir)
62
- # ~/.config/opencode/tui.json: { "plugin": ["/abs/path/dist/tui.js"] }
63
- ```
36
+ Then wire `codexbar` and drop `agents/usage-coach-harness.md` into `~/.config/opencode/agents/` for agent mode:
64
37
 
65
- ## Prompting the harness
38
+ ```bash
39
+ # provider quota data source
40
+ printf '%s' "$YOUR_PROVIDER_API_KEY" | codexbar config set-api-key --provider <id> --stdin
66
41
 
67
- How to trigger and use the harness loop for substantive work.
42
+ # harness role model mapping (place in your work directory)
43
+ cp harness.config.example.json harness.config.json # edit generator/grader
44
+ ```
68
45
 
69
- ### Triggering
70
- With the harness agent mode active (`agents/usage-coach-harness.md` installed), ask for substantive work — explicitly or just describe a multi-step task:
71
- - `"run this through the harness: write CONTRIBUTING.md from git log"`
72
- - `"harness: add TypeScript strict mode across the repo"`
73
- - Or just describe the work; the agent triages and enters the loop.
46
+ For local dev without npm: `bun install && bun run build`, then point both configs at the `dist/` files.
74
47
 
75
- Trivial requests (a one-line fix, a direct answer) are done directly — the loop only runs when generate→grade adds value.
48
+ ## Configuration
76
49
 
77
- ### Independent vs dependent tasks
78
- The harness picks the loop path based on task dependency:
79
- - **Independent** (task B doesn't need A's output) → `generate_batch` runs all tasks in **parallel** (faster). Example: `"CONTRIBUTING.md, PR template, issue template"` — three separate docs.
80
- - **Dependent** (B needs A) → **sequential** `generate` calls. Example: `"1) schema, 2) migration, 3) API"` — each needs the prior.
50
+ Four config surfaces; only the first two (install config + codexbar) are required to run. The **harness config** (`harness.config.json`) maps roles to models so per-model quota is tracked — set `generator` (required) and optionally `grader`, `lighterModel`, and `provider`. Thresholds and tuning live in env vars (`UC_STOP_5H`, `UC_THROTTLE_5H`, …).
81
51
 
82
- ### Deterministic NEXT directives
83
- Each tool appends a `[usage-coach NEXT]` line to its return value, telling the agent exactly what to call next:
84
- - `generate` → NEXT: grade the work
85
- - `grade` PASS → NEXT: mark completed, proceed
86
- - `grade` FAIL → NEXT: revise (up to 2x) or mark failed
52
+ See **[docs/configuration.md](docs/configuration.md)** for the full reference (env var table, harness config fields, agent-mode scoping, local dev setup).
87
53
 
88
- The agent follows NEXT — you just describe the work, the loop runs itself.
54
+ ## Harness Loop
89
55
 
90
- ### Quota-aware (automatic)
91
- You don't manage quota the tools adapt:
92
- - **GO** + headroom strong generator, full parallel
93
- - **THROTTLE** → auto-switch to `lighterModel`, concurrency capped at 2
94
- - **STOP** loop halts
56
+ ```
57
+ request triage trivial? done directly
58
+ substantive? generategrade ─┬─ PASS → completed
59
+
60
+ ┌── (FAIL, up to 2×) ──────┘
61
+
62
+ record_failure → investigate → verify → generalize → rules.md
63
+
64
+ next generate call includes rules ◄─┘
65
+ ```
95
66
 
96
- Set `lighterModel` in `harness.config.json` to enable THROTTLE switching.
67
+ Independent tasks run in parallel (`generate_batch`); dependent tasks run sequentially. The loop is quota-aware: **GO** → full power, **THROTTLE** → lighter model + capped concurrency, **STOP** → halt.
97
68
 
98
- ## Config
69
+ See **[docs/architecture.md](docs/architecture.md)** for details on the loop, NEXT directives, and quota-aware switching.
99
70
 
100
- This plugin has **four config surfaces**. Only the first two are required to run.
71
+ ## Troubleshooting
101
72
 
102
- ### 1. Install config (where opencode loads the plugin from)
103
- ```jsonc
104
- // ~/.config/opencode/opencode.json — server plugin (guardian + harness tools)
105
- { "plugin": ["opencode-usage-coach"] } // from npm, OR a local path:
106
- // { "plugin": ["/abs/path/dist/index.js"] }
73
+ Common issues: TUI panel missing (check `tui.json` points at `dist/tui.js`, never install `solid-js` in the config dir), model selection errors (`generator` is required), and tool-abort timeouts (platform limit — split large tasks).
107
74
 
108
- // ~/.config/opencode/tui.json TUI panel (MUST point at the built dist/tui.js)
109
- { "$schema": "https://opencode.ai/tui.json",
110
- "plugin": ["/abs/path/dist/tui.js"] }
75
+ See **[docs/troubleshooting.md](docs/troubleshooting.md)** for full diagnoses and fixes.
111
76
 
112
- // ~/.config/opencode/agents/usage-coach-harness.md — agent mode (copy from agents/)
113
- ```
114
- > The TUI file MUST be loaded via `tui.json` file path (not the `plugins/` dir), and MUST be
115
- > the compiled `dist/tui.js` (raw `.tsx` / installing `solid-js` crashes opencode).
77
+ ## Contributing
116
78
 
117
- ### 2. codexbar (quota data source)
118
- ```bash
119
- printf '%s' "$YOUR_PROVIDER_API_KEY" | codexbar config set-api-key --provider <id> --stdin
120
- ```
79
+ Bug reports and pull requests are welcome. Run `bun run build` before submitting, and keep `lint` + `typecheck` clean:
121
80
 
122
- ### 3. Harness config — `harness.config.json` (role → model) ★ the main one
123
- Place in the **work directory**. Each role runs on its model, so per-model quota is tracked
124
- (works for local LLMs — they show 0%).
125
- ```jsonc
126
- {
127
- "generator": "opencode/deepseek-v4-flash-free", // model that produces the work
128
- "grader": "opencode/mimo-v2.5-free", // model that grades it
129
- "provider": "", // codexbar quota provider ("" = default)
130
- "lighterModel": "" // suggested when throttling
131
- }
132
- ```
133
- | Field | Default | Notes |
134
- |---|---|---|
135
- | `generator` | **required** | any `provider/model` opencode knows |
136
- | `grader` | falls back to `generator` | can differ (multi-model) |
137
- | `provider` | `""` (codexbar default) | which provider's quota the guardian watches |
138
- | `lighterModel` | `""` | shown in throttle advice; env `UC_LIGHTER_MODEL` overrides |
139
-
140
- `generator` is **required** — the tools return a clear error if missing. Copy
141
- `harness.config.example.json` to get started (both example models are free → never gated).
142
-
143
- ### 4. Env vars (thresholds / tuning)
144
- | Var | Default | Meaning |
145
- |---|---|---|
146
- | `UC_STOP_5H` | 92 | 5h window STOP % (blocks tools) |
147
- | `UC_THROTTLE_5H` | 70 | 5h window throttle % |
148
- | `UC_STOP_WEEKLY` | 95 | weekly STOP % |
149
- | `UC_THROTTLE_WEEKLY` | 85 | weekly throttle % |
150
- | `UC_STOP_MONTHLY` | 98 | monthly STOP % |
151
- | `UC_LIGHTER_MODEL` | (config `lighterModel`) | suggested model when throttling |
152
- | `UC_PROVIDER` | (config `provider`) | codexbar provider for the guardian |
153
- | `UC_TTL_MS` | 60000 | quota cache TTL (ms) |
154
- | `UC_DEBUG` | 0 | set to `1` for a diagnostic log at `~/.cache/opencode-usage-coach/coach.log` |
155
- | `UC_HARNESS_AGENT` | `Usage-Coach-Harness` | comma-separated agent modes allowed to use harness tools + receive quota coaching (case-insensitive; must match the agent id, e.g. `usage-coach-harness` from `agents/usage-coach-harness.md`) |
156
- | `UC_WORM_MAX_AGE_DAYS` | 180 | domain DB worm (GC): drop nodes not accessed in N days (~6 months) |
157
- | `UC_WORM_MAX_NODES` | 100000 | domain DB worm (GC): cap node count, evict oldest-accessed beyond this |
158
-
159
- ## Agent-mode scoping
160
-
161
- Harness tools (`generate`, `grade`, `harness_start`, …) and quota coaching are **scoped to
162
- the `usage-coach-harness` agent mode**. Other modes (build, general, your custom agents) stay
163
- completely clean — no harness tools in their tool list, no quota coaching injected into their
164
- system prompt.
165
-
166
- This is enforced on two independent layers (defense in depth):
167
-
168
- 1. **Agent definition** (`agents/usage-coach-harness.md`) — its `permission` allowlist names the
169
- harness tools, so they only appear in this mode. Other agents' permission lists don't name
170
- them, so they're hidden from those modes automatically (this is the standard opencode
171
- mechanism — tool visibility is the agent definition's responsibility).
172
- 2. **Plugin runtime gate** (`tool.execute.before`) — even if a harness tool were somehow
173
- invoked, the plugin resolves the current session's agent (`client.session.get` → `info.agent`,
174
- 60s-cached) and throws unless it matches `UC_HARNESS_AGENT` (default `Usage-Coach-Harness`,
175
- case-insensitive). The quota system-prompt injection is gated the same way.
176
-
177
- **To use the harness tools**, switch to the `usage-coach-harness` agent mode.
178
-
179
- **To allow additional modes**, set `UC_HARNESS_AGENT` to a comma-separated list:
180
81
  ```bash
181
- export UC_HARNESS_AGENT="usage-coach-harness,my-other-harness"
82
+ bun run lint && bun run typecheck
182
83
  ```
183
84
 
184
- > Why not the v2 plugin API? v2 has no `tool` registration domain, so a plugin that provides
185
- > custom tools (like this one) cannot be fully rewritten in v2. Agent `permission` allowlists +
186
- > the v1 runtime gate is the structurally correct way to scope tool visibility.
85
+ ## License
187
86
 
188
- ## Architecture
189
- - **Server module** (`src/index.ts`) — SENSE/DECIDE/ACT + custom harness tools. Loaded via `opencode.json`.
190
- - **TUI module** (`src/tui.tsx`) — SolidJS, reads a state file, renders into `sidebar_footer`/`home_footer`. Loaded via `tui.json`. Bundled with `tsup` + `esbuild-plugin-solid`, solid kept **external** (resolves to opencode's bundle — avoids the duplicate-instance crash). Exports `{ tui }` (a bare function is misread as a server plugin).
191
- - server↔TUI communicate via a state file (`~/.cache/opencode-usage-coach/*.json`) — they are separate processes.
87
+ [MIT](./LICENSE) © opencode-usage-coach contributors
192
88
 
193
- **Key lessons (hard-won):** never install `solid-js` in the config dir (conflicts with
194
- opencode's bundled solid); TUI plugins must be compiled + loaded via `tui.json` file path;
195
- `codexbar` must be called via `spawn` (the `$` BunShell leaks output to the TUI).
89
+ ## Sponsor
196
90
 
197
- ## Troubleshooting
91
+ If this saves your quota budget, consider supporting development:
198
92
 
199
- Recurring issues and fixes — mostly learned the hard way during development.
200
-
201
- ### TUI panel missing or harness not shown
202
- - **Check `tui.json` points at the `dist/tui.js` file path** — not the `plugins/` directory.
203
- ```jsonc
204
- // ~/.config/opencode/tui.json
205
- { "$schema": "https://opencode.ai/tui.json", "plugin": ["/abs/path/dist/tui.js"] }
206
- ```
207
- - **Do NOT install `solid-js` in the config dir** — conflicts with opencode's bundled solid, causes a crash. Keep it peer + external only.
208
- - **Export must be `{ tui }`** (an object) — a bare function is misread as a server plugin.
209
- - **Color prop is `fg`** (not `foreground`): `style={{ fg: theme.current.success }}`.
210
- - **Call `codexbar` via `spawn`** — the `$` BunShell leaks command output into the TUI.
211
- - **Harness not visible?** The TUI shows the **current session's** harness only. A finished harness (`active:false`) is hidden. A harness started in another session won't appear here — switch to that session to see it.
212
-
213
- ### LLM model selection (generate/grade)
214
- - **`generator` is required in `harness.config.json`** — the tools return a clear error if missing (the old z.ai fallback was removed in v0.2.4).
215
- - Format: `"provider/model"` (e.g. `"opencode/deepseek-v4-flash-free"`, `"opencode/mimo-v2.5-free"` — see `harness.config.example.json`).
216
- - **Config precedence**: workdir `harness.config.json` > global `~/.config/opencode-usage-coach/harness.config.json`.
217
- - Omitting `grader` falls back to `generator`. If neither is set, the grade tool returns FAIL + guidance.
218
- - `provider` (quota source) and `lighterModel` (suggested on throttle) are also configurable in the same file.
219
-
220
- ### Parallel harness execution (generate_batch)
221
- - **Only INDEPENDENT tasks should be batched** — dependent tasks (B needs A) require sequential `generate` calls.
222
- - `generate_batch` runs each task in a separate sub-session on the same server (shared model config).
223
- - runModel polls the sub-session until `idle`/`completed` (max 10 min). Trace with `UC_DEBUG=1`:
224
- ```
225
- runModel(<generator>): session xxx created, prompt 142 chars
226
- runModel(<generator>): poll 3s status="running"
227
- runModel(<generator>): done 48s, 1203 chars
228
- ```
229
- - `(no output)` means the sub-session returned no text. Check `~/.cache/opencode-usage-coach/coach.log` for the runModel trace (requires `UC_DEBUG=1`).
230
- - **Note**: runModel only terminates on explicit `idle`/`completed` status. An earlier bug broke on `!status` (undefined) immediately — fixed.
231
-
232
- ### generate / generate_batch returns "Tool execution aborted"
233
- - **Cause:** opencode imposes a tool execution timeout (~60–120s, not configurable). `runModel` polls the sub-session for up to 10 min; if the generator model takes longer than the tool timeout, opencode aborts the call.
234
- - **This is a platform limit, not a plugin bug** — there is no config key to extend it.
235
- - **Mitigation:** split large tasks into smaller ones that finish within the timeout. The NEXT directives + revise loop help — a FAIL triggers a focused revise rather than a monolithic retry.
236
- - The sub-session keeps running in the background after abort; its file writes are preserved. Only the tool's return value is lost.
237
-
238
- ### Multi-session (per-session state isolation)
239
- - Each opencode session has a unique sessionID; harness state is isolated per-session:
240
- ```
241
- ~/.cache/opencode-usage-coach/projects/<dir-hash>/<sessionID>/harness.json
242
- ```
243
- - Different working directories get separate project state (keyed by path hash).
244
- - The TUI shows the **current session's** harness only (per-session isolation) — no cross-session leakage. A harness running in session B does not appear in session A's panel.
245
- - Harness completion sets `active:false` → hidden from the TUI.
246
- - Override the state path with `UC_STATE_DIR` (forces global state).
247
-
248
- **Key gotcha — opencode TUI `ctx` does NOT carry `session_id`.**
249
- The slot context passed to `panel(ctx)` contains only `{ theme }`. There is no `session_id`/`sessionID` field. The current session ID lives in **`api.route.current.params.sessionID`** instead — the panel reads it from there. If you ever see harnesses from other sessions leaking in, the cause is almost certainly that `sid` resolved to empty (→ fallback broad scan).
250
-
251
- **Debugging session isolation** (if it breaks again):
252
- 1. Check `~/.cache/opencode-usage-coach/projects/<hash>/tui-debug.log` — is `panel` being called? What `routeSid` value?
253
- 2. `api.route.current.params.sessionID` — populated? (Empty → panel falls back to scanning all sessions.)
254
- 3. New TUI code loaded? `tui-loaded.txt` (MARKER) should show `loaded-v2 ...`. If it still says `loaded`, the new dist isn't being picked up.
255
- 4. `appendFileSync` imported in `src/tui.tsx`? If missing, **all TUI debug logging silently fails** (ReferenceError swallowed by try/catch) — this wasted a lot of debugging time once.
256
-
257
- **Past issue (fixed v0.3.4):** panel read `ctx.session_id` which was always `undefined` → fallback scanned every session → another session's active harness leaked in. Fixed by reading `api.route.current.params.sessionID`.
258
-
259
- ## Status
260
- - ✅ Quota guardian + TUI panel (per-provider coach view, 5h/1w gauges, collapsible Alt+H)
261
- - ✅ Harness: agent mode with generate/grade tools (multi-model, 1 terminal)
262
- - ✅ Deterministic loop via NEXT directives (parallel PATH A / sequential PATH B)
263
- - ✅ Quota-aware tools (GO/THROTTLE/STOP drive model selection + concurrency)
264
- - ✅ Learning loop (record_failure → investigate → verify_diagnosis → generalize → rules.md)
265
- - ✅ Domain knowledge base (graph store, investigate/generate injection)
266
- - ✅ Session isolation (api.route, per-session harness state)
267
- - ✅ npm packaging (`opencode plugin install opencode-usage-coach`)
268
-
269
- License: MIT.
93
+ [![Ko-fi](https://img.shields.io/badge/Ko--fi-sponsor-FF5E5B)](https://ko-fi.com/lhjnano) [![GitHub Sponsors](https://img.shields.io/badge/GitHub-Sponsors-ea4aaa)](https://github.com/sponsors/lhjnano)
package/dist/tui.js CHANGED
@@ -83,8 +83,11 @@ var TLABEL = {
83
83
  completed: "done",
84
84
  failed: "fail",
85
85
  timed_out: "timeout",
86
- halted_quota: "quota-halt"
86
+ halted_quota: "quota-halt",
87
+ stale: "STALE"
87
88
  };
89
+ var STALE_MS = 5 * 6e4;
90
+ var HIDE_MS = 30 * 6e4;
88
91
  function barFill(p) {
89
92
  const n = p <= 0 ? 0 : Math.max(1, Math.min(10, Math.round(p / 10)));
90
93
  return "\u2588".repeat(n);
@@ -365,79 +368,89 @@ function initializeTui(api, disposeRoot) {
365
368
  return _el$46;
366
369
  })());
367
370
  }
368
- if (h && h.tasks.length > 0) {
369
- nodes.push((() => {
370
- var _el$48 = _$createElement("text");
371
- _$insertNode(_el$48, _$createTextNode(` `));
372
- return _el$48;
373
- })());
374
- nodes.push((() => {
375
- var _el$50 = _$createElement("text"), _el$51 = _$createTextNode(`harness: `), _el$52 = _$createTextNode(` `), _el$53 = _$createTextNode(`/`);
376
- _$insertNode(_el$50, _el$51);
377
- _$insertNode(_el$50, _el$52);
378
- _$insertNode(_el$50, _el$53);
379
- _$insert(_el$50, () => h.name, _el$52);
380
- _$insert(_el$50, () => h.current, _el$53);
381
- _$insert(_el$50, () => h.total, null);
382
- _$effect((_$p) => _$setProp(_el$50, "style", st("textMuted"), _$p));
383
- return _el$50;
384
- })());
385
- for (const t of h.tasks) {
386
- const sKey = statusKey[t.status] ?? "text";
387
- const lbl = TLABEL[t.status] ?? t.status;
388
- const rev = t.revisions > 0 && t.status === "revising" ? `(${t.revisions})` : "";
389
- const mdl = t.model ? ` ${t.model.split("/").pop() ?? t.model}` : "";
390
- const hasSub = !!t.subSessionId;
391
- const subStepStr = hasSub && t.subStep !== void 0 && t.subStep > 0 ? ` step:${t.subStep}` : "";
392
- const subEl = hasSub && t.subElapsed !== void 0 ? ` ${t.subElapsed}s` : "";
393
- const subWarn = hasSub && (t.subElapsed ?? 0) > 300;
394
- const elapsed = t.startedAt ? Math.max(0, Math.round((Date.now() - new Date(t.startedAt).getTime()) / 1e3)) : 0;
395
- const taskEl = t.status === "completed" || t.status === "failed" ? "" : elapsed > 0 ? ` ${elapsed}s` : "";
396
- const displayEl = hasSub ? subEl : taskEl;
397
- const lineKey = subWarn ? "warning" : sKey;
371
+ if (h && h.tasks.length > 0 && h.active !== false) {
372
+ const hAge = h.updatedAt ? Date.now() - new Date(h.updatedAt).getTime() : 0;
373
+ const hasActiveSub = h.tasks.some((t) => !!t.subSessionId);
374
+ const isStale = !hasActiveSub && hAge > STALE_MS;
375
+ const shouldHide = hAge > HIDE_MS && !hasActiveSub;
376
+ if (shouldHide) {
377
+ } else {
398
378
  nodes.push((() => {
399
- var _el$54 = _$createElement("text"), _el$55 = _$createTextNode(` \u25CF `), _el$56 = _$createTextNode(` `), _el$57 = _$createTextNode(` `);
400
- _$insertNode(_el$54, _el$55);
401
- _$insertNode(_el$54, _el$56);
402
- _$insertNode(_el$54, _el$57);
403
- _$insert(_el$54, () => t.id, _el$56);
404
- _$insert(_el$54, mdl, _el$56);
405
- _$insert(_el$54, lbl, _el$57);
406
- _$insert(_el$54, rev, _el$57);
407
- _$insert(_el$54, subStepStr, _el$57);
408
- _$insert(_el$54, displayEl, _el$57);
409
- _$insert(_el$54, () => t.title, null);
410
- _$effect((_$p) => _$setProp(_el$54, "style", st(lineKey), _$p));
411
- return _el$54;
379
+ var _el$48 = _$createElement("text");
380
+ _$insertNode(_el$48, _$createTextNode(` `));
381
+ return _el$48;
412
382
  })());
413
- const pv = t.model ? (t.model.split("/")[0] ?? "").split("-")[0] : "";
414
- const provCoach = pv ? s?.providers?.find((p) => p.id === pv || pv && p.id.startsWith(pv) || pv && pv.startsWith(p.id)) : s?.providers?.[0];
415
- const rawPct = provCoach?.fiveHour ?? s?.fiveHour ?? -1;
416
- const pct = rawPct < 0 ? 0 : rawPct;
417
- const pctLabel = rawPct < 0 ? "n/a" : `${rawPct}%`;
418
383
  nodes.push((() => {
419
- var _el$58 = _$createElement("box"), _el$59 = _$createElement("text"), _el$61 = _$createElement("text"), _el$62 = _$createElement("text"), _el$63 = _$createElement("text"), _el$64 = _$createTextNode(` `);
420
- _$insertNode(_el$58, _el$59);
421
- _$insertNode(_el$58, _el$61);
422
- _$insertNode(_el$58, _el$62);
423
- _$insertNode(_el$58, _el$63);
424
- _$setProp(_el$58, "flexDirection", "row");
425
- _$insertNode(_el$59, _$createTextNode(` 5h `));
426
- _$insert(_el$61, () => barFill(pct));
427
- _$insert(_el$62, () => barEmpty(pct));
428
- _$insertNode(_el$63, _el$64);
429
- _$insert(_el$63, pctLabel, null);
430
- _$effect((_p$) => {
431
- var _v$11 = st("text"), _v$12 = st("text");
432
- _v$11 !== _p$.e && (_p$.e = _$setProp(_el$61, "style", _v$11, _p$.e));
433
- _v$12 !== _p$.t && (_p$.t = _$setProp(_el$62, "style", _v$12, _p$.t));
434
- return _p$;
435
- }, {
436
- e: void 0,
437
- t: void 0
438
- });
439
- return _el$58;
384
+ var _el$50 = _$createElement("text"), _el$51 = _$createTextNode(`harness: `), _el$52 = _$createTextNode(` `), _el$53 = _$createTextNode(`/`);
385
+ _$insertNode(_el$50, _el$51);
386
+ _$insertNode(_el$50, _el$52);
387
+ _$insertNode(_el$50, _el$53);
388
+ _$insert(_el$50, () => h.name, _el$52);
389
+ _$insert(_el$50, () => h.current, _el$53);
390
+ _$insert(_el$50, () => h.total, null);
391
+ _$insert(_el$50, isStale ? " (stale)" : "", null);
392
+ _$effect((_$p) => _$setProp(_el$50, "style", st("textMuted"), _$p));
393
+ return _el$50;
440
394
  })());
395
+ for (const t of h.tasks) {
396
+ const TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "timed_out", "halted_quota"]);
397
+ const displayStatus = isStale && !TERMINAL.has(t.status) ? "stale" : t.status;
398
+ const sKey = statusKey[displayStatus] ?? "text";
399
+ const lbl = TLABEL[displayStatus] ?? displayStatus;
400
+ const rev = t.revisions > 0 && t.status === "revising" ? `(${t.revisions})` : "";
401
+ const mdl = t.model ? ` ${t.model.split("/").pop() ?? t.model}` : "";
402
+ const hasSub = !!t.subSessionId;
403
+ const subStepStr = hasSub && t.subStep !== void 0 && t.subStep > 0 ? ` step:${t.subStep}` : "";
404
+ const subEl = hasSub && t.subElapsed !== void 0 ? ` ${t.subElapsed}s` : "";
405
+ const subWarn = hasSub && (t.subElapsed ?? 0) > 300;
406
+ const elapsed = t.startedAt ? Math.max(0, Math.round((Date.now() - new Date(t.startedAt).getTime()) / 1e3)) : 0;
407
+ const taskEl = t.status === "completed" || t.status === "failed" ? "" : elapsed > 0 ? ` ${elapsed}s` : "";
408
+ const displayEl = hasSub ? subEl : taskEl;
409
+ const lineKey = subWarn ? "warning" : sKey;
410
+ nodes.push((() => {
411
+ var _el$54 = _$createElement("text"), _el$55 = _$createTextNode(` \u25CF `), _el$56 = _$createTextNode(` `), _el$57 = _$createTextNode(` `);
412
+ _$insertNode(_el$54, _el$55);
413
+ _$insertNode(_el$54, _el$56);
414
+ _$insertNode(_el$54, _el$57);
415
+ _$insert(_el$54, () => t.id, _el$56);
416
+ _$insert(_el$54, mdl, _el$56);
417
+ _$insert(_el$54, lbl, _el$57);
418
+ _$insert(_el$54, rev, _el$57);
419
+ _$insert(_el$54, subStepStr, _el$57);
420
+ _$insert(_el$54, displayEl, _el$57);
421
+ _$insert(_el$54, () => t.title, null);
422
+ _$effect((_$p) => _$setProp(_el$54, "style", st(lineKey), _$p));
423
+ return _el$54;
424
+ })());
425
+ const pv = t.model ? (t.model.split("/")[0] ?? "").split("-")[0] : "";
426
+ const provCoach = pv ? s?.providers?.find((p) => p.id === pv || pv && p.id.startsWith(pv) || pv && pv.startsWith(p.id)) : s?.providers?.[0];
427
+ const rawPct = provCoach?.fiveHour ?? s?.fiveHour ?? -1;
428
+ const pct = rawPct < 0 ? 0 : rawPct;
429
+ const pctLabel = rawPct < 0 ? "n/a" : `${rawPct}%`;
430
+ nodes.push((() => {
431
+ var _el$58 = _$createElement("box"), _el$59 = _$createElement("text"), _el$61 = _$createElement("text"), _el$62 = _$createElement("text"), _el$63 = _$createElement("text"), _el$64 = _$createTextNode(` `);
432
+ _$insertNode(_el$58, _el$59);
433
+ _$insertNode(_el$58, _el$61);
434
+ _$insertNode(_el$58, _el$62);
435
+ _$insertNode(_el$58, _el$63);
436
+ _$setProp(_el$58, "flexDirection", "row");
437
+ _$insertNode(_el$59, _$createTextNode(` 5h `));
438
+ _$insert(_el$61, () => barFill(pct));
439
+ _$insert(_el$62, () => barEmpty(pct));
440
+ _$insertNode(_el$63, _el$64);
441
+ _$insert(_el$63, pctLabel, null);
442
+ _$effect((_p$) => {
443
+ var _v$11 = st("text"), _v$12 = st("text");
444
+ _v$11 !== _p$.e && (_p$.e = _$setProp(_el$61, "style", _v$11, _p$.e));
445
+ _v$12 !== _p$.t && (_p$.t = _$setProp(_el$62, "style", _v$12, _p$.t));
446
+ return _p$;
447
+ }, {
448
+ e: void 0,
449
+ t: void 0
450
+ });
451
+ return _el$58;
452
+ })());
453
+ }
441
454
  }
442
455
  }
443
456
  return (() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-usage-coach",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "opencode closed-loop usage coach — quota SENSE -> coaching DECIDE -> loop ACT + TUI integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",