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