@asterism-app/sdk 0.0.0-stage → 0.47.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.
Files changed (90) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +20 -2
  3. package/docs/data-and-lifecycle.md +77 -0
  4. package/docs/getting-started.md +37 -0
  5. package/docs/manual.md +34 -0
  6. package/docs/minimal-plugin.md +33 -0
  7. package/docs/project-structure.md +27 -0
  8. package/docs/resources-and-tools.md +29 -0
  9. package/docs/views-and-settings.md +35 -0
  10. package/examples/author.ts +158 -0
  11. package/examples/consume-usage.ts +29 -0
  12. package/examples/content-author.ts +29 -0
  13. package/examples/content-contract.ts +10 -0
  14. package/examples/content-renderer.ts +26 -0
  15. package/examples/counter/docs/usage.md +9 -0
  16. package/examples/counter/package.json +27 -0
  17. package/examples/counter/src/contracts.ts +31 -0
  18. package/examples/counter/src/panel.module.css +2 -0
  19. package/examples/counter/src/renderer.tsx +69 -0
  20. package/examples/counter/src/runtime.ts +43 -0
  21. package/examples/counter/src/storage.ts +4 -0
  22. package/examples/counter/src/styles.d.ts +4 -0
  23. package/examples/counter/tsconfig.json +16 -0
  24. package/examples/counter-contract.ts +18 -0
  25. package/examples/data-codecs.ts +18 -0
  26. package/examples/events.ts +37 -0
  27. package/examples/i18n.ts +18 -0
  28. package/examples/model-completion.ts +15 -0
  29. package/examples/model-input.ts +16 -0
  30. package/examples/plugin-calls.ts +22 -0
  31. package/examples/provider-usage-headers.ts +20 -0
  32. package/examples/provider-usage.ts +34 -0
  33. package/examples/renderer.ts +47 -0
  34. package/examples/session-resources.ts +29 -0
  35. package/examples/settings-contract.ts +8 -0
  36. package/examples/settings-renderer.ts +30 -0
  37. package/examples/settings-runtime.ts +11 -0
  38. package/examples/storage.ts +26 -0
  39. package/examples/tool-renderer.ts +47 -0
  40. package/examples/web-provider.ts +27 -0
  41. package/package.json +39 -4
  42. package/src/calls.ts +27 -0
  43. package/src/card.ts +7 -0
  44. package/src/channels.ts +20 -0
  45. package/src/commands.ts +28 -0
  46. package/src/config.ts +25 -0
  47. package/src/content-runtime.ts +11 -0
  48. package/src/content.ts +6 -0
  49. package/src/contracts.ts +29 -0
  50. package/src/data-encoding.ts +14 -0
  51. package/src/error-details.ts +78 -0
  52. package/src/events.ts +91 -0
  53. package/src/i18n.ts +58 -0
  54. package/src/json.ts +4 -0
  55. package/src/model-cost-estimate.ts +49 -0
  56. package/src/model-cost.ts +44 -0
  57. package/src/model-input.ts +19 -0
  58. package/src/model-media.ts +11 -0
  59. package/src/model-records.ts +56 -0
  60. package/src/model-selection.ts +34 -0
  61. package/src/models.ts +137 -0
  62. package/src/module-contract.ts +44 -0
  63. package/src/objects.ts +89 -0
  64. package/src/plugin.ts +58 -0
  65. package/src/presentation.ts +21 -0
  66. package/src/provider-usage.ts +87 -0
  67. package/src/records.ts +60 -0
  68. package/src/renderer-calls.ts +11 -0
  69. package/src/renderer-card.ts +29 -0
  70. package/src/renderer-content.ts +9 -0
  71. package/src/renderer-creation.ts +83 -0
  72. package/src/renderer-i18n.ts +49 -0
  73. package/src/renderer-island.ts +66 -0
  74. package/src/renderer-records.ts +49 -0
  75. package/src/renderer-services.ts +75 -0
  76. package/src/renderer-tools.ts +103 -0
  77. package/src/renderer-view-state.ts +33 -0
  78. package/src/renderer.ts +158 -0
  79. package/src/runtime-calls.ts +20 -0
  80. package/src/runtime-channels.ts +24 -0
  81. package/src/runtime-contracts.ts +188 -0
  82. package/src/runtime-providers.ts +60 -0
  83. package/src/runtime-resources.ts +117 -0
  84. package/src/runtime-tools.ts +31 -0
  85. package/src/runtime-web.ts +36 -0
  86. package/src/runtime.ts +44 -0
  87. package/src/tool-result.ts +45 -0
  88. package/src/tool-validation.ts +14 -0
  89. package/src/web.ts +75 -0
  90. package/src/workspace.ts +43 -0
