loadouts 0.2.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/bundled/embedded/pi/loadouts-runtime.ts +19677 -0
  2. package/bundled/extensions/loadouts-runtime/index.ts +19677 -0
  3. package/bundled/extensions/loadouts-runtime/package.json +6 -0
  4. package/bundled/loadouts/opencode-runtime.yaml +5 -0
  5. package/bundled/loadouts/pi-runtime.yaml +5 -0
  6. package/bundled/opencode/plugins/loadouts/index.js +39198 -0
  7. package/bundled/opencode/plugins/loadouts/package.json +9 -0
  8. package/bundled/opencode/plugins/loadouts/tui.js +16913 -0
  9. package/dist/builtins/agent-transforms.d.ts.map +1 -1
  10. package/dist/builtins/agent-transforms.js +31 -9
  11. package/dist/builtins/agent-transforms.js.map +1 -1
  12. package/dist/builtins/index.d.ts.map +1 -1
  13. package/dist/builtins/index.js +18 -3
  14. package/dist/builtins/index.js.map +1 -1
  15. package/dist/builtins/kinds/extension-dir.d.ts +3 -0
  16. package/dist/builtins/kinds/extension-dir.d.ts.map +1 -0
  17. package/dist/builtins/kinds/extension-dir.js +8 -0
  18. package/dist/builtins/kinds/extension-dir.js.map +1 -0
  19. package/dist/builtins/kinds/opencode-cli-config.d.ts +3 -0
  20. package/dist/builtins/kinds/opencode-cli-config.d.ts.map +1 -0
  21. package/dist/builtins/kinds/opencode-cli-config.js +10 -0
  22. package/dist/builtins/kinds/opencode-cli-config.js.map +1 -0
  23. package/dist/builtins/kinds/opencode-command.d.ts +3 -0
  24. package/dist/builtins/kinds/opencode-command.d.ts.map +1 -0
  25. package/dist/builtins/kinds/opencode-command.js +8 -0
  26. package/dist/builtins/kinds/opencode-command.js.map +1 -0
  27. package/dist/builtins/kinds/opencode-plugin-dir.d.ts +3 -0
  28. package/dist/builtins/kinds/opencode-plugin-dir.d.ts.map +1 -0
  29. package/dist/builtins/kinds/opencode-plugin-dir.js +11 -0
  30. package/dist/builtins/kinds/opencode-plugin-dir.js.map +1 -0
  31. package/dist/builtins/kinds/opencode-plugin.js +1 -1
  32. package/dist/builtins/kinds/opencode-plugin.js.map +1 -1
  33. package/dist/builtins/tools/opencode.d.ts.map +1 -1
  34. package/dist/builtins/tools/opencode.js +13 -1
  35. package/dist/builtins/tools/opencode.js.map +1 -1
  36. package/dist/builtins/tools/pi.d.ts.map +1 -1
  37. package/dist/builtins/tools/pi.js +2 -1
  38. package/dist/builtins/tools/pi.js.map +1 -1
  39. package/dist/cli/commands/diff.d.ts +10 -0
  40. package/dist/cli/commands/diff.d.ts.map +1 -1
  41. package/dist/cli/commands/diff.js +71 -30
  42. package/dist/cli/commands/diff.js.map +1 -1
  43. package/dist/cli/commands/info.d.ts +10 -0
  44. package/dist/cli/commands/info.d.ts.map +1 -1
  45. package/dist/cli/commands/info.js +45 -1
  46. package/dist/cli/commands/info.js.map +1 -1
  47. package/dist/cli/commands/list.d.ts +18 -0
  48. package/dist/cli/commands/list.d.ts.map +1 -1
  49. package/dist/cli/commands/list.js +27 -0
  50. package/dist/cli/commands/list.js.map +1 -1
  51. package/dist/cli/commands/session.d.ts +3 -0
  52. package/dist/cli/commands/session.d.ts.map +1 -0
  53. package/dist/cli/commands/session.js +68 -0
  54. package/dist/cli/commands/session.js.map +1 -0
  55. package/dist/cli/commands/status.d.ts +10 -0
  56. package/dist/cli/commands/status.d.ts.map +1 -1
  57. package/dist/cli/commands/status.js +100 -2
  58. package/dist/cli/commands/status.js.map +1 -1
  59. package/dist/cli/commands/sync.d.ts.map +1 -1
  60. package/dist/cli/commands/sync.js +6 -0
  61. package/dist/cli/commands/sync.js.map +1 -1
  62. package/dist/cli/commands/tui.d.ts +3 -0
  63. package/dist/cli/commands/tui.d.ts.map +1 -0
  64. package/dist/cli/commands/tui.js +20 -0
  65. package/dist/cli/commands/tui.js.map +1 -0
  66. package/dist/cli/index.d.ts.map +1 -1
  67. package/dist/cli/index.js +4 -0
  68. package/dist/cli/index.js.map +1 -1
  69. package/dist/core/discovery.js +1 -1
  70. package/dist/core/discovery.js.map +1 -1
  71. package/dist/core/import-discovery.d.ts.map +1 -1
  72. package/dist/core/import-discovery.js +2 -0
  73. package/dist/core/import-discovery.js.map +1 -1
  74. package/dist/core/registry.d.ts +5 -0
  75. package/dist/core/registry.d.ts.map +1 -1
  76. package/dist/core/registry.js.map +1 -1
  77. package/dist/core/render.d.ts.map +1 -1
  78. package/dist/core/render.js +77 -8
  79. package/dist/core/render.js.map +1 -1
  80. package/dist/core/resolve.d.ts +1 -1
  81. package/dist/core/resolve.d.ts.map +1 -1
  82. package/dist/core/resolve.js +3 -2
  83. package/dist/core/resolve.js.map +1 -1
  84. package/dist/core/runtime.d.ts +4 -0
  85. package/dist/core/runtime.d.ts.map +1 -1
  86. package/dist/core/runtime.js +10 -3
  87. package/dist/core/runtime.js.map +1 -1
  88. package/dist/core/schema.d.ts +59 -0
  89. package/dist/core/schema.d.ts.map +1 -1
  90. package/dist/core/schema.js +5 -0
  91. package/dist/core/schema.js.map +1 -1
  92. package/dist/core/types.d.ts +13 -0
  93. package/dist/core/types.d.ts.map +1 -1
  94. package/dist/integrations/opencode/advertisement.d.ts +7 -0
  95. package/dist/integrations/opencode/advertisement.d.ts.map +1 -0
  96. package/dist/integrations/opencode/advertisement.js +17 -0
  97. package/dist/integrations/opencode/advertisement.js.map +1 -0
  98. package/dist/integrations/opencode/client.d.ts +12 -0
  99. package/dist/integrations/opencode/client.d.ts.map +1 -0
  100. package/dist/integrations/opencode/client.js +48 -0
  101. package/dist/integrations/opencode/client.js.map +1 -0
  102. package/dist/integrations/opencode/command.d.ts +11 -0
  103. package/dist/integrations/opencode/command.d.ts.map +1 -0
  104. package/dist/integrations/opencode/command.js +19 -0
  105. package/dist/integrations/opencode/command.js.map +1 -0
  106. package/dist/integrations/opencode/delta.d.ts +15 -0
  107. package/dist/integrations/opencode/delta.d.ts.map +1 -0
  108. package/dist/integrations/opencode/delta.js +75 -0
  109. package/dist/integrations/opencode/delta.js.map +1 -0
  110. package/dist/integrations/opencode/session.d.ts +207 -0
  111. package/dist/integrations/opencode/session.d.ts.map +1 -0
  112. package/dist/integrations/opencode/session.js +201 -0
  113. package/dist/integrations/opencode/session.js.map +1 -0
  114. package/dist/lib/gitignore.d.ts +7 -3
  115. package/dist/lib/gitignore.d.ts.map +1 -1
  116. package/dist/lib/gitignore.js +16 -6
  117. package/dist/lib/gitignore.js.map +1 -1
  118. package/dist/tui/core/actions.d.ts +8 -0
  119. package/dist/tui/core/actions.d.ts.map +1 -0
  120. package/dist/tui/core/actions.js +76 -0
  121. package/dist/tui/core/actions.js.map +1 -0
  122. package/dist/tui/core/controller.d.ts +28 -0
  123. package/dist/tui/core/controller.d.ts.map +1 -0
  124. package/dist/tui/core/controller.js +78 -0
  125. package/dist/tui/core/controller.js.map +1 -0
  126. package/dist/tui/core/data.d.ts +43 -0
  127. package/dist/tui/core/data.d.ts.map +1 -0
  128. package/dist/tui/core/data.js +161 -0
  129. package/dist/tui/core/data.js.map +1 -0
  130. package/dist/tui/core/host.d.ts +15 -0
  131. package/dist/tui/core/host.d.ts.map +1 -0
  132. package/dist/tui/core/host.js +2 -0
  133. package/dist/tui/core/host.js.map +1 -0
  134. package/dist/tui/core/intent.d.ts +95 -0
  135. package/dist/tui/core/intent.d.ts.map +1 -0
  136. package/dist/tui/core/intent.js +2 -0
  137. package/dist/tui/core/intent.js.map +1 -0
  138. package/dist/tui/core/keymap.d.ts +16 -0
  139. package/dist/tui/core/keymap.d.ts.map +1 -0
  140. package/dist/tui/core/keymap.js +68 -0
  141. package/dist/tui/core/keymap.js.map +1 -0
  142. package/dist/tui/core/model.d.ts +65 -0
  143. package/dist/tui/core/model.d.ts.map +1 -0
  144. package/dist/tui/core/model.js +34 -0
  145. package/dist/tui/core/model.js.map +1 -0
  146. package/dist/tui/core/selection.d.ts +12 -0
  147. package/dist/tui/core/selection.d.ts.map +1 -0
  148. package/dist/tui/core/selection.js +42 -0
  149. package/dist/tui/core/selection.js.map +1 -0
  150. package/dist/tui/core/theme.d.ts +15 -0
  151. package/dist/tui/core/theme.d.ts.map +1 -0
  152. package/dist/tui/core/theme.js +11 -0
  153. package/dist/tui/core/theme.js.map +1 -0
  154. package/dist/tui/core/update.d.ts +8 -0
  155. package/dist/tui/core/update.d.ts.map +1 -0
  156. package/dist/tui/core/update.js +206 -0
  157. package/dist/tui/core/update.js.map +1 -0
  158. package/dist/tui/core/view.d.ts +4 -0
  159. package/dist/tui/core/view.d.ts.map +1 -0
  160. package/dist/tui/core/view.js +162 -0
  161. package/dist/tui/core/view.js.map +1 -0
  162. package/dist/tui/core/viewport.d.ts +5 -0
  163. package/dist/tui/core/viewport.d.ts.map +1 -0
  164. package/dist/tui/core/viewport.js +33 -0
  165. package/dist/tui/core/viewport.js.map +1 -0
  166. package/dist/tui/core/viewspec.d.ts +49 -0
  167. package/dist/tui/core/viewspec.d.ts.map +1 -0
  168. package/dist/tui/core/viewspec.js +2 -0
  169. package/dist/tui/core/viewspec.js.map +1 -0
  170. package/dist/tui/hosts/pi/embed-entry.d.ts +3 -0
  171. package/dist/tui/hosts/pi/embed-entry.d.ts.map +1 -0
  172. package/dist/tui/hosts/pi/embed-entry.js +96 -0
  173. package/dist/tui/hosts/pi/embed-entry.js.map +1 -0
  174. package/dist/tui/hosts/pi/inject.d.ts +49 -0
  175. package/dist/tui/hosts/pi/inject.d.ts.map +1 -0
  176. package/dist/tui/hosts/pi/inject.js +31 -0
  177. package/dist/tui/hosts/pi/inject.js.map +1 -0
  178. package/dist/tui/hosts/standalone.d.ts +2 -0
  179. package/dist/tui/hosts/standalone.d.ts.map +1 -0
  180. package/dist/tui/hosts/standalone.js +109 -0
  181. package/dist/tui/hosts/standalone.js.map +1 -0
  182. package/dist/tui/skins/pi-tui/input.d.ts +5 -0
  183. package/dist/tui/skins/pi-tui/input.d.ts.map +1 -0
  184. package/dist/tui/skins/pi-tui/input.js +42 -0
  185. package/dist/tui/skins/pi-tui/input.js.map +1 -0
  186. package/dist/tui/skins/pi-tui/pi-theme-adapter.d.ts +4 -0
  187. package/dist/tui/skins/pi-tui/pi-theme-adapter.d.ts.map +1 -0
  188. package/dist/tui/skins/pi-tui/pi-theme-adapter.js +27 -0
  189. package/dist/tui/skins/pi-tui/pi-theme-adapter.js.map +1 -0
  190. package/dist/tui/skins/pi-tui/render.d.ts +38 -0
  191. package/dist/tui/skins/pi-tui/render.d.ts.map +1 -0
  192. package/dist/tui/skins/pi-tui/render.js +162 -0
  193. package/dist/tui/skins/pi-tui/render.js.map +1 -0
  194. package/dist/tui/skins/pi-tui/theme.d.ts +10 -0
  195. package/dist/tui/skins/pi-tui/theme.d.ts.map +1 -0
  196. package/dist/tui/skins/pi-tui/theme.js +58 -0
  197. package/dist/tui/skins/pi-tui/theme.js.map +1 -0
  198. package/docs/archive/opencode-v2-runtime-beta-18707.md +813 -0
  199. package/docs/authoring.md +69 -14
  200. package/docs/commands.md +24 -0
  201. package/docs/compatibility.md +8 -8
  202. package/docs/concepts.md +4 -2
  203. package/docs/opencode-v2-probes.md +253 -0
  204. package/docs/opencode-v2-runtime.md +129 -0
  205. package/docs/registry.md +1366 -0
  206. package/docs/runtime.md +115 -50
  207. package/docs/troubleshooting.md +20 -1
  208. package/docs/tui-architecture.md +503 -0
  209. package/package.json +17 -3
