@stina/extension-api 0.7.1 → 0.8.1

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/runtime.ts CHANGED
@@ -13,11 +13,21 @@ import type {
13
13
  SettingsAPI,
14
14
  ProvidersAPI,
15
15
  ToolsAPI,
16
+ ActionsAPI,
17
+ EventsAPI,
18
+ SchedulerAPI,
19
+ SchedulerJobRequest,
20
+ SchedulerFirePayload,
21
+ UserAPI,
22
+ UserProfile,
23
+ ChatAPI,
24
+ ChatInstructionMessage,
16
25
  DatabaseAPI,
17
26
  StorageAPI,
18
27
  LogAPI,
19
28
  AIProvider,
20
29
  Tool,
30
+ Action,
21
31
  ChatMessage,
22
32
  ChatOptions,
23
33
  GetModelsOptions,
@@ -81,7 +91,9 @@ let extensionContext: ExtensionContext | null = null
81
91
  const pendingRequests = new Map<string, PendingRequest>()
82
92
  const registeredProviders = new Map<string, AIProvider>()
83
93
  const registeredTools = new Map<string, Tool>()
94
+ const registeredActions = new Map<string, Action>()
84
95
  const settingsCallbacks: Array<(key: string, value: unknown) => void> = []
96
+ const schedulerCallbacks: Array<(payload: SchedulerFirePayload) => void> = []
85
97
 
86
98
  const REQUEST_TIMEOUT = 30000 // 30 seconds
87
99
 
@@ -136,6 +148,10 @@ async function handleHostMessage(message: HostToWorkerMessage): Promise<void> {
136
148
  handleSettingsChanged(message.payload.key, message.payload.value)
137
149
  break
138
150
 
151
+ case 'scheduler-fire':
152
+ handleSchedulerFire(message.payload)
153
+ break
154
+
139
155
  case 'provider-chat-request':
140
156
  await handleProviderChatRequest(message.id, message.payload)
141
157
  break
@@ -148,6 +164,10 @@ async function handleHostMessage(message: HostToWorkerMessage): Promise<void> {
148
164
  await handleToolExecuteRequest(message.id, message.payload)
149
165
  break
150
166
 
167
+ case 'action-execute-request':
168
+ await handleActionExecuteRequest(message.id, message.payload)
169
+ break
170
+
151
171
  case 'response':
152
172
  handleResponse(message.payload)
153
173
  break
@@ -218,7 +238,9 @@ async function handleDeactivate(): Promise<void> {
218
238
  extensionContext = null
219
239
  registeredProviders.clear()
220
240
  registeredTools.clear()
241
+ registeredActions.clear()
221
242
  settingsCallbacks.length = 0
243
+ schedulerCallbacks.length = 0
222
244
  }
223
245
  }
224
246
 
@@ -232,6 +254,16 @@ function handleSettingsChanged(key: string, value: unknown): void {
232
254
  }
233
255
  }
234
256
 
257
+ function handleSchedulerFire(payload: SchedulerFirePayload): void {
258
+ for (const callback of schedulerCallbacks) {
259
+ try {
260
+ callback(payload)
261
+ } catch (error) {
262
+ console.error('Error in scheduler callback:', error)
263
+ }
264
+ }
265
+ }
266
+
235
267
  // ============================================================================
236
268
  // Provider / Tool Requests
237
269
  // ============================================================================
@@ -373,6 +405,45 @@ async function handleToolExecuteRequest(
373
405
  }
374
406
  }
375
407
 