@@ -0,0 +1,9 @@
1
+ import type { ContentRef } from "./content.ts";
2
+ import type { ContentReader } from "./content-runtime.ts";
3
+
4
+ /** The host binds input drafts to contracts; business input carries only ContentRef. The host closes drafts after command success and preserves them for retry after failure. */
5
+ export interface RendererContentInput extends ContentReader {
6
+ stage(source: Blob | Uint8Array, options?: { signal?: AbortSignal; progress?(bytes: number, total: number): void }): Promise<ContentRef>;
7
+ close(): Promise<void>;
8
+ }
9
+ export interface RendererInputOptions { content?: RendererContentInput }
@@ -0,0 +1,83 @@
1
+ import { createContext, useContext, type ComponentType, type ReactNode } from "react";
2
+ import type { z } from "zod";
3
+ import type { CreationContract } from "./commands.ts";
4
+ import type { Json } from "./json.ts";
5
+ import type { RendererContentInput } from "./renderer-content.ts";
6
+
7
+ /**
8
+ * Host snapshot of dropped content: the host reads it from DataTransfer at drop time, and it remains valid after the event ends.
9
+ * The host resolves file paths through the desktop bridge; plugins cannot forge them.
10
+ */
11
+ export type DropItem =
12
+ /** Local drops carry a host-resolved path; sources without paths, such as clipboard images, contain only file bytes. */
13
+ | { readonly kind: "file"; readonly file: File; readonly name: string; readonly mime: string; readonly directory: boolean; readonly path?: string }
14
+ | { readonly kind: "url"; readonly url: string }
15
+ | { readonly kind: "text"; readonly text: string };
16
+
17
+ /**
18
+ * Declarative acceptance conditions: the host matches them synchronously during drag hover without calling plugin functions.
19
+ * Extensions include a leading dot and use lowercase; MIME supports wildcards such as `image/*`.
20
+ */
21
+ export interface DropAccepts {
22
+ /** requiresPath accepts only items with a local path; directories implicitly requires a path. */
23
+ readonly files?: { readonly requiresPath?: boolean; readonly directories?: boolean; readonly extensions?: readonly string[]; readonly mime?: readonly string[] };
24
+ readonly urls?: { readonly schemes?: readonly string[] };
25
+ readonly text?: boolean;
26
+ }
27
+
28
+ /** The source that triggered the entry point; menus have no payload. */
29
+ export type CreationSource = { readonly kind: "menu" } | { readonly kind: "drop"; readonly item: DropItem };
30
+
31
+ /** Plugin-owned preferences: the host isolates them by plugin and persists them in UI state; reads validate using the caller's schema. */
32
+ export interface RendererPreferences {
33
+ get<T extends Json>(key: string, schema: z.ZodType<T>): T | null;
34
+ set(key: string, value: Json | null): void;
35
+ }
36
+
37
+ /**
38
+ * Input phase component: can render any UI, such as forms or previews, and can only return input or cancel.
39
+ * resolve returns whether host creation succeeded; on failure the component stays open and the host shell displays the error.
40
+ */
41
+ export interface CreationInputProps<I = Json> {
42
+ readonly source: CreationSource;
43
+ readonly initialInput?: I;
44
+ readonly busy: boolean;
45
+ readonly content: RendererContentInput;
46
+ resolve(input: I): Promise<boolean>;
47
+ cancel(): void;
48
+ }
49
+
50
+ export interface CreationEntry<I = Json> {
51
+ /** Globally stable identity using the declaring plugin's namespace; routing memory stores this as its value. */
52
+ readonly id: string;
53
+ readonly contract: CreationContract<I, Json, Json>;
54
+ readonly label: UiText;
55
+ readonly description?: UiText;
56
+ readonly icon?: ReactNode;
57
+ /** Menu group; defaults to resources when omitted. */
58
+ readonly group?: "agents" | "resources";
59
+ readonly triggers: {
60
+ readonly menu?: { readonly initialInput?: I };
61
+ readonly drop?: { readonly accepts: DropAccepts; toInput(item: DropItem, content: RendererContentInput): I | null | Promise<I | null> };
62
+ };
63
+ /** When omitted, input is produced directly from the trigger source; menu sources must then provide initialInput. */
64
+ readonly InputView?: ComponentType<CreationInputProps<I>>;
65
+ }
66
+
67
+ /** Type erasure occurs only at the registration boundary; call and component input types are inferred from the creation contract. */
68
+ export function creationEntry<I, O extends Json>(contract: CreationContract<I, Json, O>,
69
+ definition: Omit<CreationEntry<NoInfer<I> & Json>, "contract">): CreationEntry {
70
+ if (!definition.triggers.menu && !definition.triggers.drop) throw new Error(`Creation entry ${definition.id} declares no trigger.`);
71
+ if (definition.triggers.menu && !definition.InputView && definition.triggers.menu.initialInput === undefined)
72
+ throw new Error(`Creation entry ${definition.id} needs an InputView or a menu initialInput.`);
73
+ return { ...definition, contract } as unknown as CreationEntry;
74
+ }
75
+
76
+ const Preferences = createContext<RendererPreferences | null>(null);
77
+ export const RendererPreferencesProvider = Preferences.Provider;
78
+ export function useRendererPreferences(): RendererPreferences {
79
+ const value = useContext(Preferences);
80
+ if (!value) throw new Error("Renderer preferences must be provided by the host.");
81
+ return value;
82
+ }
83
+ import type { UiText } from "./i18n.ts";
@@ -0,0 +1,49 @@
1
+ import { createContext, createElement, useContext, type ReactNode } from "react";
2
+ import { createIntl, createIntlCache, RawIntlProvider, FormattedMessage } from "react-intl";
3
+
4
+ import type { UiText, UiMessage, MessageValues } from "./i18n.ts";
5
+
6
+ export interface I18n {
7
+ readonly locale: string;
8
+ readonly interfaceLocale: string;
9
+ readonly revision: number;
10
+ text(value: UiText): string;
11
+ formatMessage(message: UiMessage, values?: MessageValues): string;
12
+ formatNumber(value: number, options?: Intl.NumberFormatOptions): string;
13
+ formatDate(value: number, options?: Intl.DateTimeFormatOptions): string;
14
+ resolve(message: UiMessage): { locale: string; message: string; revision: string };
15
+ }
16
+
17
+ const I18nContext = createContext<I18n | null>(null);
18
+
19
+ export function I18nProvider({ value, children }: { value: I18n; children: ReactNode }) {
20
+ return createElement(I18nContext.Provider, { value }, children);
21
+ }
22
+
23
+ export function useI18n(): I18n {
24
+ const value = useContext(I18nContext);
25
+ if (!value) throw new Error("The renderer requires the host I18nProvider.");
26
+ return value;
27
+ }
28
+
29
+ const cache = createIntlCache();
30
+
31
+ /** Rich-text mappings remain in React; default copy is still declared and extracted through standard defineMessages. */
32
+ export function UiFormattedMessage({ message, values }: {
33
+ message: UiMessage;
34
+ values?: Record<string, string | number | boolean | ((chunks: ReactNode[]) => ReactNode)>;
35
+ }) {
36
+ const i18n = useI18n();
37
+ const selected = i18n.resolve(message);
38
+ const intl = createIntl({
39
+ locale: selected.locale,
40
+ defaultLocale: message.sourceLocale,
41
+ messages: { [message.id]: selected.message },
42
+ onError: error => console.warn("Rich text message formatting failed.", error),
43
+ }, cache);
44
+ return createElement(RawIntlProvider, { value: intl }, createElement(FormattedMessage, {
45
+ id: message.id,
46
+ defaultMessage: message.defaultMessage,
47
+ values: { ...message.values, ...values },
48
+ }));
49
+ }
@@ -0,0 +1,66 @@
1
+ import { createContext, createElement, useContext, useEffect, useRef, type ComponentType, type ReactNode } from "react";
2
+ import type { UiText } from "./i18n.ts";
3
+
4
+ /**
5
+ * Island: an independent surface column within the host screen; opening one places it to the right of the current island and replaces existing deeper islands.
6
+ * The host uniformly manages width, position, motion, and focus; plugins provide only identity, an accessible name, and content.
7
+ */
8
+ export interface RendererIslandSpec {
9
+ /** Island identity unique within the plugin; the host isolates it by plugin identity. Opening the same identity again replaces its content. */
10
+ key: string;
11
+ /** The island's accessible name and overflow navigation hint. */
12
+ label: UiText;
13
+ /** The element that opened it: used for the expansion animation and receives focus again after closing. */
14
+ origin?: HTMLElement | null;
15
+ render: () => ReactNode;
16
+ }
17
+ /** Close guard: returning false (or resolving to false) aborts closing; may asynchronously wait for user confirmation. */
18
+ export type RendererCloseGuard = () => boolean | Promise<boolean>;
19
+ export interface RendererIsland {
20
+ /** Identities of deeper islands opened by this plugin to the right of this island. */
21
+ readonly child: string | undefined;
22
+ /** Open a deeper island to the right of this one; replaced islands may refuse through guards. Resolves to whether it opened. */
23
+ open(spec: RendererIslandSpec): Promise<boolean>;
24
+ /** Close this island and deeper islands; has no effect on the host-managed island containing the plugin settings page. force skips guards. */
25
+ close(options?: { force?: boolean }): Promise<boolean>;
26
+ /** Close deeper islands to the right of this one. */
27
+ closeChild(options?: { force?: boolean }): Promise<boolean>;
28
+ /** Register a close guard for this island and return an unregister function. Normally used through useRendererCloseGuard. */
29
+ guard(guard: RendererCloseGuard): () => void;
30
+ }
31
+ /**
32
+ * Panel island frame: the title bar stays fixed and the content scrolls independently; the primary action and close control sit on the right of the title bar.
33
+ * When dirty is true, the host asks for confirmation to discard changes before closing this island, including through ancestor closure or replacement.
34
+ */
35
+ export interface RendererIslandPanelProps {
36
+ title: string;
37
+ description?: string;
38
+ /** Primary title bar action, such as save. */
39
+ action?: ReactNode;
40
+ /** Fixed toolbar below the title bar, such as search. */
41
+ toolbar?: ReactNode;
42
+ dirty?: boolean;
43
+ children: ReactNode;
44
+ }
45
+
46
+ const Island = createContext<RendererIsland | null>(null);
47
+ const Panel = createContext<ComponentType<RendererIslandPanelProps> | null>(null);
48
+ export const RendererIslandProvider = Island.Provider;
49
+ export const RendererIslandPanelProvider = Panel.Provider;
50
+ function required<T>(value: T | null, name: string): T {
51
+ if (value === null) throw new Error(`Renderer ${name} must be provided by the host.`);
52
+ return value;
53
+ }
54
+ /** The containing island; available within plugin settings pages and islands they open. */
55
+ export function useRendererIsland(): RendererIsland { return required(useContext(Island), "island"); }
56
+ /** Register a close guard for the containing island; null guard permits closing, and the guard always uses the latest callback. */
57
+ export function useRendererCloseGuard(guard: RendererCloseGuard | null) {
58
+ const island = useRendererIsland();
59
+ const latest = useRef(guard);
60
+ latest.current = guard;
61
+ useEffect(() => island.guard(() => latest.current ? latest.current() : true), [island]);
62
+ }
63
+ /** Host-rendered panel frame for use within islands opened by plugins. */
64
+ export function IslandPanel(props: RendererIslandPanelProps) {
65
+ return createElement(required(useContext(Panel), "island panel"), props);
66
+ }
@@ -0,0 +1,49 @@
1
+ import { createElement, type ComponentType, type ReactNode } from "react";
2
+ import type { UiText } from "./i18n.ts";
3
+ import type { Json } from "./json.ts";
4
+ import { reference, type ContractReference } from "./presentation.ts";
5
+ import type { RecordContract, SessionRecord } from "./records.ts";
6
+
7
+ /** Host-provided queries: records carry only identities, while the host resolves titles and type names using the full history. */
8
+ export interface RecordRenderContext {
9
+ /** An object's title across the entire history; objects that were removed or belong to other branches can still be resolved. */
10
+ resourceTitle(id: string): string;
11
+ resourceLabel(kind: string): UiText;
12
+ /** Format copy as plain text in the current UI language for composing summaries. */
13
+ text(value: UiText): string;
14
+ /** Contracts wrapping other records borrow the wrapped record's view; return null when unregistered or undecodable. */
15
+ describe(record: SessionRecord): RecordView | null;
16
+ }
17
+ export interface RecordRowProps<D> { data: D; record: SessionRecord; label: string }
18
+ export interface RecordRendererDefinition<D> {
19
+ /** Required plain-text summary for search, tooltips, and accessible names. */
20
+ label(data: D, context: RecordRenderContext, record: SessionRecord): UiText;
21
+ icon?: ReactNode | ((data: D, record: SessionRecord) => ReactNode);
22
+ /** Optional summary row content; the host owns the row shell, including selection, position marker, source, and menu. */
23
+ Row?: ComponentType<RecordRowProps<D>>;
24
+ }
25
+ export interface RecordView {
26
+ label: UiText;
27
+ icon?: ReactNode;
28
+ /** Generate row content from the host-formatted summary; the host displays the summary text when omitted. */
29
+ row?(label: string): ReactNode;
30
+ }
31
+ export interface RecordRenderer {
32
+ contract: ContractReference;
33
+ resolve(record: SessionRecord, context: RecordRenderContext): RecordView | null;
34
+ }
35
+
36
+ /** Declare history presentation by record contract; data enters the view only after contract decoding, otherwise the host displays an unknown record. */
37
+ export function recordRenderer<D extends Json>(contract: RecordContract<D>, definition: RecordRendererDefinition<D>): RecordRenderer {
38
+ return { contract: reference(contract), resolve(record, context) {
39
+ const decoded = contract.decode(record);
40
+ if (decoded?.status !== "known") return null;
41
+ const { data } = decoded;
42
+ const { Row, icon } = definition;
43
+ return {
44
+ label: definition.label(data, context, record),
45
+ icon: typeof icon === "function" ? icon(data, record) : icon,
46
+ row: Row ? label => createElement(Row, { data, record, label }) : undefined,
47
+ };
48
+ } };
49
+ }
@@ -0,0 +1,75 @@
1
+ import { createContext, useContext } from "react";
2
+ import type { Json } from "./json.ts";
3
+ import type { DataContract } from "./presentation.ts";
4
+ import type { ConfigContract } from "./config.ts";
5
+ import type { ModelLiveBlock } from "./models.ts";
6
+ import type { ToolRenderProps, ToolView } from "./renderer-tools.ts";
7
+ import type { EventSubscriber } from "./events.ts";
8
+ import type { ResourceDescription } from "./renderer.ts";
9
+ import type { UiText } from "./i18n.ts";
10
+
11
+ /** Static resource type presentation catalog; querying descriptions grants no access to resource instances. */
12
+ export interface RendererResources {
13
+ list(): readonly ResourceDescription[];
14
+ get(kind: string): ResourceDescription;
15
+ }
16
+ export interface RendererCatalogs { get<T extends Json>(contract: DataContract<T>): T | null; error: UiText | null }
17
+ export interface RendererTools {
18
+ resolve(props: ToolRenderProps): ToolView & { props: ToolRenderProps };
19
+ preparation(name: string): string;
20
+ }
21
+ export interface RendererModels { readonly blocks: readonly ModelLiveBlock[] }
22
+ export interface RendererNavigation { home(): void }
23
+ const Resources = createContext<RendererResources | null>(null);
24
+ const Catalogs = createContext<RendererCatalogs | null>(null);
25
+ const Tools = createContext<RendererTools | null>(null);
26
+ const Models = createContext<RendererModels | null>(null);
27
+ const Navigation = createContext<RendererNavigation | null>(null);
28
+ const Events = createContext<EventSubscriber | null>(null);
29
+ export const RendererEventsProvider = Events.Provider;
30
+ /** Global typed subscriptions; components should close subscriptions they establish during effect cleanup. */
31
+ export function useRendererEvents(): EventSubscriber { return required(useContext(Events), "events"); }
32
+ export const RendererResourcesProvider = Resources.Provider;
33
+ export const RendererCatalogsProvider = Catalogs.Provider;
34
+ export const RendererToolsProvider = Tools.Provider;
35
+ export const RendererModelsProvider = Models.Provider;
36
+ export const RendererNavigationProvider = Navigation.Provider;
37
+ function required<T>(value: T | null, name: string): T {
38
+ if (value === null) throw new Error(`Renderer ${name} must be provided by the host.`);
39
+ return value;
40
+ }
41
+ export function useRendererResources(): RendererResources { return required(useContext(Resources), "resources"); }
42
+ export function useRendererCatalogs(): RendererCatalogs { return required(useContext(Catalogs), "catalogs"); }
43
+ export function useRendererTools(): RendererTools { return required(useContext(Tools), "tools"); }
44
+ export function useRendererModels(): RendererModels { return required(useContext(Models), "models"); }
45
+ export function useRendererNavigation(): RendererNavigation { return required(useContext(Navigation), "navigation"); }
46
+
47
+ /** Renderer access to plugin configuration: the host decodes by contract and updates on change notifications after settings are saved; can read only this plugin's configuration. */
48
+ export interface RendererConfiguration<T extends Json> {
49
+ value: T;
50
+ status: "saved" | "defaults" | "invalid";
51
+ ready: boolean;
52
+ error: UiText | null;
53
+ }
54
+ export interface RendererConfigs {
55
+ get<T extends Json>(contract: ConfigContract<T>): RendererConfiguration<T>;
56
+ save<T extends Json>(contract: ConfigContract<T>, value: NoInfer<T>): Promise<void>;
57
+ reset(contract: ConfigContract): Promise<void>;
58
+ refresh(): Promise<void>;
59
+ }
60
+ const Configs = createContext<RendererConfigs | null>(null);
61
+ export const RendererConfigsProvider = Configs.Provider;
62
+ export function useRendererConfig<T extends Json>(contract: ConfigContract<T>): T {
63
+ return required(useContext(Configs), "configs").get(contract).value;
64
+ }
65
+ /** Custom settings pages use managed configuration; writes are validated in the backend, and successful writes synchronously publish a new snapshot. */
66
+ export function useRendererConfiguration<T extends Json>(contract: ConfigContract<T>) {
67
+ const configs = required(useContext(Configs), "configs");
68
+ return { ...configs.get(contract), save: (value: T) => configs.save(contract, value), reset: () => configs.reset(contract), refresh: configs.refresh };
69
+ }
70
+
71
+ export interface RendererSettingsNavigation { open(pluginId: string, location?: string): void }
72
+ const SettingsNavigation = createContext<RendererSettingsNavigation | null>(null);
73
+ export const RendererSettingsNavigationProvider = SettingsNavigation.Provider;
74
+ /** Open plugin settings without binding the current resource or session. */
75
+ export function useRendererSettings(): RendererSettingsNavigation { return required(useContext(SettingsNavigation), "settings navigation"); }
@@ -0,0 +1,103 @@
1
+ import { createElement, type ComponentType, type ReactNode } from "react";
2
+ import type { z } from "zod";
3
+ import type { UiText } from "./i18n.ts";
4
+ import type { Json } from "./json.ts";
5
+ import { reference, type ContractReference, type DataContract } from "./presentation.ts";
6
+ import type { ToolCompletion, ToolInvocationInfo, ToolResult } from "./tool-result.ts";
7
+
8
+ interface ToolCallProps<T, A> {
9
+ name: string;
10
+ target: T;
11
+ args: A;
12
+ invocation: ToolInvocationInfo;
13
+ }
14
+ /** Partial JSON has not passed the argument contract; fields may be missing, incomplete, or of the wrong type. */
15
+ export interface ToolInputRenderProps extends ToolCallProps<Json, Json> {
16
+ phase: "input";
17
+ result: null;
18
+ }
19
+ /** Waiting for scheduling and execution share a view; complete arguments have passed contract validation. */
20
+ export interface ToolExecutionRenderProps<D = Json, T = Json, A = Json> extends ToolCallProps<T, A> {
21
+ phase: "ready" | "executing";
22
+ result: Extract<ToolResult<D>, { kind: "running" }> | null;
23
+ }
24
+ export interface ToolResultRenderProps<D = Json, T = Json, A = Json> extends ToolCallProps<T, A> {
25
+ phase: "settled";
26
+ result: ToolCompletion<D>;
27
+ }
28
+ export type ToolRenderProps<D = Json, T = Json, A = Json> =
29
+ | ToolInputRenderProps | ToolExecutionRenderProps<D, T, A> | ToolResultRenderProps<D, T, A>;
30
+
31
+ /** All three states use the same two independently optional entry points; the same declaration can be reused across phases. */
32
+ export interface ToolPhaseView<P> {
33
+ headerRenderer?: ComponentType<P>;
34
+ bodyRenderer?: ComponentType<P>;
35
+ }
36
+ /** Input and execution views declare previewable content; bodyRenderer still owns the body layout. */
37
+ export interface ToolStreamingPhaseView<P> extends ToolPhaseView<P> {
38
+ /** Pure function: returns true when a body suitable for streaming preview exists; after the first match within a phase, the shell manages expansion and collapse according to user preferences. */
39
+ streaming?: (props: P) => boolean;
40
+ }
41
+ /** Execution views may additionally declare bodies that require user attention. */
42
+ export interface ToolExecutionPhaseView<P> extends ToolStreamingPhaseView<P> {
43
+ /** Pure function: returns true when the body requires user attention; the shell immediately expands once on the first match during execution, independently of streaming preferences, and users can still collapse it. */
44
+ attention?: (props: P) => boolean;
45
+ }
46
+ export interface ToolRendererDefinition<D = Json, T = Json, A = Json> {
47
+ label?: UiText;
48
+ icon?: ReactNode;
49
+ input?: ToolStreamingPhaseView<ToolInputRenderProps>;
50
+ execution?: ToolExecutionPhaseView<ToolExecutionRenderProps<D, T, A>>;
51
+ result?: ToolPhaseView<ToolResultRenderProps<D, T, A>>;
52
+ }
53
+ export interface ToolView {
54
+ label?: UiText;
55
+ icon?: ReactNode;
56
+ header?: ReactNode;
57
+ body?: ReactNode;
58
+ /** Bodies in input or execution phases may participate in streaming preview expansion and collapse; false means no match this time, and phase completion is determined by the call lifecycle. */
59
+ streaming?: boolean;
60
+ /** The executing body requires user attention; the shell expands once when this first becomes true within the phase. */
61
+ attention?: boolean;
62
+ }
63
+ export interface ToolRenderer {
64
+ contract: ContractReference;
65
+ resolve(props: ToolRenderProps): ToolView | null;
66
+ }
67
+
68
+ function render<P extends { phase: ToolRenderProps["phase"] }>(
69
+ definition: Pick<ToolView, "label" | "icon">, view: ToolExecutionPhaseView<P> | undefined, props: P,
70
+ ): ToolView {
71
+ return {
72
+ label: definition.label, icon: definition.icon,
73
+ header: view?.headerRenderer ? createElement(view.headerRenderer, props) : undefined,
74
+ body: view?.bodyRenderer ? createElement(view.bodyRenderer, props) : undefined,
75
+ streaming: (props.phase === "input" || props.phase === "executing")
76
+ && !!view?.bodyRenderer && (view.streaming?.(props) ?? false),
77
+ attention: props.phase === "executing" && !!view?.bodyRenderer && (view.attention?.(props) ?? false),
78
+ };
79
+ }
80
+
81
+ /** Dispatch by phase; streaming input is not treated as valid arguments, while execution and results share complete argument and output validation. */
82
+ export function toolRenderer<D extends Json, T extends Json = Json, A extends Json = Json>(
83
+ contract: DataContract<D> & { targetSchema?: z.ZodType<T>; tool?: { argsSchema: z.ZodType<A> } },
84
+ definition: ToolRendererDefinition<D, T, A>,
85
+ ): ToolRenderer {
86
+ return { contract: reference(contract), resolve(props) {
87
+ if (props.phase === "input") return definition.input ? render(definition, definition.input, props) : null;
88
+ const data = props.result?.data;
89
+ const parsed = data === null || data === undefined ? null : contract.schema.safeParse(data);
90
+ if (parsed && !parsed.success) return null;
91
+ const target = contract.targetSchema?.safeParse(props.target);
92
+ const args = contract.tool?.argsSchema.safeParse(props.args);
93
+ if (target && !target.success || args && !args.success) return null;
94
+ // Pure data contracts without an argument schema preserve raw JSON; type erasure occurs only at the registration boundary.
95
+ const input = { target: (target ? target.data : props.target) as T, args: (args ? args.data : props.args) as A };
96
+ if (props.phase === "settled") return render(definition, definition.result, {
97
+ ...props, ...input, result: { ...props.result, data: parsed?.data ?? null },
98
+ });
99
+ return render(definition, definition.execution, {
100
+ ...props, ...input, result: props.result ? { ...props.result, data: parsed?.data ?? null } : null,
101
+ });
102
+ } };
103
+ }
@@ -0,0 +1,33 @@
1
+ import { createContext, createElement, useContext, useState, type ReactNode } from "react";
2
+
3
+ /**
4
+ * List item view state: items in virtualized lists unmount and remount as they scroll; user-modified expansion, tabs, and other
5
+ * state are stored per item in storage supplied by the list owner and restored on remount. Outside an item scope, this behaves like component state.
6
+ */
7
+ export type RendererViewStateEntry = Map<string, unknown>;
8
+ const Entry = createContext<RendererViewStateEntry | null>(null);
9
+
10
+ /** The list owner provides stable storage for each item; the same instance must be passed when that item remounts. */
11
+ export function RendererViewStateProvider({ value, children }: { value: RendererViewStateEntry; children?: ReactNode }) {
12
+ return createElement(Entry.Provider, { value }, children);
13
+ }
14
+
15
+ /**
16
+ * Read and write one view state value within the current item. key is unique within the item and prefixed with the plugin ID to avoid conflicts with other contributors;
17
+ * when key is null, this is only component state and does not survive remounts. initial is used only when the item has no such value yet.
18
+ */
19
+ export function useRendererViewState<T>(key: string | null, initial: T | (() => T)): [T, (value: T) => void] {
20
+ const entry = useContext(Entry);
21
+ const stored = key !== null && entry !== null;
22
+ const [value, setLocal] = useState<T>(() => {
23
+ if (stored && entry.has(key)) return entry.get(key) as T;
24
+ const created = typeof initial === "function" ? (initial as () => T)() : initial;
25
+ if (stored) entry.set(key, created);
26
+ return created;
27
+ });
28
+ const set = (next: T) => {
29
+ if (stored) entry.set(key, next);
30
+ setLocal(next);
31
+ };
32
+ return [value, set];
33
+ }
@@ -0,0 +1,158 @@
1
+ /** SDK renderer entry point: cards, tool views, and frontend capabilities bound to objects; React is shared by the host. */
2
+ import { createContext, createElement, useContext, type ComponentType, type ReactNode } from "react";
3
+ import type { UiText } from "./i18n.ts";
4
+ import type { CardAppearance } from "./card.ts";
5
+ export type { CardAppearance } from "./card.ts";
6
+ export * from "./renderer-i18n.ts";
7
+ import type { ActionContract, CreationContract } from "./commands.ts";
8
+ import type { Json } from "./json.ts";
9
+ import { reference, type ContractReference, type DataContract } from "./presentation.ts";
10
+ import type { ResourceRef, SessionSnapshot } from "./workspace.ts";
11
+ export * from "./renderer-services.ts";
12
+ export * from "./renderer-calls.ts";
13
+ export * from "./renderer-creation.ts";
14
+ export * from "./renderer-tools.ts";
15
+ export * from "./renderer-records.ts";
16
+ export * from "./renderer-card.ts";
17
+ export * from "./renderer-island.ts";
18
+ export * from "./renderer-view-state.ts";
19
+ import type { CardPanelDefinition, CardPanelView } from "./renderer-card.ts";
20
+ import type { RecordRenderer } from "./renderer-records.ts";
21
+ export type * from "./renderer-content.ts";
22
+ import type { RendererContentInput, RendererInputOptions } from "./renderer-content.ts";
23
+ import type { ContentReader } from "./content-runtime.ts";
24
+ export type { ContentReader } from "./content-runtime.ts";
25
+ import type { CreationEntry } from "./renderer-creation.ts";
26
+ import type { ToolRenderer } from "./renderer-tools.ts";
27
+ import type { EventSubscriber } from "./events.ts";
28
+ import type { PluginCalls } from "./calls.ts";
29
+ import type { ChannelContract, ChannelEnd, ChannelValue } from "./channels.ts";
30
+
31
+ /** The frontend end of a channel: messages are delivered in order and buffered until a listener registers; closed resolves when either end terminates. */
32
+ export interface RendererChannel<Up extends ChannelValue = ChannelValue, Down extends ChannelValue = ChannelValue> {
33
+ send(data: Up): void;
34
+ onMessage(listener: (data: Down) => void): () => void;
35
+ close(): void;
36
+ readonly closed: Promise<ChannelEnd>;
37
+ }
38
+
39
+ export type RendererReceipt = { ok: true } | { ok: false; error: { code: string; message: string; display?: UiText } };
40
+ /**
41
+ * Frontend state retained per object, such as input drafts: survives card remounts, switches between focus containers, and session view disposal;
42
+ * release is called when the object leaves the current session projection through removal or a cursor jump, or is explicitly discarded; state is not restored after application restart.
43
+ */
44
+ export interface RendererRetained {
45
+ /** Obtain the value for key, creating it with create on first access; the same object and key return the same instance. Prefix key with the plugin ID to avoid conflicts with other contributors. */
46
+ get<T>(key: string, create: () => T, release?: (value: T) => void | Promise<void>): T;
47
+ /** End retention immediately and call release; no effect if absent. */
48
+ discard(key: string): void;
49
+ }
50
+ /** Frontend capabilities bound to a card object; the host handles subscriptions, command gating, and current-view consistency. */
51
+ export interface RendererObjectContext {
52
+ readonly ref: ResourceRef;
53
+ readonly content: ContentReader;
54
+ input<I, O extends Json>(contract: ActionContract<I, O> | CreationContract<I, Json, O>): RendererContentInput;
55
+ readonly view: {
56
+ /** The current object view is operable; ordinary background refreshes and local commits on other objects do not change this value. */
57
+ ready: boolean;
58
+ /** View read error; object action and creation failures are returned through their respective call receipts. */
59
+ error: UiText | null; missing: boolean;
60
+ };
61
+ readonly session: {
62
+ snapshot(): SessionSnapshot | null;
63
+ subscribe(listener: () => void): () => void;
64
+ create<I, O extends Json>(contract: CreationContract<I, Json, O>, input: NoInfer<I> & Json, position: { x: number; y: number }, options?: RendererInputOptions): Promise<RendererReceipt>;
65
+ refresh(): Promise<void>;
66
+ };
67
+ invoke<I, O extends Json>(contract: ActionContract<I, O>, input: NoInfer<I> & Json, options?: RendererInputOptions): Promise<RendererReceipt>;
68
+ /** Open a duplex channel for this object; the host validates that it is on the current path and routes to its owning plugin. Ends when the object leaves the current set or the connection disconnects. */
69
+ openChannel<Up extends ChannelValue, Down extends ChannelValue>(contract: ChannelContract<Up, Down>): RendererChannel<Up, Down>;
70
+ /** Object-level retained state with a lifecycle managed uniformly by the host. */
71
+ readonly retained: RendererRetained;
72
+ }
73
+ const ObjectContext = createContext<RendererObjectContext | null>(null);
74
+ export function useRendererObject(): RendererObjectContext {
75
+ const object = useContext(ObjectContext);
76
+ if (!object) throw new Error("The renderer must be bound to an object.");
77
+ return object;
78
+ }
79
+
80
+ /** Static identification information at the type level; does not parse card data or create body components. */
81
+ export interface ResourceDescription {
82
+ kind: string;
83
+ label: UiText;
84
+ icon: ReactNode;
85
+ /** Default card dimensions available before creation, sharing a declaration with the card renderer. */
86
+ card?: CardAppearance;
87
+ /** This resource can be the endpoint of a directed link, matching backend links.receive. */
88
+ receivesLinks?: boolean;
89
+ }
90
+ export interface CardView extends CardAppearance {
91
+ body: ReactNode;
92
+ actions?: ReactNode;
93
+ panels?: readonly CardPanelView[];
94
+ }
95
+ export interface CardRenderer {
96
+ contract: ContractReference;
97
+ resolve(data: Json, context?: RendererObjectContext): CardView | null;
98
+ }
99
+ export interface RendererModule {
100
+ resources: ResourceDescription[];
101
+ cards: CardRenderer[];
102
+ tools: ToolRenderer[];
103
+ /** History record views: declare summaries, icons, and optional row content by record contract. */
104
+ records?: RecordRenderer[];
105
+ /** Creation entry points: the host arbitrates menus and drag-and-drop uniformly, with all paths ultimately using the bound creation contract. */
106
+ creations?: CreationEntry[];
107
+ /** Startup preparation: the host invokes modules in parallel before the first screen, and modules initialize their own internal resources; timeouts or failures are logged without blocking startup. */
108
+ prepare?(context: RendererPrepareContext): Promise<void>;
109
+ }
110
+ export interface PluginSettingsPage {
111
+ /** A directly addressable location within a page; an empty location means the root page. */
112
+ locations?: readonly string[];
113
+ Body: ComponentType<{ location?: string }>;
114
+ }
115
+ /** Frontend plugins own settings pages and shared service lifecycles; feature views are composed from modules. */
116
+ export interface RendererPlugin {
117
+ modules?: readonly RendererModule[];
118
+ settings?: PluginSettingsPage;
119
+ prepare?(context: RendererPrepareContext): Promise<void>;
120
+ closing?(): void | Promise<void>;
121
+ }
122
+ export function defineRendererPlugin(plugin: RendererPlugin): RendererPlugin { return plugin; }
123
+ export interface RendererPrepareContext {
124
+ /** Typed calls for frontend module-level services; cancellation propagates with the preparation scope. */
125
+ readonly calls: PluginCalls;
126
+ /** Global subscriptions bound to the module lifecycle, cleaned up on failure, timeout, or application shutdown. */
127
+ readonly events: EventSubscriber;
128
+ /** Triggered when the host stops waiting due to timeout or shutdown; the module should stop promptly. */
129
+ readonly signal: AbortSignal;
130
+ }
131
+
132
+ /** Type erasure occurs only at the registration boundary; components receive data that has passed contract validation. */
133
+ export function cardRenderer<T extends Json>(
134
+ contract: DataContract<T>,
135
+ definition: CardAppearance & {
136
+ Body: ComponentType<{ data: T }>;
137
+ Actions?: ComponentType<{ data: T }>;
138
+ panels?: readonly CardPanelDefinition<T>[];
139
+ },
140
+ ): CardRenderer {
141
+ const { Body, Actions, panels, ...appearance } = definition;
142
+ const ids = new Set<string>();
143
+ for (const panel of panels ?? []) {
144
+ if (!panel.id.trim() || panel.id !== panel.id.trim()) throw new Error("Card panel IDs must be non-empty and trimmed.");
145
+ if (ids.has(panel.id)) throw new Error(`Duplicate card panel ID: ${panel.id}`);
146
+ ids.add(panel.id);
147
+ }
148
+ return { contract: reference(contract), resolve(raw, context) {
149
+ const parsed = contract.schema.safeParse(raw);
150
+ if (!parsed.success) return null;
151
+ const bind = (node: ReactNode) => context ? createElement(ObjectContext.Provider, { value: context }, node) : node;
152
+ return { ...appearance, body: bind(createElement(Body, { data: parsed.data })),
153
+ actions: Actions ? bind(createElement(Actions, { data: parsed.data })) : undefined,
154
+ panels: panels?.map(({ Body: PanelBody, ...panel }) => ({
155
+ ...panel, body: bind(createElement(PanelBody, { data: parsed.data })),
156
+ })) };
157
+ } };
158
+ }
@@ -0,0 +1,20 @@
1
+ import type { CallContract, CallScope } from "./calls.ts";
2
+ import type { z } from "zod";
3
+ import type { Json } from "./json.ts";
4
+ import type { ObjectReader } from "./objects.ts";
5
+ import type { Source } from "./workspace.ts";
6
+
7
+ export type CallContext<S extends CallScope = CallScope> = {
8
+ /** The host binds the source for backend plugin calls; null for desktop user calls. */
9
+ readonly source: Source;
10
+ readonly signal: AbortSignal;
11
+ } & (S extends "object" ? { readonly object: ObjectReader } : { readonly object?: never });
12
+ export interface CallContribution {
13
+ readonly contract: CallContract;
14
+ execute(input: Json, context: CallContext): Promise<Json>;
15
+ }
16
+ /** Implementations receive parsed input and return JSON accepted by the output schema before caller-side decoding. */
17
+ export function implementCall<Input extends z.ZodType<Json>, Output extends z.ZodType<Json>, S extends CallScope>(contract: CallContract<Input, Output, S>,
18
+ execute: (input: NoInfer<z.output<Input>>, context: CallContext<NoInfer<S>>) => Promise<NoInfer<z.input<Output>> & Json>): CallContribution {
19
+ return { contract, execute: (input, context) => execute(input as z.output<Input>, context as CallContext<S>) };
20
+ }