@stina/extension-api 0.36.0 → 0.44.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/dist/{chunk-K53YNG2W.js → chunk-ZB7GJUPS.js} +1 -1
- package/dist/chunk-ZB7GJUPS.js.map +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -6
- package/dist/index.d.ts +34 -6
- package/dist/index.js +1 -1
- package/dist/runtime.cjs +26 -5
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.cts +2 -2
- package/dist/runtime.d.ts +2 -2
- package/dist/runtime.js +27 -6
- package/dist/runtime.js.map +1 -1
- package/dist/schemas/index.cjs +4 -0
- package/dist/schemas/index.cjs.map +1 -1
- package/dist/schemas/index.d.cts +3 -0
- package/dist/schemas/index.d.ts +3 -0
- package/dist/schemas/index.js +4 -0
- package/dist/schemas/index.js.map +1 -1
- package/dist/{types.tools-BYgcVNP4.d.cts → types.tools-BZQrEs91.d.cts} +227 -3
- package/dist/{types.tools-BYgcVNP4.d.ts → types.tools-BZQrEs91.d.ts} +227 -3
- package/package.json +1 -1
- package/schema/extension-manifest.schema.json +4 -0
- package/src/background.test.ts +6 -5
- package/src/background.ts +17 -10
- package/src/index.ts +7 -0
- package/src/messages.ts +29 -1
- package/src/runtime.ts +40 -0
- package/src/schemas/manifest.schema.ts +4 -0
- package/src/types.context.ts +105 -2
- package/src/types.contributions.ts +11 -0
- package/src/types.manifest.ts +7 -0
- package/src/types.provider.ts +112 -0
- package/src/types.ts +5 -0
- package/dist/chunk-K53YNG2W.js.map +0 -1
|
@@ -633,6 +633,17 @@ interface ProviderDefinition {
|
|
|
633
633
|
* to call extension actions (e.g. OAuth, "Test connection").
|
|
634
634
|
*/
|
|
635
635
|
configView?: ProviderConfigView;
|
|
636
|
+
/**
|
|
637
|
+
* Extra settings shown only for models that report
|
|
638
|
+
* `capabilities.voiceDuplex` — voice selection, transcription model, and
|
|
639
|
+
* whatever else is specific to this provider's realtime backend.
|
|
640
|
+
*
|
|
641
|
+
* Same DSL and same `$settings.<key>` binding as {@link configView}; the host
|
|
642
|
+
* renders it in its own section of the model editor and stores the values in
|
|
643
|
+
* the same settingsOverride. Keep general provider settings in `configView`
|
|
644
|
+
* so they stay visible for models without voice.
|
|
645
|
+
*/
|
|
646
|
+
voiceConfigView?: ProviderConfigView;
|
|
636
647
|
}
|
|
637
648
|
/**
|
|
638
649
|
* Component-tree-based configuration view for a provider.
|
|
@@ -741,6 +752,21 @@ interface AIProvider {
|
|
|
741
752
|
* Optional: Generate embeddings
|
|
742
753
|
*/
|
|
743
754
|
embed?(texts: string[]): Promise<number[][]>;
|
|
755
|
+
/**
|
|
756
|
+
* Optional: Open a duplex voice session for a model that reports
|
|
757
|
+
* `capabilities.voiceDuplex`.
|
|
758
|
+
*
|
|
759
|
+
* The provider is responsible for authenticating with its backend and
|
|
760
|
+
* returning everything the client needs to connect. Audio flows directly
|
|
761
|
+
* between the client and the provider — it never passes through Stina — but
|
|
762
|
+
* the session is configured here, on the server, so the instructions and
|
|
763
|
+
* tools cannot be tampered with client-side.
|
|
764
|
+
*
|
|
765
|
+
* Implementations must not put long-lived credentials in the returned
|
|
766
|
+
* descriptor. Either complete the handshake here (`webrtc`) or mint a
|
|
767
|
+
* short-lived secret (`websocket`).
|
|
768
|
+
*/
|
|
769
|
+
createVoiceSession?(options: VoiceSessionOptions): Promise<VoiceSessionDescriptor>;
|
|
744
770
|
}
|
|
745
771
|
/**
|
|
746
772
|
* Model information
|
|
@@ -754,6 +780,104 @@ interface ModelInfo {
|
|
|
754
780
|
description?: string;
|
|
755
781
|
/** Context window size */
|
|
756
782
|
contextLength?: number;
|
|
783
|
+
/** What this model can do beyond plain text chat */
|
|
784
|
+
capabilities?: ModelCapabilities;
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Optional model capabilities.
|
|
788
|
+
*
|
|
789
|
+
* Absent or `false` means "not supported" — a provider that says nothing keeps
|
|
790
|
+
* behaving exactly as before.
|
|
791
|
+
*/
|
|
792
|
+
interface ModelCapabilities {
|
|
793
|
+
/**
|
|
794
|
+
* The model can hold a real-time two-way voice conversation, with the user
|
|
795
|
+
* and the model able to speak at the same time.
|
|
796
|
+
*
|
|
797
|
+
* Report this per model *and* per auth mode: the same provider may support
|
|
798
|
+
* voice with one kind of credential and not another.
|
|
799
|
+
*/
|
|
800
|
+
voiceDuplex?: boolean;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* How the client wants to connect to the voice session.
|
|
804
|
+
*
|
|
805
|
+
* Clients ask for the transport they can actually implement — browsers do
|
|
806
|
+
* WebRTC, a native app may prefer a plain socket — and providers implement the
|
|
807
|
+
* ones their backend offers. A provider that is handed a transport it does not
|
|
808
|
+
* support should throw with a message naming the transports it does.
|
|
809
|
+
*/
|
|
810
|
+
type VoiceTransportRequest =
|
|
811
|
+
/** Client has created an offer and wants the provider to complete the handshake. */
|
|
812
|
+
{
|
|
813
|
+
transport: 'webrtc';
|
|
814
|
+
sdpOffer: string;
|
|
815
|
+
}
|
|
816
|
+
/** Client will open a socket itself and needs a short-lived credential for it. */
|
|
817
|
+
| {
|
|
818
|
+
transport: 'websocket';
|
|
819
|
+
};
|
|
820
|
+
/**
|
|
821
|
+
* Everything the client needs to connect, and nothing it needs to decide.
|
|
822
|
+
*
|
|
823
|
+
* `sessionConfig` is the provider-shaped session object (instructions, tools,
|
|
824
|
+
* voice, transcription). The client forwards it verbatim once the connection is
|
|
825
|
+
* up — it is a transport, not a decision-maker.
|
|
826
|
+
*/
|
|
827
|
+
type VoiceSessionDescriptor = {
|
|
828
|
+
transport: 'webrtc';
|
|
829
|
+
/** Answer to the client's offer. */
|
|
830
|
+
sdpAnswer: string;
|
|
831
|
+
sessionConfig: unknown;
|
|
832
|
+
/** Provider-side ID for the call, useful for logging and teardown. */
|
|
833
|
+
providerSessionId?: string;
|
|
834
|
+
} | {
|
|
835
|
+
transport: 'websocket';
|
|
836
|
+
/** Socket URL to connect to. */
|
|
837
|
+
url: string;
|
|
838
|
+
/** Short-lived credential. Must not be a long-lived API key. */
|
|
839
|
+
clientSecret: string;
|
|
840
|
+
/** ISO timestamp after which `clientSecret` stops working. */
|
|
841
|
+
expiresAt: string;
|
|
842
|
+
sessionConfig: unknown;
|
|
843
|
+
providerSessionId?: string;
|
|
844
|
+
};
|
|
845
|
+
/**
|
|
846
|
+
* Options for opening a voice session.
|
|
847
|
+
*
|
|
848
|
+
* Mirrors {@link ChatOptions} where it can, so a provider that already
|
|
849
|
+
* implements `chat` finds the same shapes here.
|
|
850
|
+
*/
|
|
851
|
+
interface VoiceSessionOptions {
|
|
852
|
+
/** How the client wants to connect */
|
|
853
|
+
transport: VoiceTransportRequest;
|
|
854
|
+
/** Model to use — must be one that reported `capabilities.voiceDuplex` */
|
|
855
|
+
model?: string;
|
|
856
|
+
/**
|
|
857
|
+
* System instructions for the session. This is how the assistant keeps her
|
|
858
|
+
* identity in voice mode; providers must pass it through unchanged.
|
|
859
|
+
*/
|
|
860
|
+
instructions?: string;
|
|
861
|
+
/** Tools the model may call during the conversation */
|
|
862
|
+
tools?: ToolDefinition[];
|
|
863
|
+
/**
|
|
864
|
+
* The user's language as an ISO-639-1 code, when known.
|
|
865
|
+
*
|
|
866
|
+
* Pass it to the transcription model rather than letting it detect the
|
|
867
|
+
* language per utterance. Detection is unreliable on short or quiet audio and
|
|
868
|
+
* fails in a way that looks like nonsense rather than an error — a Swedish
|
|
869
|
+
* "hej" coming back as Indonesian, for instance.
|
|
870
|
+
*/
|
|
871
|
+
language?: string;
|
|
872
|
+
/** Provider-specific settings from model configuration */
|
|
873
|
+
settings?: Record<string, unknown>;
|
|
874
|
+
/** Request context (user info, session metadata — not provider config) */
|
|
875
|
+
context?: {
|
|
876
|
+
userId?: string;
|
|
877
|
+
[key: string]: unknown;
|
|
878
|
+
};
|
|
879
|
+
/** Abort signal for cancellation */
|
|
880
|
+
signal?: AbortSignal;
|
|
757
881
|
}
|
|
758
882
|
/**
|
|
759
883
|
* Chat message
|
|
@@ -1395,6 +1519,78 @@ interface UserAPI {
|
|
|
1395
1519
|
getProfile(): Promise<UserProfile>;
|
|
1396
1520
|
listIds(): Promise<string[]>;
|
|
1397
1521
|
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Visual presentation for a conversation Stina starts proactively in response to
|
|
1524
|
+
* an event reported by an extension.
|
|
1525
|
+
*
|
|
1526
|
+
* The extension owns the *chrome* of the list entry — icon, label, framing,
|
|
1527
|
+
* accent and badge — while Stina authors the *content* (title and opening
|
|
1528
|
+
* message). Every field is optional: omitted fields fall back to sensible
|
|
1529
|
+
* defaults, and `icon` falls back to the reporting extension's manifest icon.
|
|
1530
|
+
*
|
|
1531
|
+
* Colour is intentionally a small set of theme tokens rather than free-form so
|
|
1532
|
+
* the conversation list stays visually coherent across light/dark themes.
|
|
1533
|
+
*/
|
|
1534
|
+
interface ConversationPresentation {
|
|
1535
|
+
/**
|
|
1536
|
+
* Structural shape of the list entry — *what kind of thing* this is, not how
|
|
1537
|
+
* urgent it is. Urgency is Stina's call alone (see `severityGuidance`).
|
|
1538
|
+
* Defaults to `note`. Each variant renders a different set of fields:
|
|
1539
|
+
* - `note` — icon, title and a two-line preview. The general-purpose default.
|
|
1540
|
+
* - `event` — something happening at a point in time; renders `at` as a time
|
|
1541
|
+
* block with a countdown. Meetings, reminders, deadlines.
|
|
1542
|
+
* - `message` — something a person or system sent; renders `sender` on its own
|
|
1543
|
+
* line with the subject as the title. Mail, chat, comments.
|
|
1544
|
+
* - `digest` — several things collected into one entry; renders `items` as a
|
|
1545
|
+
* short bullet list with a count. Morning briefs, weekly summaries.
|
|
1546
|
+
* - `status` — a transactional outcome; renders `statusLabel` as a pill and no
|
|
1547
|
+
* preview. Finished builds, deliveries, completed jobs.
|
|
1548
|
+
* - `insight` — Stina's own observation or suggestion rather than an extension's
|
|
1549
|
+
* report. Rendered in her own voice with a soft accent panel.
|
|
1550
|
+
*/
|
|
1551
|
+
variant?: 'note' | 'event' | 'message' | 'digest' | 'status' | 'insight';
|
|
1552
|
+
/**
|
|
1553
|
+
* Leading icon, as a Hugeicons name in lower case with hyphens (`sun-03`,
|
|
1554
|
+
* `cloud-angled-rain`). Hugeicons documents its names in PascalCase; those are
|
|
1555
|
+
* accepted and translated, but the hyphenated form is what the icon set uses.
|
|
1556
|
+
* An unknown name falls back to a default rather than rendering blank.
|
|
1557
|
+
* Falls back to the extension's manifest icon when omitted.
|
|
1558
|
+
*/
|
|
1559
|
+
icon?: HugeIconName;
|
|
1560
|
+
/** Small eyebrow label above the content, e.g. "Mail" or "Reminder". */
|
|
1561
|
+
label?: LocalizedString;
|
|
1562
|
+
/** Accent colour as a theme token. Defaults to `default`. */
|
|
1563
|
+
accent?: 'default' | 'info' | 'warning' | 'success' | 'danger';
|
|
1564
|
+
/** Optional short badge, e.g. the account a mail arrived in. */
|
|
1565
|
+
badge?: LocalizedString;
|
|
1566
|
+
/**
|
|
1567
|
+
* When the event happens (ISO 8601). Only used by `variant: 'event'`, which
|
|
1568
|
+
* renders it as a time block and derives a relative countdown from it.
|
|
1569
|
+
*/
|
|
1570
|
+
at?: string;
|
|
1571
|
+
/** Who sent it. Only used by `variant: 'message'`. */
|
|
1572
|
+
sender?: LocalizedString;
|
|
1573
|
+
/**
|
|
1574
|
+
* The collected items. Only used by `variant: 'digest'`; at most three are
|
|
1575
|
+
* rendered, and `count` reports the true total when there are more.
|
|
1576
|
+
*/
|
|
1577
|
+
items?: LocalizedString[];
|
|
1578
|
+
/** Total number of collected items, when it exceeds what `items` lists. */
|
|
1579
|
+
count?: number;
|
|
1580
|
+
/** Short outcome label for the pill, e.g. "Delivered". Only used by `variant: 'status'`. */
|
|
1581
|
+
statusLabel?: LocalizedString;
|
|
1582
|
+
/**
|
|
1583
|
+
* Guidance to Stina on how to judge how much this deserves the user's attention
|
|
1584
|
+
* — *not* a severity value. Extensions describe what would make an event
|
|
1585
|
+
* important ("urgent when the sender is on the user's team, or the deadline is
|
|
1586
|
+
* today"); Stina weighs that against everything else she knows and picks the
|
|
1587
|
+
* level herself. She may disregard it entirely.
|
|
1588
|
+
*
|
|
1589
|
+
* Plain text in any language, one or two sentences. It is shown to Stina during
|
|
1590
|
+
* her deliberation and never to the user.
|
|
1591
|
+
*/
|
|
1592
|
+
severityGuidance?: string;
|
|
1593
|
+
}
|
|
1398
1594
|
/**
|
|
1399
1595
|
* Chat instruction message
|
|
1400
1596
|
*/
|
|
@@ -1402,6 +1598,24 @@ interface ChatInstructionMessage {
|
|
|
1402
1598
|
text: string;
|
|
1403
1599
|
conversationId?: string;
|
|
1404
1600
|
userId?: string;
|
|
1601
|
+
/**
|
|
1602
|
+
* When true, the message is treated as a system event reported by the extension
|
|
1603
|
+
* rather than a plain instruction. Instead of being appended to a conversation,
|
|
1604
|
+
* Stina runs a hidden deliberation over it and decides herself whether to notify
|
|
1605
|
+
* the user (by starting a new conversation via the `core_init_chat_session` tool).
|
|
1606
|
+
*/
|
|
1607
|
+
deliberate?: boolean;
|
|
1608
|
+
/**
|
|
1609
|
+
* Identifier of the source that reported the event (typically the extension id).
|
|
1610
|
+
* Used for logging/tracing of proactive decisions.
|
|
1611
|
+
*/
|
|
1612
|
+
source?: string;
|
|
1613
|
+
/**
|
|
1614
|
+
* Optional visual presentation for the conversation Stina may start from this
|
|
1615
|
+
* event. Only meaningful together with `deliberate: true`; ignored if Stina
|
|
1616
|
+
* decides not to notify the user.
|
|
1617
|
+
*/
|
|
1618
|
+
presentation?: ConversationPresentation;
|
|
1405
1619
|
}
|
|
1406
1620
|
/**
|
|
1407
1621
|
* Chat API for appending instructions
|
|
@@ -1434,11 +1648,14 @@ interface ExtensionModule {
|
|
|
1434
1648
|
/**
|
|
1435
1649
|
* Restart policy for background tasks.
|
|
1436
1650
|
* Controls how tasks are restarted after failures.
|
|
1651
|
+
*
|
|
1652
|
+
* A task that returns from its callback without having been aborted is
|
|
1653
|
+
* considered finished on purpose and is never restarted, regardless of policy.
|
|
1437
1654
|
*/
|
|
1438
1655
|
interface BackgroundRestartPolicy {
|
|
1439
1656
|
/**
|
|
1440
1657
|
* When to restart the task:
|
|
1441
|
-
* - 'always':
|
|
1658
|
+
* - 'always': Restart if the task threw, or stopped without anyone asking it to
|
|
1442
1659
|
* - 'on-failure': Only restart if the task threw an error
|
|
1443
1660
|
* - 'never': Never restart automatically
|
|
1444
1661
|
*/
|
|
@@ -1519,6 +1736,11 @@ interface BackgroundTaskContext extends ExecutionContext {
|
|
|
1519
1736
|
* Callback function for background tasks.
|
|
1520
1737
|
* The function should run until the signal is aborted, then clean up and return.
|
|
1521
1738
|
*
|
|
1739
|
+
* Returning before the signal is aborted means the task has finished on purpose:
|
|
1740
|
+
* it is marked as completed and is not restarted, whatever the restart policy says.
|
|
1741
|
+
* A task that has nothing to do right now but wants to be retried later should
|
|
1742
|
+
* therefore keep running (and wait on the signal) rather than return early.
|
|
1743
|
+
*
|
|
1522
1744
|
* @example
|
|
1523
1745
|
* ```typescript
|
|
1524
1746
|
* const callback: BackgroundTaskCallback = async (ctx) => {
|
|
@@ -1554,8 +1776,10 @@ interface BackgroundTaskHealth {
|
|
|
1554
1776
|
userId: string;
|
|
1555
1777
|
/**
|
|
1556
1778
|
* Current task status.
|
|
1779
|
+
* 'completed' means the task returned on its own and will not be restarted,
|
|
1780
|
+
* while 'stopped' means someone asked it to stop.
|
|
1557
1781
|
*/
|
|
1558
|
-
status: 'pending' | 'running' | 'stopped' | 'failed' | 'restarting';
|
|
1782
|
+
status: 'pending' | 'running' | 'stopped' | 'completed' | 'failed' | 'restarting';
|
|
1559
1783
|
/**
|
|
1560
1784
|
* Number of times the task has been restarted.
|
|
1561
1785
|
*/
|
|
@@ -1681,4 +1905,4 @@ interface ActionResult {
|
|
|
1681
1905
|
error?: string;
|
|
1682
1906
|
}
|
|
1683
1907
|
|
|
1684
|
-
export { type
|
|
1908
|
+
export { type BackgroundTaskHealth as $, type ActionResult as A, type EventsAPI as B, type ChatMessage as C, type Disposable as D, type ExtensionContributions as E, type SchedulerAPI as F, type GetModelsOptions as G, type HugeIconName as H, type SchedulerJobRequest as I, type SchedulerSchedule as J, type UserProfile as K, type LocalizedString as L, type ModelInfo as M, type NetworkAPI as N, type ChatAPI as O, type PanelDefinition as P, type ChatInstructionMessage as Q, type ConversationPresentation as R, type SchedulerFirePayload as S, type ToolResult as T, type UserAPI as U, type VoiceSessionOptions as V, type LogAPI as W, type BackgroundWorkersAPI as X, type BackgroundTaskConfig as Y, type BackgroundTaskCallback as Z, type BackgroundTaskContext as _, type ChatOptions as a, type BackgroundRestartPolicy as a0, type Query as a1, type QueryOptions as a2, type StorageAPI as a3, type SecretsAPI as a4, type StorageCollectionConfig as a5, type StorageContributions as a6, type AIProvider as a7, type ModelCapabilities as a8, type ToolCall as a9, type GridProps as aA, type DividerProps as aB, type IconProps as aC, type IconButtonType as aD, type IconButtonProps as aE, type PanelAction as aF, type PanelProps as aG, type ToggleProps as aH, type CollapsibleProps as aI, type FrameVariant as aJ, type FrameProps as aK, type ListProps as aL, type PillVariant as aM, type PillProps as aN, type CheckboxProps as aO, type MarkdownProps as aP, type TextPreviewProps as aQ, type ModalProps as aR, type ConditionalGroupProps as aS, type ExecutionContext as aT, type VoiceTransportRequest as aa, type Tool as ab, type Action as ac, type ExtensionModule as ad, type AllowedCSSProperty as ae, type ExtensionComponentStyle as af, type ExtensionComponentData as ag, type ExtensionComponentIterator as ah, type ExtensionComponentChildren as ai, type ExtensionActionCall as aj, type ExtensionActionRef as ak, type ExtensionDataSource as al, type ExtensionPanelDefinition as am, type HeaderProps as an, type LabelProps as ao, type ParagraphProps as ap, type ButtonProps as aq, type TextInputProps as ar, type PasswordInputProps as as, type NumberInputProps as at, type TextAreaProps as au, type DateTimeInputProps as av, type SelectProps as aw, type IconPickerProps as ax, type VerticalStackProps as ay, type HorizontalStackProps as az, type StreamEvent as b, type VoiceSessionDescriptor as c, type ToolSettingsViewDefinition as d, type ToolSettingsView as e, type ToolSettingsListView as f, type ToolSettingsListMapping as g, type ToolSettingsComponentView as h, type ToolSettingsActionDataSource as i, type PanelView as j, type PanelComponentView as k, type PanelActionDataSource as l, type PanelUnknownView as m, type ProviderDefinition as n, type ProviderConfigView as o, type PromptContribution as p, type PromptSection as q, resolveLocalizedString as r, type ToolDefinition as s, type ToolConfirmationConfig as t, type CommandDefinition as u, type ExtensionContext as v, type SettingsAPI as w, type ProvidersAPI as x, type ToolsAPI as y, type ActionsAPI as z };
|
package/package.json
CHANGED
package/src/background.test.ts
CHANGED
|
@@ -185,7 +185,8 @@ describe('WorkerBackgroundTaskManager', () => {
|
|
|
185
185
|
|
|
186
186
|
expect(callback).toHaveBeenCalledTimes(1)
|
|
187
187
|
expect(sendTaskStatus).toHaveBeenCalledWith('task-1', 'running')
|
|
188
|
-
expect(sendTaskStatus).toHaveBeenCalledWith('task-1', '
|
|
188
|
+
expect(sendTaskStatus).toHaveBeenCalledWith('task-1', 'completed')
|
|
189
|
+
expect(sendTaskStatus).not.toHaveBeenCalledWith('task-1', 'stopped')
|
|
189
190
|
})
|
|
190
191
|
|
|
191
192
|
it('should abort previous execution if already running', async () => {
|
|
@@ -498,7 +499,7 @@ describe('WorkerBackgroundTaskManager', () => {
|
|
|
498
499
|
expect(disposed).toBe(true)
|
|
499
500
|
})
|
|
500
501
|
|
|
501
|
-
it('should
|
|
502
|
+
it('should run a task that was stopped before it started', async () => {
|
|
502
503
|
const config: BackgroundTaskConfig = {
|
|
503
504
|
id: 'task-1',
|
|
504
505
|
name: 'Test Task',
|
|
@@ -512,12 +513,12 @@ describe('WorkerBackgroundTaskManager', () => {
|
|
|
512
513
|
manager.stop('task-1')
|
|
513
514
|
sendTaskStatus.mockClear()
|
|
514
515
|
|
|
515
|
-
// Now start it
|
|
516
|
+
// Now start it - handleStart creates a fresh AbortController, so the
|
|
517
|
+
// earlier stop does not carry over into this run
|
|
516
518
|
await manager.handleStart('task-1')
|
|
517
519
|
|
|
518
|
-
// Should immediately detect it's aborted
|
|
519
520
|
expect(sendTaskStatus).toHaveBeenCalledWith('task-1', 'running')
|
|
520
|
-
expect(sendTaskStatus).toHaveBeenCalledWith('task-1', '
|
|
521
|
+
expect(sendTaskStatus).toHaveBeenCalledWith('task-1', 'completed')
|
|
521
522
|
})
|
|
522
523
|
|
|
523
524
|
it('should handle multiple rapid start/stop cycles', async () => {
|
package/src/background.ts
CHANGED
|
@@ -27,7 +27,7 @@ interface RegisteredTask {
|
|
|
27
27
|
config: BackgroundTaskConfig
|
|
28
28
|
callback: BackgroundTaskCallback
|
|
29
29
|
abortController: AbortController | null
|
|
30
|
-
status: 'pending' | 'running' | 'stopped' | 'failed'
|
|
30
|
+
status: 'pending' | 'running' | 'stopped' | 'completed' | 'failed'
|
|
31
31
|
lastHealthStatus?: string
|
|
32
32
|
lastHealthTime?: string
|
|
33
33
|
error?: string
|
|
@@ -49,7 +49,11 @@ export interface WorkerBackgroundTaskManagerOptions {
|
|
|
49
49
|
payload?: Record<string, unknown>
|
|
50
50
|
) => void
|
|
51
51
|
/** Send status update to host */
|
|
52
|
-
sendTaskStatus: (
|
|
52
|
+
sendTaskStatus: (
|
|
53
|
+
taskId: string,
|
|
54
|
+
status: 'running' | 'stopped' | 'completed' | 'failed',
|
|
55
|
+
error?: string
|
|
56
|
+
) => void
|
|
53
57
|
/** Send health report to host */
|
|
54
58
|
sendHealthReport: (taskId: string, status: string, timestamp: string) => void
|
|
55
59
|
/** Create a log API for a task */
|
|
@@ -156,8 +160,11 @@ export class WorkerBackgroundTaskManager {
|
|
|
156
160
|
task.abortController = null
|
|
157
161
|
}
|
|
158
162
|
|
|
159
|
-
// Create new AbortController for this run
|
|
160
|
-
|
|
163
|
+
// Create new AbortController for this run.
|
|
164
|
+
// Keep a local reference: stop() clears the field, so it cannot be used
|
|
165
|
+
// afterwards to tell an aborted run from one that returned on its own.
|
|
166
|
+
const abortController = new AbortController()
|
|
167
|
+
task.abortController = abortController
|
|
161
168
|
task.status = 'running'
|
|
162
169
|
task.error = undefined
|
|
163
170
|
|
|
@@ -171,14 +178,14 @@ export class WorkerBackgroundTaskManager {
|
|
|
171
178
|
// Execute the callback
|
|
172
179
|
await task.callback(context)
|
|
173
180
|
|
|
174
|
-
|
|
175
|
-
|
|
181
|
+
if (abortController.signal.aborted) {
|
|
182
|
+
// The run was aborted - stop() has already reported the status
|
|
176
183
|
task.status = 'stopped'
|
|
177
|
-
this.options.sendTaskStatus(taskId, 'stopped')
|
|
178
184
|
} else {
|
|
179
|
-
//
|
|
180
|
-
task
|
|
181
|
-
|
|
185
|
+
// The callback returned on its own: the task is done and must not be
|
|
186
|
+
// restarted, or a task with nothing to do would loop forever
|
|
187
|
+
task.status = 'completed'
|
|
188
|
+
this.options.sendTaskStatus(taskId, 'completed')
|
|
182
189
|
}
|
|
183
190
|
} catch (error) {
|
|
184
191
|
// Task failed with an error
|
package/src/index.ts
CHANGED
|
@@ -61,6 +61,7 @@ export type {
|
|
|
61
61
|
UserProfile,
|
|
62
62
|
ChatAPI,
|
|
63
63
|
ChatInstructionMessage,
|
|
64
|
+
ConversationPresentation,
|
|
64
65
|
LogAPI,
|
|
65
66
|
|
|
66
67
|
// Background workers
|
|
@@ -82,12 +83,18 @@ export type {
|
|
|
82
83
|
// AI Provider
|
|
83
84
|
AIProvider,
|
|
84
85
|
ModelInfo,
|
|
86
|
+
ModelCapabilities,
|
|
85
87
|
ChatMessage,
|
|
86
88
|
ChatOptions,
|
|
87
89
|
GetModelsOptions,
|
|
88
90
|
StreamEvent,
|
|
89
91
|
ToolCall,
|
|
90
92
|
|
|
93
|
+
// Duplex voice
|
|
94
|
+
VoiceSessionOptions,
|
|
95
|
+
VoiceSessionDescriptor,
|
|
96
|
+
VoiceTransportRequest,
|
|
97
|
+
|
|
91
98
|
// Tools
|
|
92
99
|
Tool,
|
|
93
100
|
ToolResult,
|
package/src/messages.ts
CHANGED
|
@@ -11,6 +11,8 @@ import type {
|
|
|
11
11
|
ActionResult,
|
|
12
12
|
ModelInfo,
|
|
13
13
|
SchedulerFirePayload,
|
|
14
|
+
VoiceSessionOptions,
|
|
15
|
+
VoiceSessionDescriptor,
|
|
14
16
|
} from './types.js'
|
|
15
17
|
|
|
16
18
|
// ============================================================================
|
|
@@ -24,6 +26,7 @@ export type HostToWorkerMessage =
|
|
|
24
26
|
| SchedulerFireMessage
|
|
25
27
|
| ProviderChatRequestMessage
|
|
26
28
|
| ProviderModelsRequestMessage
|
|
29
|
+
| ProviderVoiceSessionRequestMessage
|
|
27
30
|
| ToolExecuteRequestMessage
|
|
28
31
|
| ActionExecuteRequestMessage
|
|
29
32
|
| ResponseMessage
|
|
@@ -82,6 +85,20 @@ export interface ProviderModelsRequestMessage {
|
|
|
82
85
|
}
|
|
83
86
|
}
|
|
84
87
|
|
|
88
|
+
export interface ProviderVoiceSessionRequestMessage {
|
|
89
|
+
type: 'provider-voice-session-request'
|
|
90
|
+
id: string
|
|
91
|
+
payload: {
|
|
92
|
+
providerId: string
|
|
93
|
+
/**
|
|
94
|
+
* `signal` is dropped on the way across: an AbortSignal cannot be
|
|
95
|
+
* structured-cloned into a worker. Cancellation is handled by the host
|
|
96
|
+
* rejecting the pending request instead.
|
|
97
|
+
*/
|
|
98
|
+
options: Omit<VoiceSessionOptions, 'signal'>
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
85
102
|
export interface ToolExecuteRequestMessage {
|
|
86
103
|
type: 'tool-execute-request'
|
|
87
104
|
id: string
|
|
@@ -165,6 +182,7 @@ export type WorkerToHostMessage =
|
|
|
165
182
|
| StreamEventMessage
|
|
166
183
|
| LogMessage
|
|
167
184
|
| ProviderModelsResponseMessage
|
|
185
|
+
| ProviderVoiceSessionResponseMessage
|
|
168
186
|
| ToolExecuteResponseMessage
|
|
169
187
|
| ActionExecuteResponseMessage
|
|
170
188
|
| StreamingFetchAckMessage
|
|
@@ -289,6 +307,16 @@ export interface ProviderModelsResponseMessage {
|
|
|
289
307
|
}
|
|
290
308
|
}
|
|
291
309
|
|
|
310
|
+
export interface ProviderVoiceSessionResponseMessage {
|
|
311
|
+
type: 'provider-voice-session-response'
|
|
312
|
+
payload: {
|
|
313
|
+
requestId: string
|
|
314
|
+
/** Absent when `error` is set. */
|
|
315
|
+
descriptor?: VoiceSessionDescriptor
|
|
316
|
+
error?: string
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
292
320
|
export interface ToolExecuteResponseMessage {
|
|
293
321
|
type: 'tool-execute-response'
|
|
294
322
|
payload: {
|
|
@@ -343,7 +371,7 @@ export interface BackgroundTaskStatusMessage {
|
|
|
343
371
|
type: 'background-task-status'
|
|
344
372
|
payload: {
|
|
345
373
|
taskId: string
|
|
346
|
-
status: 'running' | 'stopped' | 'failed'
|
|
374
|
+
status: 'running' | 'stopped' | 'completed' | 'failed'
|
|
347
375
|
error?: string
|
|
348
376
|
}
|
|
349
377
|
}
|
package/src/runtime.ts
CHANGED
|
@@ -34,6 +34,7 @@ import type {
|
|
|
34
34
|
ChatMessage,
|
|
35
35
|
ChatOptions,
|
|
36
36
|
GetModelsOptions,
|
|
37
|
+
VoiceSessionOptions,
|
|
37
38
|
BackgroundWorkersAPI,
|
|
38
39
|
BackgroundTaskConfig,
|
|
39
40
|
BackgroundTaskCallback,
|
|
@@ -190,6 +191,10 @@ async function handleHostMessage(message: HostToWorkerMessage): Promise<void> {
|
|
|
190
191
|
await handleProviderModelsRequest(message.id, message.payload)
|
|
191
192
|
break
|
|
192
193
|
|
|
194
|
+
case 'provider-voice-session-request':
|
|
195
|
+
await handleProviderVoiceSessionRequest(message.id, message.payload)
|
|
196
|
+
break
|
|
197
|
+
|
|
193
198
|
case 'tool-execute-request':
|
|
194
199
|
await handleToolExecuteRequest(message.id, message.payload)
|
|
195
200
|
break
|
|
@@ -488,6 +493,37 @@ async function handleProviderModelsRequest(
|
|
|
488
493
|
}
|
|
489
494
|
}
|
|
490
495
|
|
|
496
|
+
async function handleProviderVoiceSessionRequest(
|
|
497
|
+
requestId: string,
|
|
498
|
+
payload: { providerId: string; options: VoiceSessionOptions }
|
|
499
|
+
): Promise<void> {
|
|
500
|
+
const respond = (error: string) =>
|
|
501
|
+
postMessage({ type: 'provider-voice-session-response', payload: { requestId, error } })
|
|
502
|
+
|
|
503
|
+
const provider = registeredProviders.get(payload.providerId)
|
|
504
|
+
if (!provider) {
|
|
505
|
+
respond(`Provider ${payload.providerId} not found`)
|
|
506
|
+
return
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/*
|
|
510
|
+
* Voice is opt-in per provider, so a missing method is an ordinary outcome
|
|
511
|
+
* rather than a crash. Name the provider — the caller only knows it asked for
|
|
512
|
+
* a model, and a bare "not supported" leaves it guessing which part refused.
|
|
513
|
+
*/
|
|
514
|
+
if (!provider.createVoiceSession) {
|
|
515
|
+
respond(`Provider ${payload.providerId} does not support voice sessions`)
|
|
516
|
+
return
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
const descriptor = await provider.createVoiceSession(payload.options)
|
|
521
|
+
postMessage({ type: 'provider-voice-session-response', payload: { requestId, descriptor } })
|
|
522
|
+
} catch (error) {
|
|
523
|
+
respond(error instanceof Error ? error.message : String(error))
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
491
527
|
async function handleToolExecuteRequest(
|
|
492
528
|
requestId: string,
|
|
493
529
|
payload: { toolId: string; params: Record<string, unknown>; userId?: string }
|
|
@@ -967,6 +1003,10 @@ export type {
|
|
|
967
1003
|
ChatMessage,
|
|
968
1004
|
ChatOptions,
|
|
969
1005
|
GetModelsOptions,
|
|
1006
|
+
ModelCapabilities,
|
|
1007
|
+
VoiceSessionOptions,
|
|
1008
|
+
VoiceSessionDescriptor,
|
|
1009
|
+
VoiceTransportRequest,
|
|
970
1010
|
StreamEvent,
|
|
971
1011
|
// Storage and secrets
|
|
972
1012
|
StorageAPI,
|
|
@@ -50,6 +50,10 @@ export const ExtensionManifestSchema = z
|
|
|
50
50
|
.regex(/^\d+\.\d+\.\d+/, 'Must be semver format (e.g., "1.0.0")')
|
|
51
51
|
.describe('Version string (semver)'),
|
|
52
52
|
description: z.string().min(1).describe('Short description'),
|
|
53
|
+
// Optional icon (Hugeicons name) used as the default icon for proactive
|
|
54
|
+
// conversation list entries this extension triggers. Kept optional so the
|
|
55
|
+
// field is preserved (not stripped) when a manifest is parsed at load time.
|
|
56
|
+
icon: z.string().optional().describe('Icon (Hugeicons name) representing the extension'),
|
|
53
57
|
author: AuthorSchema.describe('Author information'),
|
|
54
58
|
repository: z.string().url().optional().describe('Repository URL'),
|
|
55
59
|
license: z.string().optional().describe('License identifier'),
|