@@ -0,0 +1,503 @@
1
+ # Loadouts TUI Architecture
2
+
3
+ Status: **Historical design / RFC**. The three-layer core/skin/host architecture is
4
+ implemented. V1 runtime wiring and command names below are superseded by
5
+ [the OpenCode V2 runtime](opencode-v2-runtime.md) and [usage](runtime.md).
6
+ Author: design pass, June 2026
7
+
8
+ A plan for turning the loadouts TUI from a passive popup into a beautiful, ergonomic,
9
+ standalone dashboard — in the spirit of `lazygit`, `mason.nvim`, `lazy.nvim`, and a
10
+ Call-of-Duty "create-a-class" menu — that can also run **embedded at runtime inside
11
+ OpenCode and Pi**, while preserving the `/loadouts <cmd>` entry points.
12
+
13
+ ---
14
+
15
+ ## 1. Goals & non-goals
16
+
17
+ ### Goals
18
+
19
+ - A genuinely interactive dashboard: browse loadouts, **activate/deactivate from a list**,
20
+ inspect their artifacts ("slots"), preview diffs, fix drift — all keyboard-driven.
21
+ - **One stack, multiple hosts.** Maximize shared code across:
22
+ - a standalone `loadouts tui` binary (any terminal),
23
+ - embedded in **Pi** at runtime (most important use case),
24
+ - embedded in **OpenCode** at runtime,
25
+ - launched from Cursor / Claude Code terminals (standalone, not embedded).
26
+ - Keep the existing `/loadouts <cmd>` fast paths; add `/loadouts` (no args) → open dashboard.
27
+ - Immediate toggle + undo (lazy/mason feel), with an optional diff preview before applying.
28
+
29
+ ### Non-goals (for v1)
30
+
31
+ - Replacing the CLI. The TUI is additive; `loadouts <cmd>` stays the source of truth.
32
+ - A GUI/webview for Cursor. Cursor is Electron/VS Code and has no terminal-TUI surface;
33
+ it gets the standalone binary in its integrated terminal.
34
+ - Native embedding inside Claude Code (no TUI plugin API); it gets the standalone binary.
35
+
36
+ ---
37
+
38
+ ## 2. The hard constraint: three incompatible renderers
39
+
40
+ Each host that can *embed* UI uses a different, mutually incompatible rendering model:
41
+
42
+ | Host | Renderer | Component model |
43
+ | ----------- | -------------------------------- | ------------------------------------------------- |
44
+ | **Pi** | `@earendil-works/pi-tui` | Imperative classes: `render(width): string[]`, `handleInput(data)` |
45
+ | **OpenCode**| `@opentui/solid` | JSX + Solid reactivity: `<box>`, `<text>` |
46
+ | Standalone | (free choice) | — |
47
+
48
+ There is **no shared JSX runtime** between Pi's `pi-tui` and OpenCode's `@opentui/solid`.
49
+ A single component tree cannot natively render in both. (An earlier assumption that Pi is
50
+ Ink-based was incorrect; the conclusion is the same — its renderer is incompatible with
51
+ `@opentui/solid`.)
52
+
53
+ ### Decision: `pi-tui` is the primary substrate; OpenCode gets a thin secondary skin
54
+
55
+ `@earendil-works/pi-tui` is a **standalone, npm-publishable** terminal UI library
56
+ (`new TUI(new ProcessTerminal())`), with built-in `SelectList`, `SettingsList`, `Box`,
57
+ `Text`, `Markdown`, overlays, theming, and key handling. Therefore:
58
+
59
+ - **Standalone `loadouts tui`** → built on `pi-tui`.
60
+ - **Pi embedded** → same `pi-tui` components mounted via `ctx.ui.custom(...)`.
61
+ - → Standalone and Pi share the rendering skin **1:1** (covers the priority use case).
62
+ - **OpenCode embedded** → a separate, thin `@opentui/solid` skin.
63
+
64
+ Both skins are **dumb projections** of one shared, headless view-model (below), so the
65
+ OpenCode skin stays small and no business logic is duplicated.
66
+
67
+ > If a single renderer across all hosts ever becomes a hard requirement, the fallback is to
68
+ > launch the standalone `pi-tui` binary from OpenCode instead of embedding — but native
69
+ > embedding via the thin `@opentui/solid` skin is preferred for UX.
70
+
71
+ ---
72
+
73
+ ## 3. Layered architecture
74
+
75
+ ```
76
+ ┌──────────────────────────────────────────────────────────────────────┐
77
+ │ CORE (src/tui/core/) — framework-agnostic, zero UI deps │
78
+ │ • data: read loadout/active/drift state (via `loadouts --json` │
79
+ │ or direct calls into src/core) │
80
+ │ • actions: activate / deactivate / sync / clear / diff │
81
+ │ (wrap render-engine.applyTargetSet, planRender, doctor) │
82
+ │ • reducer: Model + Intent -> Model (Elm-style state machine) │
83
+ │ • view: Model -> ViewSpec (declarative render spec) │
84
+ │ Pure & snapshot-testable. No terminal, no host APIs. │
85
+ └───────────────┬───────────────────────────────────────────┬──────────┘
86
+ │ ViewSpec (data) Intent (data) │
87
+ ┌───────▼─────────┐ ┌────────▼─────────┐
88
+ │ SKIN: pi-tui │ │ SKIN: opentui │
89
+ │ ViewSpec→pi-tui │ │ ViewSpec→<box> │
90
+ │ keys→Intent │ │ keymap→Intent │
91
+ └───┬─────────┬───┘ └────────┬─────────┘
92
+ │ │ │
93
+ ┌────────▼──┐ ┌───▼──────────┐ ┌────────▼─────────┐
94
+ │ HOST: │ │ HOST: Pi │ │ HOST: OpenCode │
95
+ │ standalone│ │ extension │ │ tui plugin + │
96
+ │ ProcessTUI│ │ (command + │ │ server plugin │
97
+ │ + fs state│ │ inject + ui) │ │ (route + inject) │
98
+ └───────────┘ └──────────────┘ └──────────────────┘
99
+ ```
100
+
101
+ Three layers:
102
+
103
+ 1. **Core** — all logic and state. Shared everywhere. (TEA: `Model`, `Intent`, `update`, `view`.)
104
+ 2. **Skin** — translate `ViewSpec` → native widgets, and native key input → `Intent`. Two skins.
105
+ 3. **Host adapter** — mount the skin, register `/loadouts`, perform runtime injection. Three adapters.
106
+
107
+ ---
108
+
109
+ ## 4. Proposed module layout
110
+
111
+ ```
112
+ src/tui/
113
+ core/
114
+ model.ts # Model (state) + initial state
115
+ intent.ts # Intent union (all user actions, host-agnostic)
116
+ update.ts # (Model, Intent) => Model [pure]
117
+ view.ts # Model => ViewSpec [pure]
118
+ viewspec.ts # ViewSpec types (rows, panes, footer, overlay, style tokens)
119
+ data.ts # DashboardData provider (reads loadout/active/drift state)
120
+ actions.ts # async ops: activate/deactivate/sync/clear/diff (effects)
121
+ theme.ts # ThemeTokens (abstract palette) + mapping helpers
122
+ keymap.ts # abstract key->Intent table (single source for help overlay)
123
+ skins/
124
+ pi-tui/
125
+ render.ts # ViewSpec => pi-tui Container/SelectList/Box/Text
126
+ input.ts # pi-tui key data => Intent
127
+ theme.ts # ThemeTokens => pi-tui theme.fg/bg callbacks
128
+ opentui/
129
+ Dashboard.tsx # ViewSpec => <box>/<text> (Solid)
130
+ input.ts # opentui keymap layer => Intent
131
+ theme.ts # ThemeTokens => api.theme.current
132
+ hosts/
133
+ standalone.ts # `loadouts tui` entry: ProcessTerminal + pi-tui skin
134
+ pi/
135
+ extension.ts # default ExtensionAPI factory: command + inject + mount
136
+ inject.ts # before_agent_start system-prompt block
137
+ opencode/
138
+ index.ts # server entry: Plugin.define({ id, setup })
139
+ tui.tsx # TUI entry: router page, slash, slots
140
+ ```
141
+
142
+ The V1-shaped `src/integrations/opencode-runtime/` was replaced by
143
+ `src/integrations/opencode/` (session activator, instruction delta, service client,
144
+ advertisement filter). The Pi equivalent lives in `hosts/pi/` and the shared
145
+ `core/controller.ts` lifecycle.
146
+
147
+ ---
148
+
149
+ ## 5. The view-model (headless core)
150
+
151
+ The core is an Elm-style state machine. The renderers never see loadouts domain types — only
152
+ `ViewSpec` and `Intent`.
153
+
154
+ ### 5.1 Model (state)
155
+
156
+ ```ts
157
+ type Section = "active" | "available" | "issues";
158
+ type ActivationMode = "filesystem" | "runtime"; // fs = persistent; runtime = this session
159
+
160
+ interface LoadoutRow {
161
+ name: string;
162
+ scope: "global" | "project";
163
+ description?: string;
164
+ status: "active" | "available" | "drift" | "broken";
165
+ activation: ActivationMode | null; // how it is currently active, if at all
166
+ counts: { rules: number; skills: number; instructions: number; extensions: number };
167
+ tools: string[]; // tools this loadout targets
168
+ issue?: string; // e.g. "missing include: foo"
169
+ }
170
+
171
+ interface Model {
172
+ rows: LoadoutRow[];
173
+ sections: Record<Section, string[]>; // ordered row names per section
174
+ cursor: { section: Section; index: number };
175
+ filter: { text: string; scope?: "global" | "project"; tool?: string };
176
+ mode: ActivationMode; // default target for toggles (host-defaulted)
177
+ staged?: Set<string>; // optional plan-mode (defer apply)
178
+ undo?: UndoSnapshot; // last applied set, for `u`
179
+ detail?: DetailModel; // expanded artifact view of cursor row
180
+ overlay?: "help" | "diff" | "confirm" | null;
181
+ busy?: { label: string } | null; // async op in flight
182
+ lastResult?: { text: string; variant: "info" | "success" | "error" };
183
+ }
184
+ ```
185
+
186
+ ### 5.2 Intent (every user action, host-agnostic)
187
+
188
+ ```ts
189
+ type Intent =
190
+ | { t: "move"; delta: number }
191
+ | { t: "section"; dir: 1 | -1 }
192
+ | { t: "toggle" } // space: activate/deactivate cursor row
193
+ | { t: "activate" } | { t: "deactivate" }
194
+ | { t: "sync" } | { t: "clear" }
195
+ | { t: "diffPreview" }
196
+ | { t: "edit" } // open YAML in $EDITOR (host-dependent)
197
+ | { t: "enter" } // drill into detail / confirm
198
+ | { t: "filterInput"; text: string }
199
+ | { t: "cycleScope" } | { t: "cycleTool" } | { t: "cycleMode" }
200
+ | { t: "undo" } | { t: "refresh" }
201
+ | { t: "openHelp" } | { t: "closeOverlay" }
202
+ | { t: "quit" };
203
+ ```
204
+
205
+ ### 5.3 ViewSpec (declarative render output — the contract with skins)
206
+
207
+ ```ts
208
+ type StyleToken =
209
+ | "text" | "muted" | "dim" | "accent"
210
+ | "success" | "error" | "warning" | "border";
211
+
212
+ interface SpecSpan { text: string; style?: StyleToken }
213
+ interface SpecRow {
214
+ id: string;
215
+ glyph: { char: string; style: StyleToken }; // ✓ + ~ - ! ? — (visual-language.md)
216
+ spans: SpecSpan[]; // pre-formatted, renderer pads/truncates
217
+ selected: boolean;
218
+ }
219
+ interface SpecPane { title: string; rows: SpecRow[] } // list panes (sections)
220
+ interface SpecDetail { title: string; blocks: SpecSpan[][] } // detail pane lines
221
+ interface SpecFooter {
222
+ equipped: SpecSpan[]; // the COD "EQUIPPED:" status bar
223
+ keys: { key: string; label: string }[]; // hint bar (from keymap.ts)
224
+ }
225
+ interface ViewSpec {
226
+ panes: SpecPane[];
227
+ detail?: SpecDetail;
228
+ footer: SpecFooter;
229
+ overlay?: { kind: "help" | "diff" | "confirm"; title: string; lines: SpecSpan[][] };
230
+ }
231
+ ```
232
+
233
+ A skin's entire job: render `ViewSpec` (mapping `StyleToken` → host colors, padding/truncating
234
+ spans to width) and map native key events → `Intent`. Both skins are interpreters of this spec,
235
+ so they stay thin and visually consistent. `ViewSpec` is snapshot-testable with zero terminal.
236
+
237
+ ### 5.4 Effects / actions
238
+
239
+ `update` stays pure; async work returns an effect descriptor the host runs, then feeds the
240
+ result back as an `Intent`-like message:
241
+
242
+ ```ts
243
+ type Effect =
244
+ | { t: "apply"; targetSet: string[]; mode: ActivationMode } // activate/deactivate/sync
245
+ | { t: "clear"; mode: ActivationMode }
246
+ | { t: "plan"; targetSet: string[] } // diff preview
247
+ | { t: "reload" } // re-read data
248
+ | { t: "editFile"; path: string };
249
+ ```
250
+
251
+ `actions.ts` implements effects:
252
+ - **filesystem mode** → call `render-engine.applyTargetSet` / `planRender` / `removeManaged`
253
+ (or shell out to `loadouts <cmd> --json`).
254
+ - **runtime mode** → call the host's injection adapter (Pi `before_agent_start`,
255
+ OpenCode `experimental.chat.system.transform`) via the existing runtime bridge.
256
+
257
+ ---
258
+
259
+ ## 6. Host adapters
260
+
261
+ Each host implements a small `HostContext` and mounts a skin.
262
+
263
+ ```ts
264
+ interface HostContext {
265
+ theme(): ThemeTokens; // host palette mapped into abstract tokens
266
+ requestRender(): void; // ask host to re-render the mounted skin
267
+ exit(): void; // close dashboard (route/overlay/process)
268
+ openEditor(path: string): Promise<void>;
269
+ data: DashboardData; // read state (loadouts, active set, drift)
270
+ runtime?: RuntimeInjector; // present only where runtime activation exists
271
+ }
272
+
273
+ interface RuntimeInjector { // session-scoped activation
274
+ activate(names: string[], scope: "local" | "global"): Promise<void>;
275
+ deactivate(): Promise<void>;
276
+ status(): RuntimeStatus;
277
+ }
278
+ ```
279
+
280
+ ### 6.1 Pi host (`@earendil-works/pi-coding-agent` extension)
281
+
282
+ Pi's extension API is a first-class fit — arguably the best host of the three:
283
+
284
+ | Need | Pi API |
285
+ | ---------------------------- | ---------------------------------------------------------------- |
286
+ | `/loadouts` command | `pi.registerCommand("loadouts", { handler })` |
287
+ | `/loadouts` (no args) → open | handler calls `ctx.ui.custom(mountDashboard, { overlay: true })` |
288
+ | Keyboard dashboard | `ctx.ui.custom(component)` with `render/handleInput/invalidate` |
289
+ | Runtime injection | `pi.on("before_agent_start", … => ({ systemPrompt }))` |
290
+ | Resource discovery (skills) | `pi.on("resources_discover", … => ({ skillPaths, promptPaths, themePaths }))` |
291
+ | EQUIPPED status bar | `ctx.ui.setStatus("loadouts", …)` / `ctx.ui.setWidget(...)` |
292
+ | Hotkey to open | `pi.registerShortcut("ctrl+l", …)` |
293
+ | Toasts | `ctx.ui.notify(message, variant)` |
294
+ | Hot reload | extensions in `~/.pi/agent/extensions/` reload via `/reload` |
295
+
296
+ Distribution: ship a bundled loadout `pi-runtime` whose `include` renders a Pi extension to
297
+ `{base}/extensions/loadouts-runtime.ts` (Pi `extension` kind already exists in
298
+ `src/builtins/tools/pi.ts:20`). The extension bundles the `core` + `pi-tui` skin.
299
+
300
+ Notable upside: `resources_discover` lets Pi natively pick up loadout-contributed
301
+ skills/prompts/themes at runtime — a cleaner injection path than OpenCode's system-block.
302
+
303
+ ### 6.2 OpenCode host (existing plugin, upgraded)
304
+
305
+ Reuse the shared controller/lifecycle in `src/tui/core/controller.ts` (command parsing, session state, system-block
306
+ injection). Add a TUI route:
307
+
308
+ | Need | OpenCode API |
309
+ | --------------------- | -------------------------------------------------------- |
310
+ | `/loadouts` command | already registered (`plugin.ts:271`) |
311
+ | `/loadouts` → open | TUI plugin registers `api.route.register("loadouts")` and a keymap layer; server plugin signals "open" via the existing cache-file event channel |
312
+ | Dashboard | `@opentui/solid` skin in `api.route` (full screen) |
313
+ | Runtime injection | `experimental.chat.system.transform` (`plugin.ts:318`) |
314
+ | EQUIPPED bar | `api.slots.register` (e.g. `session_prompt_right`) |
315
+ | Theme | `api.theme.current` |
316
+
317
+ The current passive popup (`bundled/opencode/plugins/loadouts-runtime-tui.tsx`) is replaced by
318
+ a real route; the toast/event channel remains for non-interactive `/loadouts <cmd>` feedback.
319
+
320
+ ### 6.3 Standalone host (`loadouts tui`)
321
+
322
+ - New CLI command (`src/cli/commands/tui.ts`) wired into `src/cli/index.ts`; optionally make
323
+ bare `loadouts` default to it.
324
+ - Mounts the `pi-tui` skin against `new TUI(new ProcessTerminal())`.
325
+ - Only **filesystem** activation (no session to inject into) — `mode` is fixed to `filesystem`,
326
+ `runtime` toggle hidden.
327
+ - This binary is also the integration for Cursor / Claude Code (run it in their terminal).
328
+
329
+ ---
330
+
331
+ ## 7. The `--json` CLI contract (prerequisite)
332
+
333
+ The TUI must consume structured data, not scrape formatted tables. Add a stable
334
+ `--json` output mode to the read/▶︎mutate commands the dashboard needs. Proposed shapes:
335
+
336
+ ```jsonc
337
+ // loadouts list --json
338
+ { "loadouts": [ { "name": "backend-py", "scope": "project", "description": "...",
339
+ "status": "active", "tools": ["claude-code","cursor","opencode"],
340
+ "counts": { "rules": 3, "skills": 0, "instructions": 1, "extensions": 0 } } ] }
341
+
342
+ // loadouts status --json (active set + drift)
343
+ { "active": ["backend-py","review-mode"], "drift": [ { "name": "data-eng",
344
+ "kind": "rule", "path": ".cursor/rules/x.mdc", "reason": "modified" } ] }
345
+
346
+ // loadouts info <name> --json (artifact-level "slots")
347
+ { "name": "backend-py", "items": [ { "kind": "rule", "relativePath": "py-style.md",
348
+ "tools": ["claude-code","cursor"] } ] }
349
+
350
+ // loadouts diff <names...> --json (planRender output)
351
+ { "changes": [ { "tool": "cursor", "path": ".cursor/rules/x.mdc", "op": "overwrite" } ] }
352
+ ```
353
+
354
+ Mutations (`activate`, `deactivate`, `sync`, `clear`) should accept `--json` and emit a
355
+ result envelope `{ ok, applied, removed, shadowed, error? }`. Standalone uses these (or calls
356
+ `src/core` directly); embedded hosts may prefer direct `src/core` calls to avoid spawning.
357
+
358
+ ---
359
+
360
+ ## 8. `/loadouts` entry-point integration
361
+
362
+ Symmetric model across surfaces:
363
+
364
+ | Input | Behavior |
365
+ | ----------------------------- | ----------------------------------------------------- |
366
+ | `/loadouts` (no args) | **Open the interactive dashboard** (route/overlay) |
367
+ | `/loadouts activate x [-l/-g]`| Fast non-interactive op (current behavior, unchanged) |
368
+ | `/loadouts status \| list \| info \| ...` | Fast path; result via toast/notify |
369
+ | `loadouts` (terminal) | Open standalone dashboard (or keep help; configurable)|
370
+ | `loadouts activate x` | CLI op (unchanged) |
371
+
372
+ The slash parser (`src/integrations/opencode-runtime/command.ts`) already maps aliases and
373
+ scope flags; extend it so an empty action opens the dashboard instead of printing help. Pi's
374
+ `registerCommand("loadouts")` handler mirrors the same parse → dispatch.
375
+
376
+ ---
377
+
378
+ ## 9. UX specification
379
+
380
+ ### 9.1 Layout (master-detail, mason-style)
381
+
382
+ ```
383
+ ┌─ LOADOUTS ────────────────────────────────────┬─ backend-py ───────────────────┐
384
+ │ ▸ Active (2) │ scope: project · active · synced│
385
+ │ ✓ backend-py project 3 rules [fs] │ "Python backend conventions" │
386
+ │ ✓ review-mode global 1 skill [session] │ │
387
+ │ ▸ Available (8) │ Artifacts (slots) │
388
+ │ - frontend-react project │ rule py-style → cc cu │
389
+ │ ~ data-eng project DRIFT │ skill pytest-run → cc op │
390
+ │ - security-audit global │ instr onboarding → all │
391
+ │ ▸ Issues (1) │ │
392
+ │ ! broken-loadout missing include │ Renders to │
393
+ │ │ claude-code cursor opencode │
394
+ │ │ ✓ + ✓ │
395
+ └────────────────────────────────────────────────┴────────────────────────────────┘
396
+ EQUIPPED: backend-py + review-mode → cc, cursor, opencode · in sync
397
+ [space] toggle [a]ctivate [d]eactivate [s]ync [D]iff [e]dit [/]find [?]help
398
+ ```
399
+
400
+ - Sections **Active / Available / Issues**; glyphs reuse `docs/visual-language.md`
401
+ (`✓ + ~ - ! ? —`).
402
+ - `[fs]` vs `[session]` badge makes filesystem vs runtime activation legible.
403
+ - Detail pane shows a loadout's artifacts as **"slots"** (COD create-a-class), each with its
404
+ target tools, plus a per-tool render-status row.
405
+ - The **EQUIPPED** bar is the COD flourish: current set, render targets, sync state. Injected
406
+ counts can reuse the logic in `runtimeActivationToast` (`opencode-runtime/plugin.ts:154`).
407
+
408
+ ### 9.2 Keymap (single source → drives the `?` overlay)
409
+
410
+ | Key | Intent |
411
+ | -------------- | --------------------------------------- |
412
+ | `j/k` `↑/↓` | move |
413
+ | `tab`/`S-tab` | cycle section |
414
+ | `space` | toggle activate/deactivate (immediate) |
415
+ | `a` / `d` | explicit activate / deactivate |
416
+ | `s` / `x` | sync / clear all |
417
+ | `enter` | drill into artifacts / confirm |
418
+ | `D` | diff preview overlay (`planRender`) |
419
+ | `e` | edit YAML in `$EDITOR` |
420
+ | `m` | cycle activation mode (fs ↔ session) |
421
+ | `g` / `t` | cycle scope / tool filter |
422
+ | `/` | fuzzy filter |
423
+ | `u` / `r` | undo / refresh |
424
+ | `?` / `q` | help / quit |
425
+
426
+ **Apply model:** immediate toggle + `u` undo (chosen). `staged`/plan-mode is reserved in the
427
+ `Model` for a later opt-in, and `D` gives a pre-apply diff for the cautious path.
428
+
429
+ ---
430
+
431
+ ## 10. Runtime vs filesystem activation in the UI
432
+
433
+ The dashboard surfaces both existing activation models (see `src/core/runtime.ts`):
434
+
435
+ - **Filesystem** (`render-engine.applyTargetSet`): persistent, writes files, tracked in
436
+ `.state.json`. Available in every host (and the only mode standalone offers).
437
+ - **Runtime** (session-injected): ephemeral, this-session-only.
438
+ - OpenCode: `experimental.chat.system.transform` (built).
439
+ - Pi: `before_agent_start` `{ systemPrompt }` + `resources_discover` (to build).
440
+
441
+ `mode` defaults per host (embedded → `runtime`, standalone → `filesystem`), is shown in the
442
+ EQUIPPED bar, and is toggled with `m`. Rows display which mode they're active under.
443
+
444
+ ---
445
+
446
+ ## 11. Packaging
447
+
448
+ Bundled loadouts (in `bundled/loadouts/`) distribute the host integrations, matching the
449
+ existing `opencode-runtime.yaml` pattern:
450
+
451
+ - `opencode-runtime.yaml` — existing; add the route-based TUI plugin alongside the server plugin.
452
+ - `pi-runtime.yaml` (new) — `include:` a Pi extension rendered to
453
+ `~/.pi/agent/extensions/loadouts-runtime.ts` (the `extension` kind in `pi.ts`).
454
+
455
+ The shared `core` + skins are compiled/bundled into each host's artifact (Pi loads `.ts`
456
+ extensions via jiti; OpenCode loads the `.tsx` plugin). The standalone skin ships in the main
457
+ `loadouts` binary.
458
+
459
+ ---
460
+
461
+ ## 12. Testing strategy
462
+
463
+ - **Core (pure):** unit-test `update` (Intent → Model) and snapshot-test `view`
464
+ (Model → ViewSpec). No terminal required. This is where most behavior is verified.
465
+ - **Skins:** `pi-tui` ships a `VirtualTerminal` (`@xterm/headless`) for headless rendering
466
+ assertions; OpenCode skin tested via tmux capture per `opencode-tui-plugins` skill.
467
+ - **Hosts:** integration-test command parsing + injection (existing
468
+ `opencode-runtime/*.test.ts` pattern); add a Pi equivalent.
469
+ - **E2E:** reuse the `runtime-injection-e2e` approach for the runtime activation path.
470
+
471
+ ---
472
+
473
+ ## 13. Phased plan
474
+
475
+ 1. **Core + `--json`.** Build `src/tui/core/` (Model/Intent/update/view/ViewSpec/data/actions)
476
+ and add `--json` to `list/status/info/diff` + mutation envelopes. Fully unit-tested,
477
+ no UI. *Largest, highest-leverage step.*
478
+ 2. **Standalone + `pi-tui` skin.** Ship `loadouts tui` on `pi-tui`. Filesystem mode only.
479
+ This proves the whole stack end-to-end in any terminal.
480
+ 3. **Pi host.** `pi-runtime` extension: `/loadouts`, `ctx.ui.custom` dashboard (reusing the
481
+ skin from step 2), `before_agent_start` injection, `resources_discover`, EQUIPPED status.
482
+ *The priority use case.*
483
+ 4. **OpenCode host.** `@opentui/solid` skin + `api.route.register`; reuse existing
484
+ command/injection plumbing; replace the passive popup with the route.
485
+ 5. **Polish.** Fuzzy search, multi-select bulk ops, drift one-key fix, diff overlay,
486
+ empty/first-run CTA, themes, `?` help generation.
487
+
488
+ ---
489
+
490
+ ## 14. Open questions / risks
491
+
492
+ - **Bundle size / load time** of embedding `core` + `pi-tui` inside a Pi extension and
493
+ `core` + `@opentui/solid` inside an OpenCode plugin — measure; consider prebuilding.
494
+ - **Direct-core vs shell-out** for embedded hosts: calling `src/core` directly avoids spawn
495
+ cost but couples the plugin to internal APIs; `--json` shell-out is cleaner but slower.
496
+ Recommendation: direct-core for embedded, `--json` for standalone/tests.
497
+ - **Pi runtime injection fidelity:** confirm `before_agent_start` + `resources_discover` fully
498
+ cover instructions/rules/skills the way `runtime.ts` expects for `pi` (`native-runtime`).
499
+ - **Doc drift:** `src/builtins/tools/pi.ts` omits a native `rule` target, but some top-level
500
+ docs still show Pi rule paths — reconcile so the UI's tool/render columns are accurate.
501
+ - **Theme parity:** define `ThemeTokens` so both skins look consistent; respect `NO_COLOR`
502
+ standalone.
503
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loadouts",
3
- "version": "0.2.5",
3
+ "version": "0.4.0",
4
4
  "description": "Composable configuration bundles for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,11 +12,12 @@
