@brimveyn/aimux-plugin 0.1.4 → 0.1.5
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/package.json +1 -1
- package/src/daemon-api.ts +67 -0
- package/src/index.ts +16 -0
- package/src/manifest.ts +36 -0
- package/src/test-context.ts +2 -0
- package/src/test-ui.ts +88 -0
- package/src/types.ts +2 -1
- package/src/ui.ts +173 -2
package/package.json
CHANGED
package/src/daemon-api.ts
CHANGED
|
@@ -109,8 +109,30 @@ export interface PluginProjectsApi {
|
|
|
109
109
|
get: (projectId: string) => PluginProjectView | undefined
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
export interface PluginCreateWorkspaceInput {
|
|
113
|
+
projectId: string
|
|
114
|
+
name: string
|
|
115
|
+
/** Defaults to `aimux/<name>`. */
|
|
116
|
+
branch?: string
|
|
117
|
+
/** Base ref for the branch. Defaults to `HEAD`. */
|
|
118
|
+
base?: string
|
|
119
|
+
}
|
|
120
|
+
|
|
112
121
|
export interface PluginWorkspacesApi {
|
|
113
122
|
list: (projectId: string) => PluginWorkspaceView[]
|
|
123
|
+
/**
|
|
124
|
+
* Creates a git worktree and its catalog record — what `aimux workspace
|
|
125
|
+
* create` does, from inside the daemon. Resolves with the record; rejects
|
|
126
|
+
* with the same messages the CLI prints, a base ref that does not exist
|
|
127
|
+
* included.
|
|
128
|
+
*/
|
|
129
|
+
create: (input: PluginCreateWorkspaceInput) => Promise<PluginWorkspaceView>
|
|
130
|
+
/**
|
|
131
|
+
* Removes a worktree and its record. Refuses the primary workspace, and
|
|
132
|
+
* one with live tabs unless `force` says otherwise — a tab running in a
|
|
133
|
+
* directory that vanishes is a worse outcome than a rejected call.
|
|
134
|
+
*/
|
|
135
|
+
remove: (projectId: string, workspaceId: string, options?: { force?: boolean }) => Promise<void>
|
|
114
136
|
}
|
|
115
137
|
|
|
116
138
|
/**
|
|
@@ -128,6 +150,38 @@ export interface PluginMetricsApi {
|
|
|
128
150
|
counters: (days?: number) => PluginCounterDay[]
|
|
129
151
|
}
|
|
130
152
|
|
|
153
|
+
/** What aimux knows about the conversation behind a tab. */
|
|
154
|
+
export interface PluginSessionInfo {
|
|
155
|
+
tabId: string
|
|
156
|
+
assistant: string
|
|
157
|
+
/** The vendor's conversation id, when the tab was spawned with one. */
|
|
158
|
+
sessionId: string | null
|
|
159
|
+
/** The transcript on disk, when the vendor writes one and it exists yet. */
|
|
160
|
+
transcriptPath: string | null
|
|
161
|
+
/** The `--model` the tab was spawned with, when one was given. */
|
|
162
|
+
model: string | null
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Token usage of one conversation, read from its transcript. Cumulative
|
|
167
|
+
* across the whole session, which is what a token dashboard, a handoff
|
|
168
|
+
* threshold, or a "you are near the limit" nudge wants.
|
|
169
|
+
*/
|
|
170
|
+
export interface PluginSessionUsage {
|
|
171
|
+
tabId: string
|
|
172
|
+
input: number
|
|
173
|
+
output: number
|
|
174
|
+
cacheRead: number
|
|
175
|
+
cacheWrite: number
|
|
176
|
+
total: number
|
|
177
|
+
/** Billed assistant messages seen. */
|
|
178
|
+
turns: number
|
|
179
|
+
/** Total tokens per model id. */
|
|
180
|
+
models: Record<string, number>
|
|
181
|
+
/** ISO time of the last billed message, or null with no usage yet. */
|
|
182
|
+
lastAt: string | null
|
|
183
|
+
}
|
|
184
|
+
|
|
131
185
|
export interface PluginAssistantsApi {
|
|
132
186
|
/**
|
|
133
187
|
* Registers a complete assistant: spawn command, status classifier, question
|
|
@@ -135,6 +189,19 @@ export interface PluginAssistantsApi {
|
|
|
135
189
|
* `src/pty/assistant-registry.ts` for the shape.
|
|
136
190
|
*/
|
|
137
191
|
register: (definition: unknown) => Disposer
|
|
192
|
+
/** The conversation behind a tab, or undefined for an unknown tab. */
|
|
193
|
+
session: (tabId: string) => PluginSessionInfo | undefined
|
|
194
|
+
/**
|
|
195
|
+
* Reads the transcript. Zero everywhere for a tab whose assistant writes
|
|
196
|
+
* none, or has not written one yet; undefined for an unknown tab.
|
|
197
|
+
*/
|
|
198
|
+
usage: (tabId: string) => Promise<PluginSessionUsage | undefined>
|
|
199
|
+
/**
|
|
200
|
+
* Closes the tab and spawns a fresh one resuming the same conversation —
|
|
201
|
+
* the move after a rate limit, a crashed CLI, or a handoff. Resolves with
|
|
202
|
+
* the new tab id. Rejects when the tab has no session to resume.
|
|
203
|
+
*/
|
|
204
|
+
resume: (tabId: string) => Promise<string>
|
|
138
205
|
}
|
|
139
206
|
|
|
140
207
|
export interface PluginHooksApi {
|
package/src/index.ts
CHANGED
|
@@ -8,10 +8,13 @@ export type {
|
|
|
8
8
|
PluginAssistantsApi,
|
|
9
9
|
PluginCliApi,
|
|
10
10
|
PluginCounterDay,
|
|
11
|
+
PluginCreateWorkspaceInput,
|
|
11
12
|
PluginHooksApi,
|
|
12
13
|
PluginMetricsApi,
|
|
13
14
|
PluginProjectsApi,
|
|
14
15
|
PluginProjectView,
|
|
16
|
+
PluginSessionInfo,
|
|
17
|
+
PluginSessionUsage,
|
|
15
18
|
PluginSpawnTabInput,
|
|
16
19
|
PluginTabsApi,
|
|
17
20
|
PluginTabView,
|
|
@@ -24,6 +27,7 @@ export { type EventBusOptions, PluginEventBus } from './event-bus'
|
|
|
24
27
|
export {
|
|
25
28
|
PLUGIN_API_VERSION,
|
|
26
29
|
type PluginBarContribution,
|
|
30
|
+
type PluginCommandPaneSpec,
|
|
27
31
|
type PluginCommandSpec,
|
|
28
32
|
type PluginConfigField,
|
|
29
33
|
type PluginConfigFieldType,
|
|
@@ -31,6 +35,7 @@ export {
|
|
|
31
35
|
type PluginHost,
|
|
32
36
|
type PluginKeymapContribution,
|
|
33
37
|
type PluginManifest,
|
|
38
|
+
type PluginServiceSpec,
|
|
34
39
|
} from './manifest'
|
|
35
40
|
export { createTestUiSurface, type TestUiRegistrations, type TestUiSurface } from './test-ui'
|
|
36
41
|
export {
|
|
@@ -41,19 +46,30 @@ export {
|
|
|
41
46
|
type TestRpcCall,
|
|
42
47
|
} from './test-context'
|
|
43
48
|
export type {
|
|
49
|
+
PluginActionMeta,
|
|
44
50
|
PluginActionsApi,
|
|
45
51
|
PluginBarWidget,
|
|
52
|
+
PluginCommandEntry,
|
|
53
|
+
PluginCommandPane,
|
|
54
|
+
PluginCommandsApi,
|
|
46
55
|
PluginCommitMessage,
|
|
47
56
|
PluginCommitMessageRequest,
|
|
48
57
|
PluginComponent,
|
|
49
58
|
PluginGitApi,
|
|
59
|
+
PluginGitCommitInput,
|
|
50
60
|
PluginGitFile,
|
|
51
61
|
PluginGitStatus,
|
|
52
62
|
PluginKit,
|
|
63
|
+
PluginLayoutApi,
|
|
64
|
+
PluginLayoutNode,
|
|
53
65
|
PluginModal,
|
|
54
66
|
PluginModalsApi,
|
|
55
67
|
PluginNode,
|
|
68
|
+
PluginNotification,
|
|
69
|
+
PluginNotificationEvent,
|
|
70
|
+
PluginNotificationsApi,
|
|
56
71
|
PluginPane,
|
|
72
|
+
PluginPaneDirection,
|
|
57
73
|
PluginPanesApi,
|
|
58
74
|
PluginScreen,
|
|
59
75
|
PluginSettingsApi,
|
package/src/manifest.ts
CHANGED
|
@@ -71,6 +71,10 @@ export interface PluginBarContribution {
|
|
|
71
71
|
* every plugin, here as everywhere else.
|
|
72
72
|
*/
|
|
73
73
|
export interface PluginKeymapContribution {
|
|
74
|
+
/** Stable id used by user overrides. Defaults to `action`. */
|
|
75
|
+
id?: string
|
|
76
|
+
/** Human-readable label shown in settings and key help. */
|
|
77
|
+
description?: string
|
|
74
78
|
/** Mode id, e.g. `navigation`, or the plugin's own pane mode. */
|
|
75
79
|
mode: string
|
|
76
80
|
/** Key notation, `<leader>` included. */
|
|
@@ -84,6 +88,34 @@ export interface PluginContributions {
|
|
|
84
88
|
keymaps?: PluginKeymapContribution[]
|
|
85
89
|
}
|
|
86
90
|
|
|
91
|
+
/**
|
|
92
|
+
* A pane that runs a program, declared rather than registered — the manifest
|
|
93
|
+
* twin of `ctx.ui.panes.registerCommand`. `cwd` reads as it does there.
|
|
94
|
+
*/
|
|
95
|
+
export interface PluginCommandPaneSpec {
|
|
96
|
+
id: string
|
|
97
|
+
title?: string
|
|
98
|
+
/** argv, not a shell string. */
|
|
99
|
+
command: string[]
|
|
100
|
+
cwd?: string
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A long-running process the daemon supervises: started when the plugin
|
|
105
|
+
* loads, stopped when it unloads, restarted according to `restart`. A relay,
|
|
106
|
+
* a watcher, a bridge — anything that is not a command that finishes.
|
|
107
|
+
*
|
|
108
|
+
* Runs with the same `AIMUX_*` environment as `commands[]`, so it can call
|
|
109
|
+
* back through the CLI in any language.
|
|
110
|
+
*/
|
|
111
|
+
export interface PluginServiceSpec {
|
|
112
|
+
id: string
|
|
113
|
+
/** argv, not a shell string. */
|
|
114
|
+
command: string[]
|
|
115
|
+
/** Default `on-failure`: a clean exit stays down, a crash comes back. */
|
|
116
|
+
restart?: 'never' | 'on-failure' | 'always'
|
|
117
|
+
}
|
|
118
|
+
|
|
87
119
|
export interface PluginManifest {
|
|
88
120
|
/** Reverse-DNS-ish, `<vendor>.<name>`; the namespace for every registration. */
|
|
89
121
|
id: string
|
|
@@ -100,6 +132,10 @@ export interface PluginManifest {
|
|
|
100
132
|
build?: string[][]
|
|
101
133
|
config?: Record<string, PluginConfigField>
|
|
102
134
|
commands?: PluginCommandSpec[]
|
|
135
|
+
/** Panes that host a program. Applied when the UI half loads — or, with no UI entry, at once. */
|
|
136
|
+
panes?: PluginCommandPaneSpec[]
|
|
137
|
+
/** Processes the daemon keeps alive for the plugin. */
|
|
138
|
+
services?: PluginServiceSpec[]
|
|
103
139
|
/**
|
|
104
140
|
* What the plugin asks the interface for: a place for its widget, a key for
|
|
105
141
|
* its action. Applied by the host when the UI half loads, withdrawn when it
|
package/src/test-context.ts
CHANGED
|
@@ -211,10 +211,12 @@ export function createTestContext(options: TestContextOptions = {}): TestContext
|
|
|
211
211
|
ui: unknown
|
|
212
212
|
actions: unknown
|
|
213
213
|
store: unknown
|
|
214
|
+
commands: unknown
|
|
214
215
|
}
|
|
215
216
|
extended.ui = surface.ui
|
|
216
217
|
extended.actions = surface.actions
|
|
217
218
|
extended.store = surface.store
|
|
219
|
+
extended.commands = surface.commands
|
|
218
220
|
}
|
|
219
221
|
|
|
220
222
|
options.extend?.(ctx)
|
package/src/test-ui.ts
CHANGED
|
@@ -2,9 +2,11 @@ import type { EffectStack } from './effects'
|
|
|
2
2
|
import type { Disposer } from './types'
|
|
3
3
|
import type {
|
|
4
4
|
PluginActionsApi,
|
|
5
|
+
PluginCommandsApi,
|
|
5
6
|
PluginCommitMessage,
|
|
6
7
|
PluginCommitMessageRequest,
|
|
7
8
|
PluginGitStatus,
|
|
9
|
+
PluginNotificationEvent,
|
|
8
10
|
PluginSettingValue,
|
|
9
11
|
PluginStoreApi,
|
|
10
12
|
PluginThemeSnapshot,
|
|
@@ -35,12 +37,16 @@ export interface TestUiRegistrations {
|
|
|
35
37
|
views: string[]
|
|
36
38
|
modals: string[]
|
|
37
39
|
panes: string[]
|
|
40
|
+
/** Command panes — programs the plugin asked to host. */
|
|
41
|
+
commandPanes: string[]
|
|
38
42
|
statusBar: string[]
|
|
39
43
|
statsPages: string[]
|
|
40
44
|
themes: string[]
|
|
41
45
|
settingsSections: number
|
|
42
46
|
/** Whether the plugin claimed the commit-message slot. */
|
|
43
47
|
commitMessageProvider: boolean
|
|
48
|
+
/** Whether the plugin claimed the notification slot. */
|
|
49
|
+
notificationSink: boolean
|
|
44
50
|
actions: string[]
|
|
45
51
|
effects: string[]
|
|
46
52
|
}
|
|
@@ -49,11 +55,18 @@ export interface TestUiSurface {
|
|
|
49
55
|
ui: PluginUiApi
|
|
50
56
|
actions: PluginActionsApi
|
|
51
57
|
store: PluginStoreApi
|
|
58
|
+
commands: PluginCommandsApi
|
|
52
59
|
registrations: TestUiRegistrations
|
|
53
60
|
/** Everything `ctx.ui.toast` was asked to show, newest last. */
|
|
54
61
|
toasts: { level: 'info' | 'success' | 'error'; message: string }[]
|
|
62
|
+
/** Everything `ctx.ui.notifications.notify` raised, newest last. */
|
|
63
|
+
notifications: { title: string; message?: string; level?: string }[]
|
|
55
64
|
/** Panes and views the plugin asked to open or close, in order. */
|
|
56
65
|
opened: string[]
|
|
66
|
+
/** Layout verbs the plugin called, in order — `split:vertical`, `focus:left`, … */
|
|
67
|
+
layoutCalls: string[]
|
|
68
|
+
/** Git writes the plugin asked for, in order — `stage:a.ts`, `commit:title`, … */
|
|
69
|
+
gitWrites: string[]
|
|
57
70
|
/** Drives `ctx.ui.state`: set it, and subscribers hear about it. */
|
|
58
71
|
setState: (next: Partial<PluginUiState>) => void
|
|
59
72
|
/** Drives `ctx.ui.settings.watch` and what `get` answers. */
|
|
@@ -70,6 +83,13 @@ export interface TestUiSurface {
|
|
|
70
83
|
askForCommitMessage: (
|
|
71
84
|
request?: Partial<PluginCommitMessageRequest>
|
|
72
85
|
) => Promise<PluginCommitMessage | null>
|
|
86
|
+
/**
|
|
87
|
+
* Delivers an event to the sink the plugin provided, as aimux would on an
|
|
88
|
+
* agent finishing a turn. Throws when it provided none.
|
|
89
|
+
*/
|
|
90
|
+
deliverNotification: (event: PluginNotificationEvent) => Promise<void>
|
|
91
|
+
/** Drives `ctx.ui.git.diff`. */
|
|
92
|
+
setGitDiff: (path: string, diff: string) => void
|
|
73
93
|
}
|
|
74
94
|
|
|
75
95
|
const EMPTY_STATE: PluginUiState = {
|
|
@@ -99,9 +119,11 @@ const nothing = (): null => null
|
|
|
99
119
|
export function createTestUiSurface(effects: EffectStack): TestUiSurface {
|
|
100
120
|
const registrations: TestUiRegistrations = {
|
|
101
121
|
actions: [],
|
|
122
|
+
commandPanes: [],
|
|
102
123
|
commitMessageProvider: false,
|
|
103
124
|
effects: [],
|
|
104
125
|
modals: [],
|
|
126
|
+
notificationSink: false,
|
|
105
127
|
panes: [],
|
|
106
128
|
settingsSections: 0,
|
|
107
129
|
statsPages: [],
|
|
@@ -111,7 +133,12 @@ export function createTestUiSurface(effects: EffectStack): TestUiSurface {
|
|
|
111
133
|
widgets: [],
|
|
112
134
|
}
|
|
113
135
|
const toasts: TestUiSurface['toasts'] = []
|
|
136
|
+
const notifications: TestUiSurface['notifications'] = []
|
|
114
137
|
const opened: string[] = []
|
|
138
|
+
const layoutCalls: string[] = []
|
|
139
|
+
const gitWrites: string[] = []
|
|
140
|
+
const diffs = new Map<string, string>()
|
|
141
|
+
let notificationSink: ((event: PluginNotificationEvent) => void | Promise<void>) | null = null
|
|
115
142
|
|
|
116
143
|
let state: PluginUiState = EMPTY_STATE
|
|
117
144
|
const stateListeners = new Set<(next: PluginUiState) => void>()
|
|
@@ -150,6 +177,13 @@ export function createTestUiSurface(effects: EffectStack): TestUiSurface {
|
|
|
150
177
|
|
|
151
178
|
const ui: PluginUiApi = {
|
|
152
179
|
git: {
|
|
180
|
+
commit: async (input) => {
|
|
181
|
+
gitWrites.push(`commit:${input.title}`)
|
|
182
|
+
},
|
|
183
|
+
diff: async (path) => diffs.get(path) ?? '',
|
|
184
|
+
discard: async (paths) => {
|
|
185
|
+
gitWrites.push(`discard:${paths.join(',')}`)
|
|
186
|
+
},
|
|
153
187
|
provideCommitMessage: (provider) => {
|
|
154
188
|
commitProvider = provider
|
|
155
189
|
registrations.commitMessageProvider = true
|
|
@@ -160,7 +194,13 @@ export function createTestUiSurface(effects: EffectStack): TestUiSurface {
|
|
|
160
194
|
effects.add(dispose)
|
|
161
195
|
return dispose
|
|
162
196
|
},
|
|
197
|
+
stage: async (paths) => {
|
|
198
|
+
gitWrites.push(`stage:${paths.join(',')}`)
|
|
199
|
+
},
|
|
163
200
|
status: () => git,
|
|
201
|
+
unstage: async (paths) => {
|
|
202
|
+
gitWrites.push(`unstage:${paths.join(',')}`)
|
|
203
|
+
},
|
|
164
204
|
},
|
|
165
205
|
kit: {
|
|
166
206
|
KeyHint: nothing,
|
|
@@ -169,16 +209,42 @@ export function createTestUiSurface(effects: EffectStack): TestUiSurface {
|
|
|
169
209
|
Row: nothing,
|
|
170
210
|
useTheme: () => theme.colors,
|
|
171
211
|
},
|
|
212
|
+
layout: {
|
|
213
|
+
close: (tabId) => layoutCalls.push(`close:${tabId ?? 'active'}`),
|
|
214
|
+
focus: (direction) => layoutCalls.push(`focus:${direction}`),
|
|
215
|
+
panes: () => (state.activeTabId === null ? [] : [state.activeTabId]),
|
|
216
|
+
resize: (delta, axis) => layoutCalls.push(`resize:${axis}:${delta}`),
|
|
217
|
+
split: (direction) => layoutCalls.push(`split:${direction}`),
|
|
218
|
+
swap: (direction) => layoutCalls.push(`swap:${direction}`),
|
|
219
|
+
tree: () => null,
|
|
220
|
+
},
|
|
172
221
|
modals: {
|
|
173
222
|
close: () => opened.push('modal:close'),
|
|
174
223
|
open: (id) => opened.push(`modal:${id}`),
|
|
175
224
|
register: (modal) => record(registrations.modals, modal.id),
|
|
176
225
|
},
|
|
177
226
|
navigate: (screen) => opened.push(`screen:${screen}`),
|
|
227
|
+
notifications: {
|
|
228
|
+
notify: (notification) => {
|
|
229
|
+
notifications.push(notification)
|
|
230
|
+
},
|
|
231
|
+
provide: (sink) => {
|
|
232
|
+
notificationSink = sink
|
|
233
|
+
registrations.notificationSink = true
|
|
234
|
+
const dispose = (): void => {
|
|
235
|
+
if (notificationSink === sink) notificationSink = null
|
|
236
|
+
registrations.notificationSink = false
|
|
237
|
+
}
|
|
238
|
+
effects.add(dispose)
|
|
239
|
+
return dispose
|
|
240
|
+
},
|
|
241
|
+
},
|
|
178
242
|
panes: {
|
|
179
243
|
close: (id) => opened.push(`pane:close:${id}`),
|
|
180
244
|
open: (id) => opened.push(`pane:${id}`),
|
|
245
|
+
openCommandPanes: () => [],
|
|
181
246
|
register: (pane) => record(registrations.panes, pane.id),
|
|
247
|
+
registerCommand: (pane) => record(registrations.commandPanes, pane.id),
|
|
182
248
|
},
|
|
183
249
|
settings: {
|
|
184
250
|
get: (id) => settings.get(id),
|
|
@@ -236,6 +302,17 @@ export function createTestUiSurface(effects: EffectStack): TestUiSurface {
|
|
|
236
302
|
register: (verb) => record(registrations.actions, verb),
|
|
237
303
|
}
|
|
238
304
|
|
|
305
|
+
const commands: PluginCommandsApi = {
|
|
306
|
+
list: () =>
|
|
307
|
+
registrations.actions.map((verb) => ({
|
|
308
|
+
id: verb,
|
|
309
|
+
kind: 'action' as const,
|
|
310
|
+
pluginId: 'test',
|
|
311
|
+
title: verb,
|
|
312
|
+
})),
|
|
313
|
+
run: () => false,
|
|
314
|
+
}
|
|
315
|
+
|
|
239
316
|
const store: PluginStoreApi = {
|
|
240
317
|
dispatch: () => {
|
|
241
318
|
/* a slice reducer is the plugin's; a bare context has none to run */
|
|
@@ -260,8 +337,19 @@ export function createTestUiSurface(effects: EffectStack): TestUiSurface {
|
|
|
260
337
|
if (commitProvider === null) throw new Error('the plugin registered no commit provider')
|
|
261
338
|
return commitProvider({ ...EMPTY_REQUEST, ...request }, new AbortController().signal)
|
|
262
339
|
},
|
|
340
|
+
commands,
|
|
341
|
+
deliverNotification: async (event) => {
|
|
342
|
+
if (notificationSink === null) throw new Error('the plugin provided no notification sink')
|
|
343
|
+
await notificationSink(event)
|
|
344
|
+
},
|
|
345
|
+
gitWrites,
|
|
346
|
+
layoutCalls,
|
|
347
|
+
notifications,
|
|
263
348
|
opened,
|
|
264
349
|
registrations,
|
|
350
|
+
setGitDiff: (path, diff) => {
|
|
351
|
+
diffs.set(path, diff)
|
|
352
|
+
},
|
|
265
353
|
setGitStatus: (status) => {
|
|
266
354
|
git = status
|
|
267
355
|
},
|
package/src/types.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
PluginWorkspacesApi,
|
|
9
9
|
} from './daemon-api'
|
|
10
10
|
import type { PluginHost, PluginManifest } from './manifest'
|
|
11
|
-
import type { PluginActionsApi, PluginStoreApi, PluginUiApi } from './ui'
|
|
11
|
+
import type { PluginActionsApi, PluginCommandsApi, PluginStoreApi, PluginUiApi } from './ui'
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Everything a plugin registers hands back one of these. The kernel calls
|
|
@@ -122,6 +122,7 @@ export interface UiPluginContext<Slice = unknown> extends PluginContext {
|
|
|
122
122
|
readonly ui: PluginUiApi
|
|
123
123
|
readonly actions: PluginActionsApi
|
|
124
124
|
readonly store: PluginStoreApi<Slice>
|
|
125
|
+
readonly commands: PluginCommandsApi
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
/**
|
package/src/ui.ts
CHANGED
|
@@ -159,6 +159,32 @@ export interface PluginPane {
|
|
|
159
159
|
render: () => PluginNode
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
/**
|
|
163
|
+
* A pane that hosts a *program* rather than React: lazygit, yazi, a Rust TUI
|
|
164
|
+
* the plugin ships. Same story the daemon's `commands[]` tells — a plugin in
|
|
165
|
+
* any language — applied to the interface.
|
|
166
|
+
*
|
|
167
|
+
* Opening one spawns the argv in a terminal pane aimux owns on the plugin's
|
|
168
|
+
* behalf: it is a real PTY tab, so it takes the keyboard like any terminal
|
|
169
|
+
* does. The pane outlives a reload of the plugin (a re-registration under the
|
|
170
|
+
* same id adopts it) and dies with the plugin (unlink, uninstall, disable). A
|
|
171
|
+
* program that exits leaves a pane that says so, and `Ctrl+r` restarts it.
|
|
172
|
+
*/
|
|
173
|
+
export interface PluginCommandPane {
|
|
174
|
+
/** Unqualified; the host prefixes the plugin id. */
|
|
175
|
+
id: string
|
|
176
|
+
/** Drawn in the pane's border and in the tab strip. */
|
|
177
|
+
title: string
|
|
178
|
+
/** argv, not a shell string — no quoting rules to get wrong. */
|
|
179
|
+
command: string[]
|
|
180
|
+
/**
|
|
181
|
+
* Where to run it. `workspace` (the default) is the active workspace's
|
|
182
|
+
* directory, `project` the project's, `plugin` the plugin's own root; any
|
|
183
|
+
* other value is taken as an absolute path.
|
|
184
|
+
*/
|
|
185
|
+
cwd?: 'workspace' | 'project' | 'plugin' | (string & {})
|
|
186
|
+
}
|
|
187
|
+
|
|
162
188
|
export interface PluginPanesApi {
|
|
163
189
|
/**
|
|
164
190
|
* Declares a pane: a leaf in the layout tree that draws something other than
|
|
@@ -169,6 +195,12 @@ export interface PluginPanesApi {
|
|
|
169
195
|
* Registering does not put it on screen; `open` does.
|
|
170
196
|
*/
|
|
171
197
|
register: (pane: PluginPane) => Disposer
|
|
198
|
+
/**
|
|
199
|
+
* Declares a pane that runs a program. The manifest's `panes[]` block is the
|
|
200
|
+
* same declaration without a line of TypeScript. `open` and `close` take the
|
|
201
|
+
* same unqualified id either way.
|
|
202
|
+
*/
|
|
203
|
+
registerCommand: (pane: PluginCommandPane) => Disposer
|
|
172
204
|
/**
|
|
173
205
|
* Splits the pane the user is in and puts this one beside it. Takes the
|
|
174
206
|
* unqualified id. Opening one that is already open does nothing: the id is
|
|
@@ -181,8 +213,112 @@ export interface PluginPanesApi {
|
|
|
181
213
|
* in its own mode — `plugin.pane.<pluginId>.<id>`.
|
|
182
214
|
*/
|
|
183
215
|
open: (id: string, direction?: 'horizontal' | 'vertical') => void
|
|
184
|
-
/**
|
|
216
|
+
/**
|
|
217
|
+
* Takes it off screen. The layout collapses as it would for a closed tab.
|
|
218
|
+
* For a command pane this also kills the program.
|
|
219
|
+
*/
|
|
185
220
|
close: (id: string) => void
|
|
221
|
+
/**
|
|
222
|
+
* Which command panes are on screen right now, by unqualified id. A React
|
|
223
|
+
* pane is either registered or not; a command pane also has a process, and
|
|
224
|
+
* a plugin that wants to toggle one needs to know.
|
|
225
|
+
*/
|
|
226
|
+
openCommandPanes: () => string[]
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The layout of one group, as a plugin reads it. Same shape aimux persists. */
|
|
230
|
+
export type PluginLayoutNode =
|
|
231
|
+
| { type: 'leaf'; id: string; kind: 'tab' | 'plugin' }
|
|
232
|
+
| {
|
|
233
|
+
type: 'split'
|
|
234
|
+
direction: 'horizontal' | 'vertical'
|
|
235
|
+
ratio: number
|
|
236
|
+
first: PluginLayoutNode
|
|
237
|
+
second: PluginLayoutNode
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export type PluginPaneDirection = 'left' | 'right' | 'up' | 'down'
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The layout as an API rather than as keys. Every verb here is one the
|
|
244
|
+
* keyboard already has, dispatched through the same reducer, so a plugin
|
|
245
|
+
* cannot reach a layout the user could not have made by hand.
|
|
246
|
+
*
|
|
247
|
+
* Everything acts on the pane holding the keyboard unless a `tabId` says
|
|
248
|
+
* otherwise; nothing here throws for a pane that cannot move, because a
|
|
249
|
+
* key press does not either.
|
|
250
|
+
*/
|
|
251
|
+
export interface PluginLayoutApi {
|
|
252
|
+
/** Splits the active pane and puts a new terminal beside it, same assistant. */
|
|
253
|
+
split: (direction: 'horizontal' | 'vertical') => void
|
|
254
|
+
/** Moves the keyboard to the neighbouring pane. */
|
|
255
|
+
focus: (direction: PluginPaneDirection) => void
|
|
256
|
+
/** Exchanges the active pane with its neighbour in that direction. */
|
|
257
|
+
swap: (direction: PluginPaneDirection) => void
|
|
258
|
+
/** Nudges the split the active pane sits in. `delta` is in steps, ±1 typically. */
|
|
259
|
+
resize: (delta: number, axis: 'horizontal' | 'vertical') => void
|
|
260
|
+
/** Closes a pane — the active one when no id is given. Kills its process. */
|
|
261
|
+
close: (tabId?: string) => void
|
|
262
|
+
/** The active group's tree, or null when the active tab is not split. */
|
|
263
|
+
tree: () => PluginLayoutNode | null
|
|
264
|
+
/** Every pane id in the active group, in draw order. One id when unsplit. */
|
|
265
|
+
panes: () => string[]
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** What a notification says. */
|
|
269
|
+
export interface PluginNotification {
|
|
270
|
+
title: string
|
|
271
|
+
message?: string
|
|
272
|
+
level?: 'info' | 'success' | 'warning' | 'error'
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* A notification aimux is about to make — its own (`waiting-input`,
|
|
277
|
+
* `turn-complete`) or one a plugin raised (`custom`). What a sink receives.
|
|
278
|
+
*/
|
|
279
|
+
export interface PluginNotificationEvent extends PluginNotification {
|
|
280
|
+
kind: 'waiting-input' | 'turn-complete' | 'custom'
|
|
281
|
+
tabId?: string
|
|
282
|
+
workspaceId?: string
|
|
283
|
+
/** The plugin that raised it; absent on aimux's own. */
|
|
284
|
+
pluginId?: string
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export interface PluginNotificationsApi {
|
|
288
|
+
/** Shows a toast — or hands it to the sink, when a plugin provides one. */
|
|
289
|
+
notify: (notification: PluginNotification) => void
|
|
290
|
+
/**
|
|
291
|
+
* Replaces aimux's own notifications: the sound on an agent asking a
|
|
292
|
+
* question or finishing a turn stops playing, and every event — aimux's
|
|
293
|
+
* and other plugins' — lands here instead. A ntfy or Telegram plugin
|
|
294
|
+
* *replaces* the native toast rather than doubling it.
|
|
295
|
+
*
|
|
296
|
+
* One plugin at a time, on the `provideCommitMessage` model: the second to
|
|
297
|
+
* ask is refused and told so in its log.
|
|
298
|
+
*/
|
|
299
|
+
provide: (sink: (event: PluginNotificationEvent) => void | Promise<void>) => Disposer
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** One entry of what `ctx.commands.list()` enumerates. */
|
|
303
|
+
export interface PluginCommandEntry {
|
|
304
|
+
/** `action` runs in the UI, `exec` is a manifest `commands[]` subprocess, `cli` an `aimux <group> <verb>`. */
|
|
305
|
+
kind: 'action' | 'exec' | 'cli'
|
|
306
|
+
/** The name to run it by: the qualified action, `<pluginId> <commandId>`, or `<group> <verb>`. */
|
|
307
|
+
id: string
|
|
308
|
+
pluginId: string
|
|
309
|
+
title: string
|
|
310
|
+
description?: string
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Everything runnable that plugins have contributed, in one list. Without it
|
|
315
|
+
* a command palette written by a third party has nothing to show; with it,
|
|
316
|
+
* `run` fires an action the way its key would.
|
|
317
|
+
*/
|
|
318
|
+
export interface PluginCommandsApi {
|
|
319
|
+
list: () => PluginCommandEntry[]
|
|
320
|
+
/** Runs an `action` entry by its qualified id. Returns false when nothing answered. */
|
|
321
|
+
run: (id: string) => boolean
|
|
186
322
|
}
|
|
187
323
|
|
|
188
324
|
export interface PluginStatusBarSegment {
|
|
@@ -329,9 +465,33 @@ export interface PluginCommitMessage {
|
|
|
329
465
|
body?: string
|
|
330
466
|
}
|
|
331
467
|
|
|
468
|
+
export interface PluginGitCommitInput {
|
|
469
|
+
title: string
|
|
470
|
+
body?: string
|
|
471
|
+
}
|
|
472
|
+
|
|
332
473
|
export interface PluginGitApi {
|
|
333
474
|
/** The panel's last refresh. */
|
|
334
475
|
status: () => PluginGitStatus
|
|
476
|
+
/**
|
|
477
|
+
* The diff of one file, unified. `staged` reads the index against HEAD; the
|
|
478
|
+
* default reads the working tree against the index, which is what the
|
|
479
|
+
* panel shows for an unstaged file. Empty for an untracked file — git has
|
|
480
|
+
* nothing to compare it with — so read the file itself in that case.
|
|
481
|
+
*/
|
|
482
|
+
diff: (path: string, options?: { staged?: boolean }) => Promise<string>
|
|
483
|
+
/** `git add -- <paths>`. Rejects with git's own message on failure. */
|
|
484
|
+
stage: (paths: readonly string[]) => Promise<void>
|
|
485
|
+
/** `git restore --staged -- <paths>`. */
|
|
486
|
+
unstage: (paths: readonly string[]) => Promise<void>
|
|
487
|
+
/**
|
|
488
|
+
* Throws away working-tree changes: `git checkout --` for a tracked file,
|
|
489
|
+
* deletion for an untracked one. There is no undo, which is why it takes
|
|
490
|
+
* paths rather than "everything".
|
|
491
|
+
*/
|
|
492
|
+
discard: (paths: readonly string[]) => Promise<void>
|
|
493
|
+
/** Commits what is staged. Rejects when nothing is, with git's own words. */
|
|
494
|
+
commit: (input: PluginGitCommitInput) => Promise<void>
|
|
335
495
|
/**
|
|
336
496
|
* Answers "what should this commit say", replacing the headless model call
|
|
337
497
|
* aimux would otherwise make. Return `null` to decline this one — aimux falls
|
|
@@ -366,6 +526,8 @@ export interface PluginUiApi {
|
|
|
366
526
|
themes: PluginThemesApi
|
|
367
527
|
toast: PluginToastApi
|
|
368
528
|
panes: PluginPanesApi
|
|
529
|
+
layout: PluginLayoutApi
|
|
530
|
+
notifications: PluginNotificationsApi
|
|
369
531
|
state: PluginStateApi
|
|
370
532
|
stats: PluginStatsApi
|
|
371
533
|
statusBar: PluginStatusBarApi
|
|
@@ -376,13 +538,22 @@ export interface PluginUiApi {
|
|
|
376
538
|
* Keyboard actions and their effects. Registered by unqualified verb; a user's
|
|
377
539
|
* keymap binds the qualified name with `k.plugin('acme.thing.open')`.
|
|
378
540
|
*/
|
|
541
|
+
/** What a palette shows for an action. Optional: an action without one lists under its verb. */
|
|
542
|
+
export interface PluginActionMeta {
|
|
543
|
+
title?: string
|
|
544
|
+
description?: string
|
|
545
|
+
}
|
|
546
|
+
|
|
379
547
|
export interface PluginActionsApi {
|
|
380
548
|
/**
|
|
381
549
|
* The action a key produces. Receives the mode context and returns a
|
|
382
550
|
* `KeyResult` — the same value a built-in binding produces — or null for
|
|
383
551
|
* "not handled here".
|
|
552
|
+
*
|
|
553
|
+
* `meta` gives it a title. An action with one is something a palette can
|
|
554
|
+
* list and a user can find; without it the verb is all anyone sees.
|
|
384
555
|
*/
|
|
385
|
-
register: (verb: string, handler: (ctx: unknown) => unknown) => Disposer
|
|
556
|
+
register: (verb: string, handler: (ctx: unknown) => unknown, meta?: PluginActionMeta) => Disposer
|
|
386
557
|
/**
|
|
387
558
|
* The side of a binding that is allowed to do things: spawn a tab, write a
|
|
388
559
|
* file, call out. Reached from an action's `KeyResult` as a `plugin-effect`.
|