@litfamily/litopencode 1.0.36 โ†’ 1.0.38

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/CHANGELOG.md +16 -0
  2. package/README-Ko-KR.md +6 -6
  3. package/README.md +6 -6
  4. package/dist/state.d.ts +1 -0
  5. package/dist/state.js +1 -0
  6. package/dist/tools.d.ts +15 -5
  7. package/dist/tui/mission-band.d.ts +7 -0
  8. package/dist/tui/mission-band.js +4 -0
  9. package/dist/tui/mission-skills.js +3 -1
  10. package/dist/tui/mission-source-v2.d.ts +23 -0
  11. package/dist/tui/mission-source-v2.js +29 -4
  12. package/dist/tui/mission-source.d.ts +1 -0
  13. package/dist/tui/mission-source.js +3 -2
  14. package/dist/tui/mission-text.d.ts +3 -3
  15. package/dist/tui/mission-text.js +5 -5
  16. package/dist/v2/events.d.ts +1 -1
  17. package/dist/v2/events.js +31 -5
  18. package/dist/v2/notices.d.ts +13 -0
  19. package/dist/v2/notices.js +143 -0
  20. package/dist/v2/tui-footer.d.ts +20 -0
  21. package/dist/v2/tui-footer.js +56 -0
  22. package/dist/v2/tui-notices.d.ts +7 -0
  23. package/dist/v2/tui-notices.js +45 -0
  24. package/dist/v2/tui.d.ts +43 -1
  25. package/dist/v2/tui.js +345 -32
  26. package/dist/v2/usage-limits.d.ts +19 -0
  27. package/dist/v2/usage-limits.js +70 -0
  28. package/docs/assets/readme/badge-version.svg +1 -1
  29. package/docs/reference-Ko-KR.md +24 -5
  30. package/docs/reference.md +29 -7
  31. package/package.json +6 -2
  32. package/skills/lit-typographic-motion/engine/cover/render.mjs +1 -1
  33. package/skills/lit-typographic-motion/engine/stage/capture.mjs +3 -2
  34. package/skills/lit-typographic-motion/engine/stage/gate.mjs +1 -0
  35. package/skills/lit-typographic-motion/engine/stage/render.mjs +1 -0
  36. package/skills/lit-typographic-motion/engine/stage/scan.mjs +109 -5
  37. package/skills/lit-typographic-motion/engine/stage/stage-kit.js +39 -1
  38. package/skills/lit-typographic-motion/references/cover.md +2 -1
  39. package/skills/lit-typographic-motion/references/runtime.md +1 -1
  40. package/skills/lit-typographic-motion/references/stage.md +48 -2
  41. package/skills/lit-typographic-motion/requirements-audio.txt +92 -89
  42. package/skills/managed-skill-manifest.json +13 -13
  43. package/skills/readme-studio/templates/hyperframes/package-lock.json +4 -4
  44. package/skills/readme-studio/templates/remotion/package-lock.json +3 -3
  45. package/skills/readme-studio/templates/remotion/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ are tracked separately from this product history.
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 1.0.38 - 2026-11-02
9
+
10
+ - Dependency alerts cleared: zod 4.6.6 (the runtime dependency and the Remotion cover template), the audio runtime's msgpack 1.2.3, @hono/node-server 2.1.5 in the HyperFrames cover template, and toml 5.0.1 in the development tree.
11
+ - Releases are faster: the publish workflow no longer repeats the test suite and the QA probes. It checks with GitHub that CI already passed on the exact commit being released and stops if it did not. A publish from your own machine still runs the full suite first.
12
+ - On OpenCode 2 the footer line beside the prompt now steps down with the terminal width instead of being clipped by the host: the ignition mark with the whole stage chip, the mark with a smaller chip, the chip alone, `๐Ÿ”ฅ 1/3`, the bare flame, then nothing. Before, a narrow terminal cut the mark mid-word and mangled the host's own figures next to it.
13
+ - On OpenCode 2 the status card shows the OpenCode Go plan windows (`5h 42% ยท 1w 18%`) for sessions on the `opencode-go` provider, read from the host's usage source and asked at most once a minute. Other providers and signed-out hosts show nothing, because OpenCode 2 gives a plugin no other usage data.
14
+ - The automatic-handoff notices now reach the screen on OpenCode 2. The server plugin cannot raise a toast there, so it leaves each notice in `.litopencode/notices.jsonl` and the TUI plugin shows it.
15
+ - Automatic handoff now fires on OpenCode 2. The plugin waited for a `session.status` or `session.idle` event that OpenCode 2 never publishes, so a turn that crossed the percent was never followed by the handoff request. It now reads the end of a turn from `session.execution.succeeded`, `failed` and `interrupted` (and the start from `session.execution.started`), which also records the session as idle for the status card.
16
+ - A flaky test of the alias goal path now waits for the ledger event instead of only the goal in the fake host, so slow runners no longer fail it.
17
+
18
+ ## 1.0.37 - 2026-11-01
19
+
20
+ - `lit-typographic-motion` films can now carry real keyed footage, such as a reaction clip or a spinning-cat loop shot on a green screen. Key it outside the renderer into 8-bit RGBA PNG frames, put them in `stage/clips/<folder>/` and list them in `stage/clips/clips.json` (`id`, `dir`, `count`, `fps`). The page loads `/lit/clips.js`, registers a clip once with `LitStage.clip({ parent, id, box })` and shows it from `render(t)`; the frame is picked from film time, so a cue is plain arithmetic and the determinism replay holds. A declared clip is checked against its bytes (12 clips, 360 frames each and 1500 in all, 40 MB each and 128 MB in all, 720 px on the long side, up to 30 fps, real transparency) and is exempt from the flipbook rule; any frame sequence you did not declare is still refused with exit 17. A cover takes no clips. The flash audit and the text checks run on the final pixels, and the report lists each clip. `LitStage.clip.circle`, `wipe` and `iris` keep working.
21
+ - On OpenCode 2 the lit HUD is as complete as on OpenCode 1. The status card (discipline, workflow map, bar, live step, skill chips, lanes in parallel, context use and branch) now sits under every session, the stage chip appears beside the ignition line under the prompt, and the home screen shows the last goal. Until now only the ignition line and the sidebar panel were drawn there. The card reads OpenCode 2's own tool names, so a `skill` call (`id`) lights its chip and `subagent` calls show as lanes.
22
+ - OpenCode 2 also gets pop-ups for the workflow stage moving, a discipline switching and a lane starting or finishing, next to the plan, goal, blocker and completion pop-ups it already had. The restart notice, the `lit adhd` suggestion and the opt-in finish alert (`notify` in the plugin options) work there too.
23
+
8
24
  ## 1.0.36 - 2026-10-31