12
12
  "bundled"
13
13
  ],
14
14
  "scripts": {
15
- "build": "tsc -p tsconfig.build.json && chmod +x dist/index.js",
15
+ "build": "tsc -p tsconfig.build.json && node scripts/build-embedded.mjs && chmod +x dist/index.js",
16
+ "build:embedded": "node scripts/build-embedded.mjs",
16
17
  "dev": "tsc --watch",
17
18
  "test": "vitest run",
18
19
  "test:watch": "vitest",
19
- "lint": "tsc --noEmit",
20
+ "lint": "tsc --noEmit -p tsconfig.lint.json && tsc --noEmit -p tsconfig.tui.json",
20
21
  "loadouts": "node dist/index.js"
21
22
  },
22
23
  "keywords": [
@@ -36,12 +37,25 @@
36
37
  "url": "https://github.com/evatths/loadouts.git"
37
38
  },
38
39
  "devDependencies": {
40
+ "@babel/core": "7.28.0",
41
+ "@babel/preset-typescript": "7.27.1",
42
+ "@earendil-works/pi-coding-agent": "^0.79.7",
43
+ "@opencode/plugin": "2.0.3",
44
+ "@opentui/core": "0.5.11",
45
+ "@opentui/keymap": "0.5.11",
46
+ "@opentui/solid": "0.5.11",
47
+ "@opencode/theme": "2.0.3",
39
48
  "@types/node": "^20.12.0",
40
49
  "@types/update-notifier": "^6.0.8",
50
+ "babel-preset-solid": "1.9.12",
51
+ "esbuild": "^0.23.1",
52
+ "solid-js": "1.9.15",
41
53
  "typescript": "^5.4.0",
42
54
  "vitest": "^1.5.0"
43
55
  },
44
56
  "dependencies": {
57
+ "@earendil-works/pi-tui": "^0.79.7",
58
+ "@opencode/client": "2.0.3",
45
59
  "chalk": "^5.3.0",
46
60
  "commander": "^12.0.0",
47
61
  "update-notifier": "^7.3.1",