@brimveyn/aimux-plugin 0.1.2

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/src/ui.ts ADDED
@@ -0,0 +1,326 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ import type { Disposer } from './types'
4
+
5
+ /**
6
+ * The UI half's services — what a plugin reaches through `ctx.ui`, `ctx.actions`
7
+ * and `ctx.store`.
8
+ *
9
+ * Declared here, implemented by `src/ui/plugin-host.tsx`. The split matters:
10
+ * this package must stay free of React and of aimux's internals, so a plugin
11
+ * can be typechecked with nothing but `bun install`.
12
+ *
13
+ * Ids are namespaced by the host. A plugin registers `board` and the widget
14
+ * becomes `acme.thing.board`, so two plugins can each have a "board" and the
15
+ * owner of any id is readable from the id alone.
16
+ */
17
+
18
+ /**
19
+ * What a plugin renders. `react` is an optional peer of this package, typed
20
+ * only — no runtime dependency, and any plugin drawing UI already needs
21
+ * `@types/react` to write JSX at all.
22
+ *
23
+ * The node must come from *aimux's* React at runtime. The module loader forces
24
+ * that resolution, so a plugin that simply imports `react` gets the right one
25
+ * and its hooks work.
26
+ */
27
+ export type PluginNode = ReactNode
28
+
29
+ /** A component the kit hands back, usable directly in a plugin's JSX. */
30
+ export type PluginComponent<Props> = (props: Props) => PluginNode
31
+
32
+ /** Aimux's own types, re-declared structurally where the shape is small. */
33
+ export interface PluginToastApi {
34
+ info: (message: string) => void
35
+ success: (message: string) => void
36
+ error: (message: string) => void
37
+ }
38
+
39
+ export interface PluginBarWidget {
40
+ /** Unqualified; the host prefixes the plugin id. */
41
+ id: string
42
+ label: string
43
+ render: (contentWidth: number) => PluginNode
44
+ }
45
+
46
+ export interface PluginView {
47
+ /** Unqualified; the host prefixes the plugin id. */
48
+ id: string
49
+ title: string
50
+ render: () => PluginNode
51
+ }
52
+
53
+ export interface PluginModal {
54
+ /** Unqualified; the host prefixes the plugin id. */
55
+ id: string
56
+ title: string
57
+ render: (props: unknown) => PluginNode
58
+ }
59
+
60
+ export interface PluginWidgetsApi {
61
+ register: (widget: PluginBarWidget) => Disposer
62
+ }
63
+
64
+ export interface PluginViewsApi {
65
+ register: (view: PluginView) => Disposer
66
+ /** Replaces the panes with this view. Takes the unqualified id. */
67
+ open: (id: string) => void
68
+ /** Returns to the panes. */
69
+ close: () => void
70
+ }
71
+
72
+ export interface PluginModalsApi {
73
+ register: (modal: PluginModal) => Disposer
74
+ open: (id: string, props?: unknown) => void
75
+ close: () => void
76
+ }
77
+
78
+ /** What a settings row holds. Absent when nothing has ever set it. */
79
+ export type PluginSettingValue = boolean | number | string | undefined
80
+
81
+ export interface PluginSettingsApi {
82
+ /**
83
+ * Registers a section beyond the one generated from the manifest's `config`
84
+ * schema. Most plugins need neither — declaring `config` is enough.
85
+ */
86
+ registerSection: (section: unknown) => Disposer
87
+ /**
88
+ * Reads one of *aimux's own* settings by row id — the same dotted id that
89
+ * appears in `aimux.config.ts` and on the settings screen.
90
+ *
91
+ * A plugin's own configuration is `ctx.config`; this is for the cases where
92
+ * a plugin has to agree with aimux about something the user already set,
93
+ * rather than ask them a second time.
94
+ */
95
+ get: (id: string) => PluginSettingValue
96
+ /**
97
+ * Calls back on every change to that row, and once immediately with the
98
+ * current value — so a plugin gating itself on a toggle is one call, with no
99
+ * separate "read it first" step to forget.
100
+ */
101
+ watch: (id: string, listener: (value: PluginSettingValue) => void) => Disposer
102
+ }
103
+
104
+ /**
105
+ * One tab, as a UI plugin sees it. A narrow projection on purpose: the app's
106
+ * own `TabSession` carries a viewport, terminal modes and a scrollback buffer,
107
+ * none of which a plugin has any business holding a reference to.
108
+ */
109
+ export interface PluginTabInfo {
110
+ id: string
111
+ title: string
112
+ assistant: string
113
+ status: string
114
+ /** `idle` / `working` / `waiting-input`, or null when nothing has said yet. */
115
+ activity: string | null
116
+ workspaceId?: string
117
+ }
118
+
119
+ /** What aimux is currently showing, as much of it as a plugin can act on. */
120
+ export interface PluginUiState {
121
+ tabs: readonly PluginTabInfo[]
122
+ activeTabId: string | null
123
+ /** The same tab `activeTabId` names, or null. Saved lookups add up. */
124
+ activeTab: PluginTabInfo | null
125
+ projectId: string | null
126
+ }
127
+
128
+ export interface PluginStateApi {
129
+ /** A snapshot, outside React. */
130
+ get: () => PluginUiState
131
+ /** Fires on every change, and once immediately with the current value. */
132
+ subscribe: (listener: (state: PluginUiState) => void) => Disposer
133
+ /**
134
+ * The hook a renderer wants: re-renders only when the selected value
135
+ * changes. The snapshot object is stable between changes, so selecting a
136
+ * field is cheap — selecting a *new object* re-renders every time, which is
137
+ * the same rule every store hook has.
138
+ */
139
+ use: <T>(select: (state: PluginUiState) => T) => T
140
+ }
141
+
142
+ export interface PluginPane {
143
+ /** Unqualified; the host prefixes the plugin id. */
144
+ id: string
145
+ /** Drawn in the pane's border. */
146
+ title: string
147
+ render: () => PluginNode
148
+ }
149
+
150
+ export interface PluginPanesApi {
151
+ /**
152
+ * Declares a pane: a leaf in the layout tree that draws something other than
153
+ * a terminal. A widget is a narrow strip and a view takes the whole screen;
154
+ * a pane is the one that sits *beside* an agent — a board, a diff, a log
155
+ * browser.
156
+ *
157
+ * Registering does not put it on screen; `open` does.
158
+ */
159
+ register: (pane: PluginPane) => Disposer
160
+ /**
161
+ * Splits the pane the user is in and puts this one beside it. Takes the
162
+ * unqualified id. Opening one that is already open does nothing: the id is
163
+ * the plugin's name for it, and two panes claiming it would make `close`
164
+ * ambiguous.
165
+ *
166
+ * Opening does not move the keyboard: `direction` decides where the pane
167
+ * goes, and focus stays on the terminal it was split from. The user walks
168
+ * into it with the ordinary pane-navigation keys, and its own keys are bound
169
+ * in its own mode — `plugin.pane.<pluginId>.<id>`.
170
+ */
171
+ open: (id: string, direction?: 'horizontal' | 'vertical') => void
172
+ /** Takes it off screen. The layout collapses as it would for a closed tab. */
173
+ close: (id: string) => void
174
+ }
175
+
176
+ export interface PluginStatusBarSegment {
177
+ /** Unqualified; the host prefixes the plugin id. */
178
+ id: string
179
+ render: () => PluginNode
180
+ }
181
+
182
+ export interface PluginStatusBarApi {
183
+ /**
184
+ * Adds a tile to the right of the status bar, before the version.
185
+ *
186
+ * The bar draws its own separators and tile colours around it, so a segment
187
+ * renders content and nothing else — there are no powerline glyphs to get
188
+ * wrong. Order is registration order.
189
+ */
190
+ register: (segment: PluginStatusBarSegment) => Disposer
191
+ }
192
+
193
+ export interface PluginStatsPage {
194
+ /** Unqualified; the host prefixes the plugin id. */
195
+ id: string
196
+ label: string
197
+ /** One cell, text presentation — the rule every nav glyph follows. */
198
+ glyph: string
199
+ render: () => PluginNode
200
+ }
201
+
202
+ export interface PluginStatsApi {
203
+ /** Adds a page to the stats screen's nav, after the built-in three. */
204
+ registerPage: (page: PluginStatsPage) => Disposer
205
+ }
206
+
207
+ /** Light or dark. The user picks it, or the terminal does. */
208
+ export type PluginThemeMode = 'dark' | 'light'
209
+
210
+ export interface PluginThemeSnapshot {
211
+ /** Resolved colour tokens — the same values `kit.useTheme()` returns. */
212
+ colors: Record<string, string>
213
+ mode: PluginThemeMode
214
+ }
215
+
216
+ export interface PluginThemesApi {
217
+ /** The shipped theme JSON shape. A shipped id may not be replaced. */
218
+ register: (id: string, theme: unknown) => Disposer
219
+ /**
220
+ * The active theme outside React. `kit.useTheme()` is the hook for anything
221
+ * being rendered; this is for the rest — a plugin writing the palette to a
222
+ * file, or telling another program about it.
223
+ */
224
+ current: () => PluginThemeSnapshot
225
+ /** Fires on every theme or mode change. Does not fire for the initial value. */
226
+ onChange: (listener: (snapshot: PluginThemeSnapshot) => void) => Disposer
227
+ }
228
+
229
+ /**
230
+ * The primitives a plugin renders with, already styled like the rest of aimux.
231
+ *
232
+ * Handed over on the context rather than imported: they are implemented in the
233
+ * app, against the same theme store and the same `Surface`/`ListItem` the
234
+ * built-in screens use, and re-implementing them in this package would be the
235
+ * duplication the type de-duplication just removed.
236
+ *
237
+ * Not a component library. A plugin that needs something else drops to `<box>`
238
+ * and `<text>` and styles it from `kit.useTheme()`, which is what every
239
+ * built-in view does.
240
+ */
241
+ export interface PluginKit {
242
+ /**
243
+ * The resolved theme. The one thing a plugin must not hard-code: aimux ships
244
+ * 34 themes and loads more from disk, and a plugin with its own colours is
245
+ * the part of the screen that stops matching when the user switches.
246
+ */
247
+ useTheme: () => Record<string, string>
248
+ /** A titled container — what a bar widget and a full-screen view both are. */
249
+ Panel: PluginComponent<{
250
+ children?: PluginNode
251
+ title?: string
252
+ tone?: 'muted' | 'elevated'
253
+ padding?: number
254
+ flexGrow?: number
255
+ }>
256
+ /** A label/value line — what every settings and stats row already is. */
257
+ Row: PluginComponent<{ label: PluginNode; value?: PluginNode; dim?: boolean }>
258
+ /** A selectable list, with the built-in cursor glyph and mouse wiring. */
259
+ List: PluginComponent<{
260
+ items: readonly unknown[]
261
+ selectedIndex?: number
262
+ keyOf?: (item: unknown, index: number) => string
263
+ renderItem: (item: unknown, index: number) => PluginNode
264
+ empty?: PluginNode
265
+ onSelect?: (index: number) => void
266
+ onHover?: (index: number) => void
267
+ }>
268
+ /** The footer line every modal and screen ends with. */
269
+ KeyHint: PluginComponent<{ hints: readonly { keys: string; label: string }[] }>
270
+ }
271
+
272
+ export interface PluginUiApi {
273
+ widgets: PluginWidgetsApi
274
+ views: PluginViewsApi
275
+ modals: PluginModalsApi
276
+ settings: PluginSettingsApi
277
+ themes: PluginThemesApi
278
+ toast: PluginToastApi
279
+ panes: PluginPanesApi
280
+ state: PluginStateApi
281
+ stats: PluginStatsApi
282
+ statusBar: PluginStatusBarApi
283
+ kit: PluginKit
284
+ }
285
+
286
+ /**
287
+ * Keyboard actions and their effects. Registered by unqualified verb; a user's
288
+ * keymap binds the qualified name with `k.plugin('acme.thing.open')`.
289
+ */
290
+ export interface PluginActionsApi {
291
+ /**
292
+ * The action a key produces. Receives the mode context and returns a
293
+ * `KeyResult` — the same value a built-in binding produces — or null for
294
+ * "not handled here".
295
+ */
296
+ register: (verb: string, handler: (ctx: unknown) => unknown) => Disposer
297
+ /**
298
+ * The side of a binding that is allowed to do things: spawn a tab, write a
299
+ * file, call out. Reached from an action's `KeyResult` as a `plugin-effect`.
300
+ */
301
+ effect: (effectId: string, handler: (payload: unknown) => void | Promise<void>) => Disposer
302
+ }
303
+
304
+ /**
305
+ * This plugin's slice of `AppState`, at `state.plugins[<pluginId>]`. Opaque to
306
+ * the core reducer — the shape is the plugin's, and knowing it would make the
307
+ * app depend on plugins rather than the other way round.
308
+ */
309
+ export interface PluginStoreApi<Slice = unknown> {
310
+ /** Installs the slice reducer. One per plugin; registering again replaces it. */
311
+ reducer: (
312
+ reduce: (slice: Slice | undefined, action: { actionId: string; payload?: unknown }) => Slice
313
+ ) => Disposer
314
+ /** Reads the current slice. A snapshot: it does not subscribe. */
315
+ get: () => Slice | undefined
316
+ /**
317
+ * The slice, as a hook. What a renderer wants: `get()` inside a component
318
+ * reads the right value once and then never hears about the next one, which
319
+ * is a widget that quietly stops updating.
320
+ */
321
+ use: () => Slice | undefined
322
+ /** Replaces the slice outright. */
323
+ set: (slice: Slice) => void
324
+ /** Dispatches into this plugin's reducer. */
325
+ dispatch: (actionId: string, payload?: unknown) => void
326
+ }