9
25
 
10
26
  - On OpenCode 2.2 and later, `lit loop`, `litwork` and `/start-work` set OpenCode's native `/goal` for you. The goal carries your task plus what done means here (tests pass, a real-surface check ran, temporary files are cleaned up, the ledger is closed), and each change prints one `LitOpenCode goal:` line. LitOpenCode manages only goals it created: it resumes one that a server restart paused, clears one after OpenCode's verdict and the ledger both say done, and reports a goal you made as left alone without touching it. When OpenCode says the goal is met but the ledger is open, you get a conflict line and no done. Planning, research, review and bare `lit` never set a goal. OpenCode 1 and older OpenCode 2 builds have no `/goal`, so the ledger carries the work and `native-goal-unavailable` is recorded once.
package/README-Ko-KR.md CHANGED
@@ -1,13 +1,13 @@
1
- <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/assets/cover-motion.webp" width="100%" alt="LitFamily ๋ชจ์…˜ ์ปค๋ฒ„: ์ผ๊ณฑ ๋กœ๋ด‡ ํŒจ๋„์ด ์ฐจ๋ก€๋กœ ์ผœ์ง€๊ณ , LitOpenCode ๋กœ๋ด‡์˜ ๋ˆˆ๊ณผ ํ…Œ๋‘๋ฆฌ๊ฐ€ ๋น›๋‚œ ๋’ค LITFAMILY์™€ KEEP THE WORK LIT. ๋ฌธ๊ตฌ๊ฐ€ ๋ฐ์•„์ง€๋Š” ์˜์ƒ" /></picture></p>
1
+ <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/assets/cover-motion.webp" width="100%" alt="LitFamily ๋ชจ์…˜ ์ปค๋ฒ„: ์ผ๊ณฑ ๋กœ๋ด‡ ํŒจ๋„์ด ์ฐจ๋ก€๋กœ ์ผœ์ง€๊ณ , LitOpenCode ๋กœ๋ด‡์˜ ๋ˆˆ๊ณผ ํ…Œ๋‘๋ฆฌ๊ฐ€ ๋น›๋‚œ ๋’ค LITFAMILY์™€ KEEP THE WORK LIT. ๋ฌธ๊ตฌ๊ฐ€ ๋ฐ์•„์ง€๋Š” ์˜์ƒ" /></picture></p>
2
2
 
3
3
  # LitOpenCode
4
4
 
5
5
  **LitOpenCode๋Š” OpenCode์šฉ ์›Œํฌํ”Œ๋กœ ํ”Œ๋Ÿฌ๊ทธ์ธ์ž…๋‹ˆ๋‹ค. ํ”„๋กฌํ”„ํŠธ์— `lit`๋งŒ ๋ถ™์ด๋ฉด ๊ณ„ํšํ•˜๊ณ , ๋งŒ๋“ค๊ณ , ํ™•์ธํ•˜๊ณ , ๊ธฐ๋ก๊นŒ์ง€ ๋‚จ๊ฒจ์„œ ๋‹ค์Œ ์„ธ์…˜์ด ์–ด๋””์„œ ๋ฉˆ์ท„๋Š”์ง€ ๋ฐ”๋กœ ์••๋‹ˆ๋‹ค.**
6
6
 
7
7
  <p align="center">
8
- <a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/assets/readme/badge-version.svg" alt="1.0.36" /></a>
8
+ <a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/assets/readme/badge-version.svg" alt="1.0.38" /></a>
9
9
  <a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://img.shields.io/npm/dm/@litfamily/litopencode?label=downloads&color=D7F75B" alt="์›”๊ฐ„ npm ๋‹ค์šด๋กœ๋“œ ์ˆ˜" /></a>
10
- <a href="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/assets/readme/badge-license.svg" alt="MIT ๋ผ์ด์„ ์Šค" /></a>
10
+ <a href="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/assets/readme/badge-license.svg" alt="MIT ๋ผ์ด์„ ์Šค" /></a>
11
11
  <a href="https://github.com/wjgoarxiv/litopencode"><img src="https://img.shields.io/github/stars/wjgoarxiv/litopencode?style=social" alt="GitHub ์Šคํƒ€" /></a>