408
+ async function handleActionExecuteRequest(
409
+ requestId: string,
410
+ payload: { actionId: string; params: Record<string, unknown> }
411
+ ): Promise<void> {
412
+ const action = registeredActions.get(payload.actionId)
413
+ if (!action) {
414
+ postMessage({
415
+ type: 'action-execute-response',
416
+ payload: {
417
+ requestId,
418
+ result: { success: false, error: `Action ${payload.actionId} not found` },
419
+ error: `Action ${payload.actionId} not found`,
420
+ },
421
+ })
422
+ return
423
+ }
424
+
425
+ try {
426
+ const result = await action.execute(payload.params)
427
+ postMessage({
428
+ type: 'action-execute-response',
429
+ payload: {
430
+ requestId,
431
+ result,
432
+ },
433
+ })
434
+ } catch (error) {
435
+ const errorMessage = error instanceof Error ? error.message : String(error)
436
+ postMessage({
437
+ type: 'action-execute-response',
438
+ payload: {
439
+ requestId,
440
+ result: { success: false, error: errorMessage },
441
+ error: errorMessage,
442
+ },
443
+ })
444
+ }
445
+ }
446
+
376
447
  // ============================================================================
377
448
  // Context Building
378
449
  // ============================================================================
@@ -490,6 +561,79 @@ function buildContext(
490
561
  ;(context as { tools: ToolsAPI }).tools = toolsApi
491
562
  }
492
563
 
564
+ // Add actions API if permitted
565
+ if (hasPermission('actions.register')) {
566
+ const actionsApi: ActionsAPI = {
567
+ register(action: Action): Disposable {
568
+ registeredActions.set(action.id, action)
569
+ postMessage({
570
+ type: 'action-registered',
571
+ payload: {
572
+ id: action.id,
573
+ },
574
+ })
575
+ return {
576
+ dispose: () => {
577
+ registeredActions.delete(action.id)
578
+ },
579
+ }
580
+ },
581
+ }
582
+ ;(context as { actions: ActionsAPI }).actions = actionsApi
583
+ }
584
+
585
+ // Add events API if permitted
586
+ if (hasPermission('events.emit')) {
587
+ const eventsApi: EventsAPI = {
588
+ async emit(name: string, payload?: Record<string, unknown>): Promise<void> {
589
+ await sendRequest<void>('events.emit', { name, payload })
590
+ },
591
+ }
592
+ ;(context as { events: EventsAPI }).events = eventsApi
593
+ }
594
+
595
+ // Add scheduler API if permitted
596
+ if (hasPermission('scheduler.register')) {
597
+ const schedulerApi: SchedulerAPI = {
598
+ async schedule(job: SchedulerJobRequest): Promise<void> {
599
+ await sendRequest<void>('scheduler.schedule', { job })
600
+ },
601
+ async cancel(jobId: string): Promise<void> {
602
+ await sendRequest<void>('scheduler.cancel', { jobId })
603
+ },
604
+ onFire(callback: (payload: SchedulerFirePayload) => void): Disposable {
605
+ schedulerCallbacks.push(callback)
606
+ return {
607
+ dispose: () => {
608
+ const index = schedulerCallbacks.indexOf(callback)
609
+ if (index >= 0) schedulerCallbacks.splice(index, 1)
610
+ },
611
+ }
612
+ },
613
+ }
614
+ ;(context as { scheduler: SchedulerAPI }).scheduler = schedulerApi
615
+ }
616
+
617
+ // Add user profile API if permitted
618
+ if (hasPermission('user.profile.read')) {
619
+ const userApi: UserAPI = {
620
+ async getProfile(): Promise<UserProfile> {
621
+ return sendRequest<UserProfile>('user.getProfile', {})
622
+ },
623
+ }
624
+ ;(context as { user: UserAPI }).user = userApi
625
+ }
626
+
627
+ // Add chat API if permitted
628
+ if (hasPermission('chat.message.write')) {
629
+ const chatApi: ChatAPI = {
630
+ async appendInstruction(message: ChatInstructionMessage): Promise<void> {
631
+ await sendRequest<void>('chat.appendInstruction', message)
632
+ },
633
+ }
634
+ ;(context as { chat: ChatAPI }).chat = chatApi
635
+ }
636
+
493
637
  // Add database API if permitted