12
12
  </p>
13
13
 
@@ -57,9 +57,9 @@ npm uninstall -g @litfamily/litopencode
57
57
  ## ๋” ๋ณด๊ธฐ
58
58
 
59
59
  - [GitHub์˜ ์‚ฌ์šฉ ์•ˆ๋‚ด](https://github.com/wjgoarxiv/litopencode/blob/main/docs/guide-Ko-KR.md): ์„ค์น˜ ์˜ต์…˜, ์ž๋™ ์ธ์ˆ˜์ธ๊ณ„, ๋ฌธ์ œ ํ•ด๊ฒฐ, ์ œ๊ฑฐ
60
- - [์›Œํฌํ”Œ๋กœ ์ƒ์„ธ ์•ˆ๋‚ด](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/reference-Ko-KR.md) ยท [English reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/reference.md)
61
- - [๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์•ˆ๋‚ด](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/migration.md) ยท [Changelog](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/CHANGELOG.md) ยท [๊ฐœ์ธ์ •๋ณด์™€ ๋„คํŠธ์›Œํฌ ๋™์ž‘](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/privacy.md)
62
- - [LitFamily ํ—ˆ๋ธŒ](https://github.com/wjgoarxiv/litfamily) ยท [MIT ๋ผ์ด์„ ์Šค](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/LICENSE) ยท [์ƒํ‘œ ์ •์ฑ…](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/TRADEMARK.md) ยท [ASCII ๊ธ€๊ผด ๋ผ์ด์„ ์Šค](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/assets/readme/JetBrainsMono-OFL.txt)
60
+ - [์›Œํฌํ”Œ๋กœ ์ƒ์„ธ ์•ˆ๋‚ด](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/reference-Ko-KR.md) ยท [English reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/reference.md)
61
+ - [๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์•ˆ๋‚ด](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/migration.md) ยท [Changelog](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/CHANGELOG.md) ยท [๊ฐœ์ธ์ •๋ณด์™€ ๋„คํŠธ์›Œํฌ ๋™์ž‘](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/privacy.md)
62
+ - [LitFamily ํ—ˆ๋ธŒ](https://github.com/wjgoarxiv/litfamily) ยท [MIT ๋ผ์ด์„ ์Šค](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/LICENSE) ยท [์ƒํ‘œ ์ •์ฑ…](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/TRADEMARK.md) ยท [ASCII ๊ธ€๊ผด ๋ผ์ด์„ ์Šค](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/assets/readme/JetBrainsMono-OFL.txt)
63
63
 
64
64
  <details>
65
65
  <summary>ASCII ๋กœ๊ณ  ๋ณต์‚ฌ</summary>
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
- <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/assets/cover-motion.webp" width="100%" alt="LitFamily motion cover: seven armored robots power on one by one, the LitOpenCode robot wakes with glowing eyes and a lit frame, then LITFAMILY and KEEP THE WORK LIT. light up." /></picture></p>
1
+ <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/assets/cover-motion.webp" width="100%" alt="LitFamily motion cover: seven armored robots power on one by one, the LitOpenCode robot wakes with glowing eyes and a lit frame, then LITFAMILY and KEEP THE WORK LIT. light up." /></picture></p>
2
2
 
3
3
  # LitOpenCode
4
4
 
5
5
  **LitOpenCode is a workflow plugin for OpenCode. Add `lit` to a prompt and the work gets planned, built, checked and written down, so the next session knows where you stopped.**
6
6
 
7
7
  <p align="center">
8
- <a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/assets/readme/badge-version.svg" alt="1.0.36" /></a>
8
+ <a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/assets/readme/badge-version.svg" alt="1.0.38" /></a>
9
9
  <a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://img.shields.io/npm/dm/@litfamily/litopencode?label=downloads&color=D7F75B" alt="npm downloads per month" /></a>
10
- <a href="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/assets/readme/badge-license.svg" alt="MIT license" /></a>
10
+ <a href="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/assets/readme/badge-license.svg" alt="MIT license" /></a>
11
11
  <a href="https://github.com/wjgoarxiv/litopencode"><img src="https://img.shields.io/github/stars/wjgoarxiv/litopencode?style=social" alt="GitHub stars" /></a>
12
12
  </p>
13
13
 
@@ -57,9 +57,9 @@ npm uninstall -g @litfamily/litopencode
57
57
  ## More
58
58
 
59
59
  - [Guide on GitHub](https://github.com/wjgoarxiv/litopencode/blob/main/docs/guide.md): install options, automatic handoff, troubleshooting and removal.
60
- - [Workflow reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/reference.md)
61
- - [Migration notes](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/migration.md) ยท [Changelog](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/CHANGELOG.md) ยท [Privacy and network behavior](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/privacy.md)
62
- - [LitFamily hub](https://github.com/wjgoarxiv/litfamily) ยท [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/LICENSE) ยท [Trademark policy](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/TRADEMARK.md) ยท [ASCII font license](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/assets/readme/JetBrainsMono-OFL.txt)
60
+ - [Workflow reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/reference.md)
61
+ - [Migration notes](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/migration.md) ยท [Changelog](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/CHANGELOG.md) ยท [Privacy and network behavior](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/privacy.md)
62
+ - [LitFamily hub](https://github.com/wjgoarxiv/litfamily) ยท [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/LICENSE) ยท [Trademark policy](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/TRADEMARK.md) ยท [ASCII font license](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/assets/readme/JetBrainsMono-OFL.txt)
63
63
 
64
64
  <details>
65
65
  <summary>Copy ASCII logo</summary>
package/dist/state.d.ts CHANGED
@@ -18,6 +18,7 @@ export type RuntimePaths = {
18
18
  knowledgeDir: string;
19
19
  knowledgeClaimsFile: string;
20
20
  autoHandoffFile: string;
21
+ noticesFile: string;
21
22
  workflowStackDir: string;
22
23
  adhdDir: string;
23
24
  livenessDir: string;
package/dist/state.js CHANGED
@@ -34,6 +34,7 @@ export function createRuntimePaths(projectRoot) {
34
34
  knowledgeDir: path.join(runtimeDir, "knowledge"),
35
35
  knowledgeClaimsFile: path.join(runtimeDir, "knowledge", "claims.jsonl"),
36
36
  autoHandoffFile: path.join(runtimeDir, "auto-handoff.json"),
37
+ noticesFile: path.join(runtimeDir, "notices.jsonl"),
37
38
  workflowStackDir: path.join(runtimeDir, "workflow-stack"),
38
39
  adhdDir: path.join(runtimeDir, "adhd"),
39
40
  livenessDir: path.join(runtimeDir, "liveness"),
package/dist/tools.d.ts CHANGED
@@ -10,34 +10,44 @@ export declare const litOpenCodeTools: Readonly<{
10
10
  args: Readonly<{
11
11
  [k: string]: import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>>;
12
12
  }>;
13
- execute(args: Record<string, unknown>, context: import("@opencode-ai/plugin").ToolContext): Promise<ToolResult>;
13
+ execute(args: {
14
+ [x: string]: unknown;
15
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<ToolResult>;
14
16
  };
15
17
  litwork: {
16
18
  description: string;
17
19
  args: Readonly<{
18
20
  [k: string]: import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>>;
19
21
  }>;
20
- execute(args: Record<string, unknown>, context: import("@opencode-ai/plugin").ToolContext): Promise<ToolResult>;
22
+ execute(args: {
23
+ [x: string]: unknown;
24
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<ToolResult>;
21
25
  };
22
26
  "start-work": {
23
27
  description: string;
24
28
  args: Readonly<{
25
29
  [k: string]: import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>>;
26
30
  }>;
27
- execute(args: Record<string, unknown>, context: import("@opencode-ai/plugin").ToolContext): Promise<ToolResult>;
31
+ execute(args: {
32
+ [x: string]: unknown;
33
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<ToolResult>;
28
34
  };
29
35
  "review-work": {
30
36
  description: string;
31
37
  args: Readonly<{
32
38
  [k: string]: import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>>;
33
39
  }>;
34
- execute(args: Record<string, unknown>, context: import("@opencode-ai/plugin").ToolContext): Promise<ToolResult>;
40
+ execute(args: {
41
+ [x: string]: unknown;
42
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<ToolResult>;
35
43
  };
36
44
  wikify: {
37
45
  description: string;
38
46
  args: Readonly<{
39
47
  [k: string]: import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>>;
40
48
  }>;
41
- execute(args: Record<string, unknown>, context: import("@opencode-ai/plugin").ToolContext): Promise<ToolResult>;
49
+ execute(args: {
50
+ [x: string]: unknown;
51
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<ToolResult>;
42
52
  };
43
53
  }>;
@@ -36,6 +36,13 @@ export type CardFigures = {
36
36
  readonly branch?: string;
37
37
  readonly inLane: boolean;
38
38
  readonly restartTo?: string;
39
+ readonly limits?: readonly CardLimit[];
40
+ };
41
+ /** A plan window the host reports (`5h 42%`), shown after the context figure; absent when the host reports none. */
42
+ export type CardLimit = {
43
+ readonly label: string;
44
+ readonly percent: number;
45
+ readonly limited: boolean;
39
46
  };
40
47
  export type CardInputs = {
41
48
  readonly view: MissionView;
@@ -150,6 +150,7 @@ function topLine(view, frame, palette, width, border, rest, mark) {
150
150
  }
151
151
  return fitLine([{ text: "โ•ญ" + "โ”€".repeat(Math.max(0, width)), fg: border }], width, palette);
152
152
  }
153
+ const limitWarnPercent = 90;
153
154
  /** The figures in the bottom border, most important first; the last ones go first when room runs out. */
154
155
  function figureParts(view, figures, palette) {
155
156
  const parts = [];
@@ -159,6 +160,9 @@ function figureParts(view, figures, palette) {
159
160
  const percent = Math.round(figures.contextPercent);
160
161
  parts.push({ text: `ctx ${percent}%`, fg: percent >= contextWarnPercent ? palette.warning : palette.muted });
161
162
  }
163
+ for (const limit of figures.limits ?? []) {
164
+ parts.push({ text: `${limit.label} ${limit.percent}%${limit.limited ? " limited" : ""}`, fg: limit.limited || limit.percent >= limitWarnPercent ? palette.warning : palette.muted });
165
+ }
162
166
  if (view.active)
163
167
  parts.push({ text: `โŸณ ${elapsedText(view.elapsedMinutes)}${figures.inLane ? " ยท in lane" : ""}`, fg: palette.muted });
164
168
  if (figures.branch !== undefined)
@@ -25,7 +25,9 @@ export function readSkillUse(part, at) {
25
25
  const input = isRecord(state.input) ? state.input : {};
26
26
  const start = isRecord(state.time) && typeof state.time.start === "number" ? state.time.start : at;
27
27
  if (part.tool === "skill") {
28
- return typeof input.name === "string" && ownSkills.has(input.name) ? { name: input.name, at: start } : undefined;
28
+ // OpenCode 1's skill tool takes `name`; OpenCode 2's takes `id`.
29
+ const skill = typeof input.name === "string" ? input.name : typeof input.id === "string" ? input.id : undefined;
30
+ return skill !== undefined && ownSkills.has(skill) ? { name: skill, at: start } : undefined;
29
31
  }
30
32
  const name = ownTools[part.tool];
31
33
  if (name === undefined || input.action === "status")
@@ -15,16 +15,39 @@ type V2Message = {
15
15
  readonly content?: ReadonlyArray<{
16
16
  readonly type: string;
17
17
  readonly name?: string;
18
+ readonly text?: string;
18
19
  readonly state?: {
19
20
  readonly status?: string;
21
+ readonly input?: unknown;
22
+ readonly metadata?: unknown;
20
23
  };
21
24
  readonly time?: {
22
25
  readonly created?: number;
23
26
  readonly ran?: number;
27
+ readonly completed?: number;
24
28
  };
25
29
  }>;
26
30
  };
31
+ type V2Model = {
32
+ readonly providerID: string;
33
+ readonly modelID: string;
34
+ readonly limit?: {
35
+ readonly context?: number;
36
+ };
37
+ };
27
38
  export type MissionDataV2 = {
39
+ readonly location?: {
40
+ readonly vcs?: {
41
+ info(): {
42
+ readonly branch?: {
43
+ readonly current?: string;
44
+ };
45
+ } | undefined;
46
+ };
47
+ readonly model?: {
48
+ list(): ReadonlyArray<V2Model> | undefined;
49
+ };
50
+ };
28
51
  readonly session: {
29
52
  status(sessionID: string): "idle" | "running";
30
53
  readonly message: {
@@ -5,11 +5,23 @@ function isMessage(value) {
5
5
  export function sessionMessagesV2(data, sessionID) {
6
6
  return (data.session.message.list(sessionID) ?? []).filter(isMessage).filter((message) => message.type === "user" || message.type === "assistant");
7
7
  }
8
+ /** The provider table the context figure reads: each model's context window, from the host's model list. */
9
+ function providersV2(data) {
10
+ const byProvider = new Map();
11
+ for (const model of data.location?.model?.list() ?? []) {
12
+ const models = byProvider.get(model.providerID) ?? {};
13
+ models[model.modelID] = { limit: { context: model.limit?.context } };
14
+ byProvider.set(model.providerID, models);
15
+ }
16
+ return [...byProvider].map(([id, models]) => ({ id, models }));
17
+ }
8
18
  export function missionStateFromV2(data, directory) {
9
19
  const index = new Map();
20
+ const branch = data.location?.vcs?.info()?.branch?.current?.trim();
10
21
  return {
11
22
  path: directory === undefined ? {} : { directory },
12
- provider: [],
23
+ ...(branch === undefined || branch === "" ? {} : { vcs: { branch } }),
24
+ provider: providersV2(data),
13
25
  session: {
14
26
  messages(sessionID) {
15
27
  return sessionMessagesV2(data, sessionID).map((message) => {
@@ -35,9 +47,22 @@ export function missionStateFromV2(data, directory) {
35
47
  return [];
36
48
  if (message.type === "user")
37
49
  return message.metadata === undefined ? [] : [{ metadata: message.metadata }];
38
- return (message.content ?? [])
39
- .filter((item) => item.type === "tool" && typeof item.name === "string")
40
- .map((item) => ({ type: "tool", tool: item.name, state: { status: item.state?.status, time: { start: item.time?.ran ?? item.time?.created } } }));
50
+ return (message.content ?? []).flatMap((item) => {
51
+ if (item.type === "text" && typeof item.text === "string")
52
+ return [{ type: "text", text: item.text }];
53
+ if (item.type !== "tool" || typeof item.name !== "string")
54
+ return [];
55
+ return [{
56
+ type: "tool",
57
+ tool: item.name,
58
+ state: {
59
+ status: item.state?.status,
60
+ input: item.state?.input,
61
+ metadata: item.state?.metadata,
62
+ time: { start: item.time?.ran ?? item.time?.created, ...(item.time?.completed === undefined ? {} : { end: item.time.completed }) }
63
+ }
64
+ }];
65
+ });
41
66
  }
42
67
  };
43
68
  }
@@ -52,6 +52,7 @@ export type MissionStateSource = {
52
52
  readonly tool?: unknown;
53
53
  readonly state?: unknown;
54
54
  readonly metadata?: unknown;
55
+ readonly text?: unknown;
55
56
  }>;
56
57
  };
57
58
  /** The same root the server plugin uses: the worktree unless it is the filesystem root, else the directory. */
@@ -330,7 +330,8 @@ function taskSources(state, sessionID, now) {
330
330
  const assistants = state.session.messages(sessionID).filter((message) => message.role === "assistant").slice(-recentAssistantMessages);
331
331
  for (const message of assistants) {
332
332
  for (const part of state.part(message.id)) {
333
- if (part.type !== "tool" || part.tool !== "task" || !isRecord(part.state))
333
+ // OpenCode 1 delegates through `task`, OpenCode 2 through `subagent`.
334
+ if (part.type !== "tool" || (part.tool !== "task" && part.tool !== "subagent") || !isRecord(part.state))
334
335
  continue;
335
336
  const toolState = part.state;
336
337
  const status = toolState.status;
@@ -348,7 +349,7 @@ function taskSources(state, sessionID, now) {
348
349
  children.add(child);
349
350
  const todos = child === undefined ? [] : state.session.todo(child).filter((todo) => todo.status !== "cancelled");
350
351
  const done = todos.filter((todo) => todo.status === "completed").length;
351
- const name = typeof input.description === "string" && input.description.trim() !== "" ? input.description.trim() : typeof input.subagent_type === "string" ? input.subagent_type : "subagent";
352
+ const name = typeof input.description === "string" && input.description.trim() !== "" ? input.description.trim() : typeof input.subagent_type === "string" ? input.subagent_type : typeof input.agent === "string" ? input.agent : "subagent";
352
353
  // A running task last moved when its own session did; its start time alone would read a long, busy run as idle.
353
354
  const moved = child === undefined ? undefined : lastActivity(state, child);
354
355
  sources.push({
@@ -100,13 +100,13 @@ export declare function groupsWidth(groups: readonly ChipGroup[]): number;
100
100
  export declare function fade(segment: Segment, alpha: number, palette: MissionPalette): Segment;
101
101
  export declare const chipSparkWidth = 7;
102
102
  export declare function chipSparkCells(t: number, palette: MissionPalette): Segment[] | undefined;
103
- export declare function chipTier(view: MissionView, frame: MotionFrame, palette: MissionPalette, budget: number): ChipTier;
103
+ export declare function chipTier(view: MissionView, frame: MotionFrame, palette: MissionPalette, budget: number, largest?: ChipTier): ChipTier;
104
104
  /**
105
105
  * The map chip on the right of the prompt's agent/model row: `๐Ÿ”ฅ LIT plan โœ“ โ€บ WORK โ–ฐโ–ฐโ–ฐโ–ฑโ–ฑโ–ฑโ–ฑ 3/7 โ€บ review`,
106
106
  * then the live step when the full chip leaves room for it, shedding labels before values as `budget`
107
- * shrinks. Idle sessions render nothing.
107
+ * shrinks, and never larger than `largest`. Idle sessions render nothing.
108
108
  */
109
- export declare function chipLine(view: MissionView, frame: MotionFrame, palette: MissionPalette, budget: number): Line;
109
+ export declare function chipLine(view: MissionView, frame: MotionFrame, palette: MissionPalette, budget: number, largest?: ChipTier): Line;
110
110
  /** The header status: a glyph and words, or the COMPLETE badge that pops in and settles to green text. */
111
111
  export declare function statusSegments(view: MissionView, frame: MotionFrame, palette: MissionPalette): {
112
112
  readonly segments: Segment[];
@@ -396,8 +396,8 @@ export function chipSparkCells(t, palette) {
396
396
  }
397
397
  return cells;
398
398
  }
399
- export function chipTier(view, frame, palette, budget) {
400
- for (const tier of ["full", "medium"]) {
399
+ export function chipTier(view, frame, palette, budget, largest = "full") {
400
+ for (const tier of largest === "full" ? ["full", "medium"] : largest === "medium" ? ["medium"] : []) {
401
401
  if (groupsWidth(chipGroups(view, frame, palette, tier)) <= budget)
402
402
  return tier;
403
403
  }
@@ -406,12 +406,12 @@ export function chipTier(view, frame, palette, budget) {
406
406
  /**
407
407
  * The map chip on the right of the prompt's agent/model row: `๐Ÿ”ฅ LIT plan โœ“ โ€บ WORK โ–ฐโ–ฐโ–ฐโ–ฑโ–ฑโ–ฑโ–ฑ 3/7 โ€บ review`,
408
408
  * then the live step when the full chip leaves room for it, shedding labels before values as `budget`
409
- * shrinks. Idle sessions render nothing.
409
+ * shrinks, and never larger than `largest`. Idle sessions render nothing.
410
410
  */
411
- export function chipLine(view, frame, palette, budget) {
411
+ export function chipLine(view, frame, palette, budget, largest = "full") {
412
412
  if (!view.active)
413
413
  return [];
414
- const tier = chipTier(view, frame, palette, budget);
414
+ const tier = chipTier(view, frame, palette, budget, largest);
415
415
  const groups = chipGroups(view, frame, palette, tier);
416
416
  if (tier === "full") {
417
417
  const live = liveSegments(view, frame, palette, 24);
@@ -36,7 +36,7 @@ export type EventModelLike = {
36
36
  };
37
37
  export type V2EventOptions = {
38
38
  readonly projectRoot: string;
39
- readonly paths: Pick<RuntimePaths, "autoHandoffFile">;
39
+ readonly paths: Pick<RuntimePaths, "autoHandoffFile"> & Partial<Pick<RuntimePaths, "noticesFile">>;
40
40
  readonly autoHandoffConfig: AutoHandoffConfig;
41
41
  readonly boundedAuthority: BoundedAuthorityOptions;
42
42
  readonly getSession: SessionLookup;
package/dist/v2/events.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createAutoHandoff } from "../auto-handoff-hooks.js";
2
2
  import { LifecycleConflictError, LifecycleSafetyError, createBoundedAuthorityLifecycle, parseFencedProgress } from "../bounded-authority.js";
3
3
  import { markLiveness } from "../session-liveness.js";
4
+ import { appendNotice } from "./notices.js";
4
5
  function isRecord(value) {
5
6
  return typeof value === "object" && value !== null && !Array.isArray(value);
6
7
  }
@@ -9,10 +10,20 @@ function str(value) {
9
10
  }
10
11
  /**
11
12
  * The calls the OpenCode 1 automatic handoff makes, answered by OpenCode 2. There is no server toast on
12
- * OpenCode 2, so the client has no `tui`; the handoff's own notices are not shown there.
13
+ * OpenCode 2, so `tui.showToast` leaves the notice in the project's notices file, which the TUI plugin
14
+ * reads and shows (notices.ts). Without a notices file the call is accepted and dropped.
13
15
  */
14
- function handoffClient(session, model, statuses) {
16
+ function handoffClient(session, model, statuses, noticesFile) {
15
17
  const client = {
18
+ tui: {
19
+ async showToast(input) {
20
+ const body = input.body;
21
+ if (noticesFile !== undefined && body !== undefined && typeof body.message === "string") {
22
+ appendNotice(noticesFile, { title: body.title ?? "๐Ÿ”ฅ LIT", message: body.message, variant: (body.variant ?? "info"), duration: body.duration ?? 8000 });
23
+ }
24
+ return { data: true };
25
+ }
26
+ },
16
27
  session: {
17
28
  async promptAsync(input) {
18
29
  const text = [input.body.system, ...(input.body.parts ?? []).map((part) => part.text)].filter((part) => typeof part === "string" && part !== "").join("\n\n");
@@ -50,7 +61,7 @@ export function createV2Events(options) {
50
61
  const autoHandoff = createAutoHandoff({
51
62
  projectRoot: options.projectRoot,
52
63
  paths: options.paths,
53
- client: handoffClient(options.session, options.model, statuses),
64
+ client: handoffClient(options.session, options.model, statuses, options.paths.noticesFile),
54
65
  config: options.autoHandoffConfig,
55
66
  getSession: options.getSession
56
67
  });
@@ -88,6 +99,11 @@ export function createV2Events(options) {
88
99
  throw error;
89
100
  }
90
101
  }
102
+ async function idle(sessionID) {
103
+ statuses.set(sessionID, { type: "idle" });
104
+ await markLiveness(options.projectRoot, sessionID, "idle");
105
+ await toHandoff("session.idle", { sessionID });
106
+ }
91
107
  async function handle(event) {
92
108
  const data = isRecord(event.data) ? event.data : {};
93
109
  const sessionID = str(data.sessionID);
@@ -135,13 +151,23 @@ export function createV2Events(options) {
135
151
  });
136
152
  return;
137
153
  }
154
+ // OpenCode 2.2.5 publishes no `session.status` or `session.idle`. A turn runs from
155
+ // `session.execution.started` to one of three endings, and the handoff reads them as busy and idle.
156
+ case "session.execution.started":
157
+ statuses.set(sessionID, { type: "busy" });
158
+ await toHandoff("session.status", { sessionID, status: { type: "busy" } });
159
+ return;
160
+ case "session.execution.succeeded":
161
+ case "session.execution.failed":
162
+ case "session.execution.interrupted":
163
+ await idle(sessionID);
164
+ return;
138
165
  case "session.status":
139
166
  statuses.set(sessionID, data.status);
140
167
  await toHandoff("session.status", { sessionID, status: data.status });
141
168
  return;
142
169
  case "session.idle":
143
- await markLiveness(options.projectRoot, sessionID, "idle");
144
- await toHandoff("session.idle", { sessionID });
170
+ await idle(sessionID);
145
171
  return;
146
172
  case "session.compaction.ended":
147
173
  await toHandoff("session.compacted", { sessionID });
@@ -0,0 +1,13 @@
1
+ export type NoticeVariant = "info" | "success" | "warning" | "error";
2
+ export type TuiNotice = {
3
+ readonly title: string;
4
+ readonly message: string;
5
+ readonly variant: NoticeVariant;
6
+ readonly duration: number;
7
+ };
8
+ /** Leaves one popup for the TUI. Best effort: a place that cannot be written loses the notice, never the caller. */
9
+ export declare function appendNotice(file: string, notice: TuiNotice): void;
10
+ /** Reads the notices that appear after it was made. Each call returns the new ones once, oldest first. */
11
+ export declare function createNoticeTail(file: string): {
12
+ readonly poll: () => readonly TuiNotice[];
13
+ };
@@ -0,0 +1,143 @@
1
+ // The way a server-side popup reaches the screen on OpenCode 2. The server plugin's context only subscribes to
2
+ // events, so it cannot raise a toast; the TUI plugin can, but it knows nothing of the server's state machines
3
+ // (the automatic handoff). The server therefore appends each popup it would have shown to a small file under
4
+ // the project's .litopencode, and the TUI reads the lines that appear after it started and toasts them. Both
5
+ // halves ship in this package; neither needs the other to be there. A symbolic link in place of the file is
6
+ // never followed, a torn line waits for its newline, and the file is rewritten to its newest lines past a cap.
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ const variants = new Set(["info", "success", "warning", "error"]);
10
+ const titleCells = 80;
11
+ const messageCells = 400;
12
+ const minDuration = 1000;
13
+ const maxDuration = 15_000;
14
+ const defaultDuration = 8000;
15
+ const capBytes = 64_000;
16
+ const keepBytes = 24_000;
17
+ const readCapBytes = 128_000;
18
+ const seenLimit = 64;
19
+ let counter = 0;
20
+ function clip(value, max) {
21
+ const chars = [...value];
22
+ return chars.length <= max ? value : `${chars.slice(0, max - 1).join("")}โ€ฆ`;
23
+ }
24
+ function clean(value) {
25
+ if (typeof value !== "object" || value === null)
26
+ return undefined;
27
+ const entry = value;
28
+ if (typeof entry.title !== "string" || typeof entry.message !== "string" || entry.message === "")
29
+ return undefined;
30
+ if (typeof entry.variant !== "string" || !variants.has(entry.variant))
31
+ return undefined;
32
+ const duration = typeof entry.duration === "number" && Number.isFinite(entry.duration) ? Math.min(maxDuration, Math.max(minDuration, entry.duration)) : defaultDuration;
33
+ return { title: clip(entry.title, titleCells), message: clip(entry.message, messageCells), variant: entry.variant, duration };
34
+ }
35
+ function plainFile(file) {
36
+ try {
37
+ const stat = fs.lstatSync(file);
38
+ return stat.isFile() ? stat : undefined;
39
+ }
40
+ catch {
41
+ return undefined;
42
+ }
43
+ }
44
+ function trimToNewest(file) {
45
+ const text = fs.readFileSync(file, "utf8");
46
+ const lines = text.split("\n").filter((line) => line !== "");
47
+ const kept = [];
48
+ let size = 0;
49
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
50
+ size += Buffer.byteLength(lines[index]) + 1;
51
+ if (size > keepBytes)
52
+ break;
53
+ kept.unshift(lines[index]);
54
+ }
55
+ const temp = `${file}.${process.pid}.tmp`;
56
+ fs.writeFileSync(temp, kept.length === 0 ? "" : `${kept.join("\n")}\n`);
57
+ fs.renameSync(temp, file);
58
+ }
59
+ /** Leaves one popup for the TUI. Best effort: a place that cannot be written loses the notice, never the caller. */
60
+ export function appendNotice(file, notice) {
61
+ const cleaned = clean(notice);
62
+ if (cleaned === undefined)
63
+ return;
64
+ try {
65
+ fs.mkdirSync(path.dirname(file), { recursive: true });
66
+ const existing = (() => {
67
+ try {
68
+ return fs.lstatSync(file);
69
+ }
70
+ catch {
71
+ return undefined;
72
+ }
73
+ })();
74
+ if (existing !== undefined && !existing.isFile())
75
+ return;
76
+ counter += 1;
77
+ const stored = { id: `${Date.now().toString(36)}-${process.pid}-${counter}`, at: Date.now(), ...cleaned };
78
+ fs.appendFileSync(file, `${JSON.stringify(stored)}\n`);
79
+ if ((plainFile(file)?.size ?? 0) > capBytes)
80
+ trimToNewest(file);
81
+ }
82
+ catch { // no-excuse-ok: catch -- a popup is best-effort feedback; the handoff itself must never stop for it.
83
+ }
84
+ }
85
+ /** Reads the notices that appear after it was made. Each call returns the new ones once, oldest first. */
86
+ export function createNoticeTail(file) {
87
+ let offset = plainFile(file)?.size ?? 0;
88
+ const seen = [];
89
+ return {
90
+ poll() {
91
+ const stat = plainFile(file);
92
+ if (stat === undefined)
93
+ return [];
94
+ // A smaller file was rewritten to its newest lines; read it again and let the ids drop what was shown.
95
+ if (stat.size < offset)
96
+ offset = 0;
97
+ if (stat.size === offset)
98
+ return [];
99
+ const length = Math.min(stat.size - offset, readCapBytes);
100
+ const buffer = Buffer.alloc(length);
101
+ let fd;
102
+ try {
103
+ fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
104
+ fs.readSync(fd, buffer, 0, length, offset);
105
+ }
106
+ catch {
107
+ return [];
108
+ }
109
+ finally {
110
+ if (fd !== undefined)
111
+ fs.closeSync(fd);
112
+ }
113
+ const text = buffer.toString("utf8");
114
+ const end = text.lastIndexOf("\n");
115
+ if (end < 0)
116
+ return length === readCapBytes ? (offset += length, []) : [];
117
+ offset += Buffer.byteLength(text.slice(0, end + 1));
118
+ const out = [];
119
+ for (const line of text.slice(0, end).split("\n")) {
120
+ let parsed;
121
+ try {
122
+ parsed = JSON.parse(line);
123
+ }
124
+ catch {
125
+ continue;
126
+ }
127
+ const id = typeof parsed?.id === "string" ? parsed.id : undefined;
128
+ if (id !== undefined && seen.includes(id))
129
+ continue;
130
+ const entry = clean(parsed);
131
+ if (entry === undefined)
132
+ continue;
133
+ if (id !== undefined) {
134
+ seen.push(id);
135
+ if (seen.length > seenLimit)
136
+ seen.shift();
137
+ }
138
+ out.push(entry);
139
+ }
140
+ return out;
141
+ }
142
+ };
143
+ }