494
638
  if (hasPermission('database.own')) {
495
639
  const databaseApi: DatabaseAPI = {
@@ -556,6 +700,8 @@ export type {
556
700
  ToolDefinition,
557
701
  ToolResult,
558
702
  ToolCall,
703
+ Action,
704
+ ActionResult,
559
705
  ModelInfo,
560
706
  ChatMessage,
561
707
  ChatOptions,
@@ -0,0 +1,236 @@
1
+ /** Base interface for dynamically rendered extension components. */
2
+ export interface ExtensionComponentData {
3
+ component: string
4
+ [key: string]: unknown
5
+ }
6
+
7
+ // =============================================================================
8
+ // Iteration & Children
9
+ // =============================================================================
10
+
11
+ /**
12
+ * Iterator for rendering a list of components from data.
13
+ * Used with layout components like VerticalStack, HorizontalStack, Grid.
14
+ *
15
+ * @example
16
+ * ```json
17
+ * {
18
+ * "each": "$todos",
19
+ * "as": "todo",
20
+ * "items": [{ "component": "Label", "text": "$todo.title" }]
21
+ * }
22
+ * ```
23
+ */
24
+ export interface ExtensionComponentIterator {
25
+ /** Data source to iterate over. Use "$name" for dynamic reference or inline array. */
26
+ each: string | unknown[]
27
+ /** Variable name for current item in scope */
28
+ as: string
29
+ /** Components to render for each item */
30
+ items: ExtensionComponentData[]
31
+ }
32
+
33
+ /**
34
+ * Children can be either a static array of components or an iterator.
35
+ */
36
+ export type ExtensionComponentChildren =
37
+ | ExtensionComponentData[]
38
+ | ExtensionComponentIterator
39
+
40
+ // =============================================================================
41
+ // Actions
42
+ // =============================================================================
43
+
44
+ /**
45
+ * Action call with parameters.
46
+ * Used for component events like onClick, onChange, etc.
47
+ *
48
+ * @example
49
+ * ```json
50
+ * {
51
+ * "action": "deleteTodo",
52
+ * "params": { "todoId": "$todo.id" }
53
+ * }
54
+ * ```
55
+ */
56
+ export interface ExtensionActionCall {
57
+ /** Name of the registered action */
58
+ action: string
59
+ /** Parameters to pass. Values starting with "$" are resolved from scope. */
60
+ params?: Record<string, unknown>
61
+ }
62
+
63
+ /**
64
+ * Action reference - can be a simple string (action name) or full action call.
65
+ */
66
+ export type ExtensionActionRef = string | ExtensionActionCall
67
+
68
+ // =============================================================================
69
+ // Data Sources & Panel Definition
70
+ // =============================================================================
71
+
72
+ /**
73
+ * Data source definition for fetching data via an action.
74
+ *
75
+ * @example
76
+ * ```json
77
+ * {
78
+ * "action": "getProjects",
79
+ * "params": { "includeArchived": false },
80
+ * "refreshOn": ["project.changed"]
81
+ * }
82
+ * ```
83
+ */
84
+ export interface ExtensionDataSource {
85
+ /** Action to call for fetching data */
86
+ action: string
87
+ /** Parameters to pass to the action */
88
+ params?: Record<string, unknown>
89
+ /** Event names that should trigger a refresh of this data */
90
+ refreshOn?: string[]
91
+ }
92
+
93
+ /**
94
+ * Panel definition for extension-contributed panels.
95
+ *
96
+ * @example
97
+ * ```json
98
+ * {
99
+ * "data": {
100
+ * "projects": { "action": "getProjectsWithTodos", "refreshOn": ["todo.changed"] }
101
+ * },
102
+ * "content": {
103
+ * "component": "VerticalStack",
104
+ * "children": { "each": "$projects", "as": "project", "items": [...] }
105
+ * }
106
+ * }
107
+ * ```
108
+ */
109
+ export interface ExtensionPanelDefinition {
110
+ /** Data sources available in the panel. Keys become variable names (e.g., "$projects"). */
111
+ data?: Record<string, ExtensionDataSource>
112
+ /** Root component to render */
113
+ content: ExtensionComponentData
114
+ }
115
+
116
+ /** The extension API properties for the Header component. */
117
+ export interface HeaderProps extends ExtensionComponentData {
118
+ component: 'Header'
119
+ level: number
120
+ title: string
121
+ description?: string | string[]
122
+ icon?: string
123
+ }
124
+
125
+ /** The extension API properties for the Label component. */
126
+ export interface LabelProps extends ExtensionComponentData {
127
+ component: 'Label'
128
+ text: string
129
+ }
130
+
131
+ /** The extension API properties for the paragraph component. */
132
+ export interface ParagraphProps extends ExtensionComponentData {
133
+ component: 'Paragraph'
134
+ text: string
135
+ }
136
+
137
+ /** The extension API properties for the Button component. */
138
+ export interface ButtonProps extends ExtensionComponentData {
139
+ component: 'Button'
140
+ text: string
141
+ onClickAction: ExtensionActionRef
142
+ }
143
+
144
+ /** The extension API properties for the TextInput component. */
145
+ export interface TextInputProps extends ExtensionComponentData {
146
+ component: 'TextInput'
147
+ label: string
148
+ placeholder?: string
149
+ value?: string
150
+ onChangeAction: ExtensionActionRef
151
+ }
152
+
153
+ /** The extension API properties for the Select component. */
154
+ export interface SelectProps extends ExtensionComponentData {
155
+ component: 'Select'
156
+ label: string
157
+ options: Array<{ label: string; value: string }>
158
+ selectedValue?: string
159
+ onChangeAction: ExtensionActionRef
160
+ }
161
+
162
+ /** The extension API properties for the VerticalStack component. */
163
+ export interface VerticalStackProps extends ExtensionComponentData {
164
+ component: 'VerticalStack'
165
+ gap?: number
166
+ children: ExtensionComponentChildren
167
+ }
168
+
169
+ /** The extension API properties for the HorizontalStack component. */
170
+ export interface HorizontalStackProps extends ExtensionComponentData {
171
+ component: 'HorizontalStack'
172
+ gap?: number
173
+ children: ExtensionComponentChildren
174
+ }
175
+
176
+ /** The extension API properties for the Grid component. */
177
+ export interface GridProps extends ExtensionComponentData {
178
+ component: 'Grid'
179
+ columns: number
180
+ gap?: number
181
+ children: ExtensionComponentChildren
182
+ }
183
+
184
+ /** The extension API properties for the Divider component. */
185
+ export interface DividerProps extends ExtensionComponentData {
186
+ component: 'Divider'
187
+ }
188
+
189
+ /** The extension API properties for the Icon component. */
190
+ export interface IconProps extends ExtensionComponentData {
191
+ component: 'Icon'
192
+ name: string
193
+ title?: string
194
+ }
195
+
196
+ /** Button type for IconButton. */
197
+ export type IconButtonType = 'normal' | 'primary' | 'danger' | 'accent'
198
+
199
+ /** The extension API properties for the IconButton component. */
200
+ export interface IconButtonProps extends ExtensionComponentData {
201
+ component: 'IconButton'
202
+ icon: string
203
+ tooltip: string
204
+ active?: boolean
205
+ disabled?: boolean
206
+ type?: IconButtonType
207
+ onClickAction: ExtensionActionRef
208
+ }
209
+
210
+ /** Action button definition for Panel component. */
211
+ export interface PanelAction {
212
+ icon: string
213
+ tooltip: string
214
+ action: ExtensionActionRef
215
+ type?: IconButtonType
216
+ }
217
+
218
+ /** The extension API properties for the Panel component. */
219
+ export interface PanelProps extends ExtensionComponentData {
220
+ component: 'Panel'
221
+ title: string
222
+ description?: string | string[]
223
+ icon?: string
224
+ actions?: PanelAction[]
225
+ content?: ExtensionComponentData
226
+ }
227
+
228
+ /** The extension API properties for the Toggle component. */
229
+ export interface ToggleProps extends ExtensionComponentData {
230
+ component: 'Toggle'
231
+ label?: string
232
+ description?: string
233
+ checked?: boolean
234
+ disabled?: boolean
235
+ onChangeAction: ExtensionActionRef
236
+ }