@digitornai/sdk 0.1.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/LICENSE +201 -0
- package/dist/agentAction.d.ts +52 -0
- package/dist/agentAction.js +53 -0
- package/dist/agentContext.d.ts +97 -0
- package/dist/agentContext.js +184 -0
- package/dist/agentSnapshot.d.ts +28 -0
- package/dist/agentSnapshot.js +54 -0
- package/dist/compile.d.ts +31 -0
- package/dist/compile.js +48 -0
- package/dist/connect.d.ts +11 -0
- package/dist/connect.js +89 -0
- package/dist/hooks.d.ts +24 -0
- package/dist/hooks.js +41 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +23 -0
- package/dist/previewAttach.d.ts +37 -0
- package/dist/previewAttach.js +62 -0
- package/dist/provider.d.ts +48 -0
- package/dist/provider.js +182 -0
- package/dist/reducer.d.ts +14 -0
- package/dist/reducer.js +181 -0
- package/dist/reviewMode.d.ts +33 -0
- package/dist/reviewMode.js +59 -0
- package/dist/shared.d.ts +24 -0
- package/dist/shared.js +55 -0
- package/dist/topbar.d.ts +78 -0
- package/dist/topbar.js +89 -0
- package/dist/types.d.ts +94 -0
- package/dist/types.js +10 -0
- package/dist/workspace.d.ts +81 -0
- package/dist/workspace.js +187 -0
- package/package.json +37 -0
package/dist/reducer.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
const MAX_EVENTS = 500;
|
|
2
|
+
function messageText(m) {
|
|
3
|
+
if (!m)
|
|
4
|
+
return "";
|
|
5
|
+
if (typeof m.content === "string" && m.content)
|
|
6
|
+
return m.content;
|
|
7
|
+
if (Array.isArray(m.parts)) {
|
|
8
|
+
return m.parts.map((p) => p.text ?? p.content ?? "").join("");
|
|
9
|
+
}
|
|
10
|
+
return "";
|
|
11
|
+
}
|
|
12
|
+
function ensureStreamingAssistant(messages) {
|
|
13
|
+
const last = messages[messages.length - 1];
|
|
14
|
+
if (last && last.role === "assistant" && last.streaming) {
|
|
15
|
+
return { messages, tail: last };
|
|
16
|
+
}
|
|
17
|
+
const fresh = {
|
|
18
|
+
role: "assistant",
|
|
19
|
+
content: "",
|
|
20
|
+
streaming: true,
|
|
21
|
+
toolCalls: [],
|
|
22
|
+
ts: Date.now(),
|
|
23
|
+
};
|
|
24
|
+
return { messages: [...messages, fresh], tail: fresh };
|
|
25
|
+
}
|
|
26
|
+
function replaceTail(messages, next) {
|
|
27
|
+
return [...messages.slice(0, -1), next];
|
|
28
|
+
}
|
|
29
|
+
function applyEvent(state, env) {
|
|
30
|
+
const lastSeq = env.seq > state.lastSeq ? env.seq : state.lastSeq;
|
|
31
|
+
const events = [...state.events, env].slice(-MAX_EVENTS);
|
|
32
|
+
const base = { ...state, lastSeq, events };
|
|
33
|
+
switch (env.type) {
|
|
34
|
+
case "user_message": {
|
|
35
|
+
const cid = env.correlation_id;
|
|
36
|
+
if (cid && state.messages.some((m) => m.correlationId === cid)) {
|
|
37
|
+
return base;
|
|
38
|
+
}
|
|
39
|
+
const msg = {
|
|
40
|
+
role: "user",
|
|
41
|
+
content: messageText(env.message),
|
|
42
|
+
correlationId: cid,
|
|
43
|
+
ts: Date.now(),
|
|
44
|
+
};
|
|
45
|
+
return { ...base, messages: [...state.messages, msg] };
|
|
46
|
+
}
|
|
47
|
+
case "turn_started":
|
|
48
|
+
case "message_started":
|
|
49
|
+
return { ...base, turnActive: true, status: "streaming", error: null };
|
|
50
|
+
case "assistant_delta": {
|
|
51
|
+
const { messages, tail } = ensureStreamingAssistant(state.messages);
|
|
52
|
+
const next = { ...tail, content: tail.content + messageText(env.message) };
|
|
53
|
+
return {
|
|
54
|
+
...base,
|
|
55
|
+
status: "streaming",
|
|
56
|
+
messages: replaceTail(messages, next),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
case "assistant_reasoning_delta": {
|
|
60
|
+
const { messages, tail } = ensureStreamingAssistant(state.messages);
|
|
61
|
+
const delta = env.message?.reasoning ?? messageText(env.message);
|
|
62
|
+
const next = { ...tail, reasoning: (tail.reasoning ?? "") + delta };
|
|
63
|
+
return {
|
|
64
|
+
...base,
|
|
65
|
+
status: "thinking",
|
|
66
|
+
messages: replaceTail(messages, next),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
case "assistant_message":
|
|
70
|
+
case "message_done": {
|
|
71
|
+
const { messages, tail } = ensureStreamingAssistant(state.messages);
|
|
72
|
+
const full = messageText(env.message);
|
|
73
|
+
const next = {
|
|
74
|
+
...tail,
|
|
75
|
+
content: full || tail.content,
|
|
76
|
+
reasoning: env.message?.reasoning ?? tail.reasoning,
|
|
77
|
+
streaming: false,
|
|
78
|
+
};
|
|
79
|
+
return { ...base, messages: replaceTail(messages, next) };
|
|
80
|
+
}
|
|
81
|
+
case "tool_call": {
|
|
82
|
+
if (!env.tool)
|
|
83
|
+
return base;
|
|
84
|
+
const { messages, tail } = ensureStreamingAssistant(state.messages);
|
|
85
|
+
const call = {
|
|
86
|
+
callId: env.tool.call_id,
|
|
87
|
+
name: env.tool.name,
|
|
88
|
+
args: env.tool.arguments,
|
|
89
|
+
status: env.tool.status ?? "running",
|
|
90
|
+
};
|
|
91
|
+
const next = { ...tail, toolCalls: [...(tail.toolCalls ?? []), call] };
|
|
92
|
+
return {
|
|
93
|
+
...base,
|
|
94
|
+
status: "tool_use",
|
|
95
|
+
messages: replaceTail(messages, next),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
case "tool_result": {
|
|
99
|
+
if (!env.tool)
|
|
100
|
+
return base;
|
|
101
|
+
const messages = state.messages.map((m) => {
|
|
102
|
+
if (m.role !== "assistant" || !m.toolCalls)
|
|
103
|
+
return m;
|
|
104
|
+
return {
|
|
105
|
+
...m,
|
|
106
|
+
toolCalls: m.toolCalls.map((c) => c.callId === env.tool.call_id
|
|
107
|
+
? { ...c, status: env.tool.status ?? "done", result: env.tool.text }
|
|
108
|
+
: c),
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
return { ...base, messages };
|
|
112
|
+
}
|
|
113
|
+
case "approval_request": {
|
|
114
|
+
if (!env.approval)
|
|
115
|
+
return base;
|
|
116
|
+
const a = env.approval;
|
|
117
|
+
if (state.approvals.some((x) => x.id === a.id))
|
|
118
|
+
return base;
|
|
119
|
+
return {
|
|
120
|
+
...base,
|
|
121
|
+
approvals: [
|
|
122
|
+
...state.approvals,
|
|
123
|
+
{ id: a.id, kind: a.kind, payload: a.payload, status: a.status },
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
case "approval_granted":
|
|
128
|
+
case "approval_denied": {
|
|
129
|
+
const id = env.approval?.id;
|
|
130
|
+
if (!id)
|
|
131
|
+
return base;
|
|
132
|
+
return {
|
|
133
|
+
...base,
|
|
134
|
+
approvals: state.approvals.filter((x) => x.id !== id),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
case "turn_ended": {
|
|
138
|
+
const last = state.messages[state.messages.length - 1];
|
|
139
|
+
const messages = last && last.role === "assistant" && last.streaming
|
|
140
|
+
? replaceTail(state.messages, { ...last, streaming: false })
|
|
141
|
+
: state.messages;
|
|
142
|
+
return { ...base, turnActive: false, status: "idle", messages };
|
|
143
|
+
}
|
|
144
|
+
case "error":
|
|
145
|
+
return {
|
|
146
|
+
...base,
|
|
147
|
+
status: "error",
|
|
148
|
+
turnActive: false,
|
|
149
|
+
error: env.error?.message ?? "Unknown error",
|
|
150
|
+
};
|
|
151
|
+
default:
|
|
152
|
+
return base;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
export function reduce(state, action) {
|
|
156
|
+
switch (action.type) {
|
|
157
|
+
case "connected":
|
|
158
|
+
return { ...state, connected: true, error: null };
|
|
159
|
+
case "disconnected":
|
|
160
|
+
return { ...state, connected: false };
|
|
161
|
+
case "local_user":
|
|
162
|
+
return {
|
|
163
|
+
...state,
|
|
164
|
+
turnActive: true,
|
|
165
|
+
status: "streaming",
|
|
166
|
+
messages: [
|
|
167
|
+
...state.messages,
|
|
168
|
+
{
|
|
169
|
+
role: "user",
|
|
170
|
+
content: action.content,
|
|
171
|
+
correlationId: action.correlationId,
|
|
172
|
+
ts: Date.now(),
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
};
|
|
176
|
+
case "event":
|
|
177
|
+
return applyEvent(state, action.env);
|
|
178
|
+
default:
|
|
179
|
+
return state;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** A review command the host relays to the app (from the chat, or the app's own
|
|
2
|
+
* chrome). `accept`/`reject` act on one proposal by `id`; the `*-all` variants
|
|
3
|
+
* act on every pending one. */
|
|
4
|
+
export interface ReviewCommand {
|
|
5
|
+
/** `*-all`: every pending change. `next`/`prev`: move to and focus the
|
|
6
|
+
* neighbouring change (one-by-one review). `accept-current`/`reject-current`:
|
|
7
|
+
* resolve the change currently focused/at the cursor. `accept`/`reject`: a
|
|
8
|
+
* specific change by `id`. */
|
|
9
|
+
action: "accept-all" | "reject-all" | "next" | "prev" | "accept-current" | "reject-current" | "accept" | "reject";
|
|
10
|
+
/** Proposal id for `accept`/`reject`; ignored by the others. */
|
|
11
|
+
id?: string;
|
|
12
|
+
}
|
|
13
|
+
/** State an app publishes so the host can surface a review affordance ("12
|
|
14
|
+
* suggestions — Accept all / Review"). */
|
|
15
|
+
export interface ProposalsState {
|
|
16
|
+
/** Pending proposals awaiting the person's decision. 0 clears the affordance. */
|
|
17
|
+
count: number;
|
|
18
|
+
/** Optional label for the current review context ("12 suggestions",
|
|
19
|
+
* "3 changes in this section"). The host may show it verbatim. */
|
|
20
|
+
label?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Report the current pending-proposals state to the host. Call it whenever the
|
|
24
|
+
* count changes (a suggestion lands, one is accepted/rejected). `count: 0`
|
|
25
|
+
* removes the chat's review affordance. Vanilla — safe from any editor.
|
|
26
|
+
*/
|
|
27
|
+
export declare function reportProposals(state: ProposalsState): void;
|
|
28
|
+
/**
|
|
29
|
+
* Subscribe to review commands the host relays (Accept all / Reject all / accept
|
|
30
|
+
* one / reject one). The app applies them in its own engine (e.g. SuperDoc
|
|
31
|
+
* acceptAll). Returns an unsubscribe function. Vanilla — usable outside React.
|
|
32
|
+
*/
|
|
33
|
+
export declare function onReviewCommand(handler: (cmd: ReviewCommand) => void): () => void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// The Digitorn "review" primitive — the generic channel for "the assistant
|
|
2
|
+
// PROPOSES changes the person accepts or rejects", reusable by ANY app (a docx
|
|
3
|
+
// editor's tracked changes, a spreadsheet's proposed cells, a slide edit, a
|
|
4
|
+
// code diff). The SDK knows nothing app-specific: an app REPORTS how many
|
|
5
|
+
// proposals are pending, and RECEIVES accept/reject commands the host relays
|
|
6
|
+
// (e.g. from an "Accept all" button in the chat). How proposals are produced
|
|
7
|
+
// and rendered is entirely the app's business.
|
|
8
|
+
//
|
|
9
|
+
// Pairs with Digitorn's native mode system: an app exposes an "Auto" and a
|
|
10
|
+
// "Suggestions" mode (app.yaml), whose per-mode system_prompt tells the agent
|
|
11
|
+
// whether to edit directly or as tracked changes. This channel is only the
|
|
12
|
+
// review surface on top of that.
|
|
13
|
+
//
|
|
14
|
+
// No React import at module scope, so a vanilla (non-React) editor can import
|
|
15
|
+
// `reportProposals` / `onReviewCommand` directly.
|
|
16
|
+
/**
|
|
17
|
+
* Report the current pending-proposals state to the host. Call it whenever the
|
|
18
|
+
* count changes (a suggestion lands, one is accepted/rejected). `count: 0`
|
|
19
|
+
* removes the chat's review affordance. Vanilla — safe from any editor.
|
|
20
|
+
*/
|
|
21
|
+
export function reportProposals(state) {
|
|
22
|
+
if (typeof window === "undefined")
|
|
23
|
+
return;
|
|
24
|
+
const count = Math.max(0, Math.floor(Number(state.count) || 0));
|
|
25
|
+
try {
|
|
26
|
+
window.parent?.postMessage({
|
|
27
|
+
type: "digi:proposals-state",
|
|
28
|
+
count,
|
|
29
|
+
label: typeof state.label === "string" ? state.label.slice(0, 80) : undefined,
|
|
30
|
+
}, "*");
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
/* cross-origin parent may reject — nothing to fall back to */
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Subscribe to review commands the host relays (Accept all / Reject all / accept
|
|
38
|
+
* one / reject one). The app applies them in its own engine (e.g. SuperDoc
|
|
39
|
+
* acceptAll). Returns an unsubscribe function. Vanilla — usable outside React.
|
|
40
|
+
*/
|
|
41
|
+
export function onReviewCommand(handler) {
|
|
42
|
+
if (typeof window === "undefined")
|
|
43
|
+
return () => { };
|
|
44
|
+
const listener = (ev) => {
|
|
45
|
+
const d = ev.data;
|
|
46
|
+
if (!d || typeof d !== "object" || d.type !== "digi:review-command")
|
|
47
|
+
return;
|
|
48
|
+
const action = d.action;
|
|
49
|
+
const valid = [
|
|
50
|
+
"accept-all", "reject-all", "next", "prev",
|
|
51
|
+
"accept-current", "reject-current", "accept", "reject",
|
|
52
|
+
];
|
|
53
|
+
if (!valid.includes(action))
|
|
54
|
+
return;
|
|
55
|
+
handler({ action, id: typeof d.id === "string" ? d.id : undefined });
|
|
56
|
+
};
|
|
57
|
+
window.addEventListener("message", listener);
|
|
58
|
+
return () => window.removeEventListener("message", listener);
|
|
59
|
+
}
|
package/dist/shared.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface SharedDocOptions {
|
|
2
|
+
/** Debounce (ms) before writing a local change back to the workdir.
|
|
3
|
+
* Default 200. High-frequency editors (Excalidraw, canvases) benefit
|
|
4
|
+
* from a slightly higher value. */
|
|
5
|
+
writeDebounceMs?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* `useSharedDoc` - convenience for the common "one JSON file" case: a workdir
|
|
9
|
+
* document synced two-way with the agent. It is just sugar over `useWorkspace`
|
|
10
|
+
* + `useWatch`.
|
|
11
|
+
*
|
|
12
|
+
* - The agent edits the file → `useWatch` fires → we re-read → re-render.
|
|
13
|
+
* - `setDoc(next)` writes it back (debounced) so the agent sees user edits.
|
|
14
|
+
*
|
|
15
|
+
* Echo from your own writes is suppressed so you never loop. Apps whose state
|
|
16
|
+
* is NOT a single JSON (a folder, many files, binary…) use `useWatch` +
|
|
17
|
+
* `useWorkspace` directly instead.
|
|
18
|
+
*
|
|
19
|
+
* @example Excalidraw - the agent draws in real time
|
|
20
|
+
* ```tsx
|
|
21
|
+
* const [scene, setScene] = useSharedDoc("scene.excalidraw", { elements: [] });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function useSharedDoc<T = unknown>(path: string, initial?: T, options?: SharedDocOptions): [T | undefined, (next: T) => void];
|
package/dist/shared.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { useWorkspace, useWatch } from "./workspace.js";
|
|
3
|
+
/**
|
|
4
|
+
* `useSharedDoc` - convenience for the common "one JSON file" case: a workdir
|
|
5
|
+
* document synced two-way with the agent. It is just sugar over `useWorkspace`
|
|
6
|
+
* + `useWatch`.
|
|
7
|
+
*
|
|
8
|
+
* - The agent edits the file → `useWatch` fires → we re-read → re-render.
|
|
9
|
+
* - `setDoc(next)` writes it back (debounced) so the agent sees user edits.
|
|
10
|
+
*
|
|
11
|
+
* Echo from your own writes is suppressed so you never loop. Apps whose state
|
|
12
|
+
* is NOT a single JSON (a folder, many files, binary…) use `useWatch` +
|
|
13
|
+
* `useWorkspace` directly instead.
|
|
14
|
+
*
|
|
15
|
+
* @example Excalidraw - the agent draws in real time
|
|
16
|
+
* ```tsx
|
|
17
|
+
* const [scene, setScene] = useSharedDoc("scene.excalidraw", { elements: [] });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function useSharedDoc(path, initial, options = {}) {
|
|
21
|
+
const { readFile, writeFile } = useWorkspace();
|
|
22
|
+
const [value, setValue] = React.useState(initial);
|
|
23
|
+
// The exact bytes we last wrote - a change event echoing our own write is
|
|
24
|
+
// ignored so a write→event→read→setState loop never fires.
|
|
25
|
+
const lastWritten = React.useRef(null);
|
|
26
|
+
const read = React.useCallback(async () => {
|
|
27
|
+
const text = await readFile(path);
|
|
28
|
+
if (text === undefined)
|
|
29
|
+
return; // missing / unreadable - keep current
|
|
30
|
+
if (text === lastWritten.current)
|
|
31
|
+
return; // our own echo
|
|
32
|
+
try {
|
|
33
|
+
setValue(text ? JSON.parse(text) : undefined);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
/* invalid JSON mid-write - keep current, next event retries */
|
|
37
|
+
}
|
|
38
|
+
}, [readFile, path]);
|
|
39
|
+
React.useEffect(() => {
|
|
40
|
+
void read();
|
|
41
|
+
}, [read]);
|
|
42
|
+
useWatch(path, () => void read());
|
|
43
|
+
const timer = React.useRef(null);
|
|
44
|
+
const setDoc = React.useCallback((next) => {
|
|
45
|
+
setValue(next);
|
|
46
|
+
const content = JSON.stringify(next);
|
|
47
|
+
if (timer.current)
|
|
48
|
+
clearTimeout(timer.current);
|
|
49
|
+
timer.current = setTimeout(() => {
|
|
50
|
+
lastWritten.current = content;
|
|
51
|
+
void writeFile(path, content);
|
|
52
|
+
}, options.writeDebounceMs ?? 200);
|
|
53
|
+
}, [writeFile, path, options.writeDebounceMs]);
|
|
54
|
+
return [value, setDoc];
|
|
55
|
+
}
|
package/dist/topbar.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A button (or a small dropdown, via ``items``) the app adds to the host's
|
|
3
|
+
* top bar - the same bar Digitorn renders its own Code/Preview tabs and the
|
|
4
|
+
* GitHub/Publish buttons into. Only shown while THIS custom view is the
|
|
5
|
+
* active tab; switching away removes it automatically.
|
|
6
|
+
*
|
|
7
|
+
* Only ``label``/``icon`` cross the postMessage boundary - the host owns
|
|
8
|
+
* the actual chrome. ``onClick`` stays in your own code and never leaves
|
|
9
|
+
* the iframe; the host just tells you which id was clicked.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Bounded, themed style knobs for one button - not raw CSS. Every value is
|
|
13
|
+
* an enum the host maps to its own design tokens, so a view can stand its
|
|
14
|
+
* button out (a prominent "Deploy" CTA, a compact icon-only refresh) while
|
|
15
|
+
* staying visually native to Digitorn's chrome - and without a "data only"
|
|
16
|
+
* postMessage payload turning into arbitrary styling/markup control over
|
|
17
|
+
* the host's own page.
|
|
18
|
+
*/
|
|
19
|
+
export interface TopBarActionStyle {
|
|
20
|
+
/** Button height/padding/text size. Default `"md"`. */
|
|
21
|
+
size?: "sm" | "md" | "lg";
|
|
22
|
+
/** Text weight. Default `"medium"`. */
|
|
23
|
+
weight?: "normal" | "medium" | "semibold" | "bold";
|
|
24
|
+
/** Corner rounding. Default `"rounded"`. */
|
|
25
|
+
shape?: "square" | "rounded" | "pill";
|
|
26
|
+
/** `"ghost"` (default, blends into the bar), `"outline"` (bordered), or
|
|
27
|
+
* `"solid"` (filled - reach for this on the one action that should read
|
|
28
|
+
* as the primary CTA, e.g. a "Deploy" button). */
|
|
29
|
+
variant?: "ghost" | "outline" | "solid";
|
|
30
|
+
/** `"default"` (neutral) or `"accent"` (Digitorn's brand color). */
|
|
31
|
+
tone?: "default" | "accent";
|
|
32
|
+
}
|
|
33
|
+
export interface TopBarAction {
|
|
34
|
+
id: string;
|
|
35
|
+
label: string;
|
|
36
|
+
/** A lucide icon name, e.g. "git-branch". Falls back to a generic icon. */
|
|
37
|
+
icon?: string;
|
|
38
|
+
onClick?: () => void;
|
|
39
|
+
/**
|
|
40
|
+
* Opens this URL in a new tab instead of relaying a click - `onClick` is
|
|
41
|
+
* ignored when this is set. Popups a host-relayed `onClick` opens with
|
|
42
|
+
* `window.open` get blocked: the click happens in the host's real DOM,
|
|
43
|
+
* but `window.open` would run later inside the iframe on a postMessage,
|
|
44
|
+
* which browsers don't count as the same user gesture. The host opens
|
|
45
|
+
* `href` itself, synchronously, in the click it actually received.
|
|
46
|
+
*/
|
|
47
|
+
href?: string;
|
|
48
|
+
style?: TopBarActionStyle;
|
|
49
|
+
/** Turns this entry into a small dropdown instead of a plain button. */
|
|
50
|
+
items?: {
|
|
51
|
+
id: string;
|
|
52
|
+
label: string;
|
|
53
|
+
icon?: string;
|
|
54
|
+
onClick?: () => void;
|
|
55
|
+
href?: string;
|
|
56
|
+
style?: TopBarActionStyle;
|
|
57
|
+
}[];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Register this view's top-bar actions with the host. Call with an empty
|
|
61
|
+
* array (or unmount) to clear them - e.g. because the current screen inside
|
|
62
|
+
* your app doesn't have any right now.
|
|
63
|
+
*
|
|
64
|
+
* ```tsx
|
|
65
|
+
* useTopBarActions([
|
|
66
|
+
* { id: "clone", label: "Clone", icon: "git-branch", onClick: () => setCloneOpen(true) },
|
|
67
|
+
* { id: "more", label: "More", icon: "more-horizontal", items: [
|
|
68
|
+
* { id: "reset", label: "Reset", onClick: () => reset() },
|
|
69
|
+
* { id: "docs", label: "Docs", onClick: () => switchView("docs") },
|
|
70
|
+
* ]},
|
|
71
|
+
* ]);
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export declare function useTopBarActions(actions: TopBarAction[]): void;
|
|
75
|
+
/** Asks the host to switch the workspace to another view - a built-in one
|
|
76
|
+
* ("code" | "preview" | "changes" | "activity" | "documents" | "project")
|
|
77
|
+
* or another `custom_views` id. */
|
|
78
|
+
export declare function switchView(viewId: string): void;
|
package/dist/topbar.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
function flatten(actions) {
|
|
3
|
+
const map = new Map();
|
|
4
|
+
for (const a of actions) {
|
|
5
|
+
if (a.onClick && !a.href)
|
|
6
|
+
map.set(a.id, a.onClick);
|
|
7
|
+
for (const s of a.items ?? []) {
|
|
8
|
+
if (s.onClick && !s.href)
|
|
9
|
+
map.set(s.id, s.onClick);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return map;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Register this view's top-bar actions with the host. Call with an empty
|
|
16
|
+
* array (or unmount) to clear them - e.g. because the current screen inside
|
|
17
|
+
* your app doesn't have any right now.
|
|
18
|
+
*
|
|
19
|
+
* ```tsx
|
|
20
|
+
* useTopBarActions([
|
|
21
|
+
* { id: "clone", label: "Clone", icon: "git-branch", onClick: () => setCloneOpen(true) },
|
|
22
|
+
* { id: "more", label: "More", icon: "more-horizontal", items: [
|
|
23
|
+
* { id: "reset", label: "Reset", onClick: () => reset() },
|
|
24
|
+
* { id: "docs", label: "Docs", onClick: () => switchView("docs") },
|
|
25
|
+
* ]},
|
|
26
|
+
* ]);
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function useTopBarActions(actions) {
|
|
30
|
+
const handlersRef = React.useRef(new Map());
|
|
31
|
+
handlersRef.current = flatten(actions);
|
|
32
|
+
// Full replace, never a diff - simplest contract for both sides. Only
|
|
33
|
+
// label/icon/id cross the wire; `onClick` stays local (see `flatten`).
|
|
34
|
+
const wireItems = React.useMemo(() => actions.map((a) => ({
|
|
35
|
+
id: a.id,
|
|
36
|
+
label: a.label,
|
|
37
|
+
icon: a.icon,
|
|
38
|
+
href: a.href,
|
|
39
|
+
style: a.style,
|
|
40
|
+
items: a.items?.map((s) => ({ id: s.id, label: s.label, icon: s.icon, href: s.href, style: s.style })),
|
|
41
|
+
})),
|
|
42
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- content, not identity
|
|
43
|
+
[JSON.stringify(actions)]);
|
|
44
|
+
React.useEffect(() => {
|
|
45
|
+
if (typeof window === "undefined")
|
|
46
|
+
return;
|
|
47
|
+
try {
|
|
48
|
+
window.parent?.postMessage({ type: "digi:topbar-register", items: wireItems }, "*");
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* cross-origin parent may reject - nothing to fall back to here */
|
|
52
|
+
}
|
|
53
|
+
}, [wireItems]);
|
|
54
|
+
React.useEffect(() => {
|
|
55
|
+
if (typeof window === "undefined")
|
|
56
|
+
return;
|
|
57
|
+
const onMessage = (e) => {
|
|
58
|
+
const d = e.data;
|
|
59
|
+
if (!d || d.type !== "digi:topbar-click" || typeof d.id !== "string")
|
|
60
|
+
return;
|
|
61
|
+
handlersRef.current.get(d.id)?.();
|
|
62
|
+
};
|
|
63
|
+
window.addEventListener("message", onMessage);
|
|
64
|
+
return () => window.removeEventListener("message", onMessage);
|
|
65
|
+
}, []);
|
|
66
|
+
React.useEffect(() => {
|
|
67
|
+
return () => {
|
|
68
|
+
try {
|
|
69
|
+
window.parent?.postMessage({ type: "digi:topbar-register", items: [] }, "*");
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
/* best-effort - the host also clears on iframe unmount */
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}, []);
|
|
76
|
+
}
|
|
77
|
+
/** Asks the host to switch the workspace to another view - a built-in one
|
|
78
|
+
* ("code" | "preview" | "changes" | "activity" | "documents" | "project")
|
|
79
|
+
* or another `custom_views` id. */
|
|
80
|
+
export function switchView(viewId) {
|
|
81
|
+
if (typeof window === "undefined")
|
|
82
|
+
return;
|
|
83
|
+
try {
|
|
84
|
+
window.parent?.postMessage({ type: "digi:switch-view", viewId }, "*");
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
/* cross-origin parent may reject */
|
|
88
|
+
}
|
|
89
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export type SessionInfo = {
|
|
2
|
+
baseUrl: string;
|
|
3
|
+
appId: string;
|
|
4
|
+
sessionId: string;
|
|
5
|
+
/** JWT - used only outside the embedded preview. */
|
|
6
|
+
token?: string;
|
|
7
|
+
/** Per-session preview token (`?t=`) carried by the embedded-preview URL.
|
|
8
|
+
* When present, the SDK reads/writes via the public `preview/files` routes
|
|
9
|
+
* instead of the JWT-only `workspace/files` routes - no BFF, no cookie. */
|
|
10
|
+
previewToken?: string;
|
|
11
|
+
};
|
|
12
|
+
export type MessagePart = {
|
|
13
|
+
type?: string;
|
|
14
|
+
text?: string;
|
|
15
|
+
content?: string;
|
|
16
|
+
};
|
|
17
|
+
export type MessagePayload = {
|
|
18
|
+
role: string;
|
|
19
|
+
content?: string;
|
|
20
|
+
reasoning?: string;
|
|
21
|
+
parts?: MessagePart[];
|
|
22
|
+
};
|
|
23
|
+
export type ToolPayload = {
|
|
24
|
+
call_id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
arguments?: Record<string, unknown>;
|
|
27
|
+
status?: string;
|
|
28
|
+
text?: string;
|
|
29
|
+
};
|
|
30
|
+
export type ApprovalPayload = {
|
|
31
|
+
id: string;
|
|
32
|
+
kind: string;
|
|
33
|
+
payload?: Record<string, unknown>;
|
|
34
|
+
status?: string;
|
|
35
|
+
reason?: string;
|
|
36
|
+
};
|
|
37
|
+
export type TurnPayload = {
|
|
38
|
+
turn_id: string;
|
|
39
|
+
phase?: string;
|
|
40
|
+
status?: string;
|
|
41
|
+
reason?: string;
|
|
42
|
+
};
|
|
43
|
+
export type ErrorPayload = {
|
|
44
|
+
code?: string;
|
|
45
|
+
message: string;
|
|
46
|
+
category?: string;
|
|
47
|
+
};
|
|
48
|
+
export type Envelope = {
|
|
49
|
+
seq: number;
|
|
50
|
+
type: string;
|
|
51
|
+
ts: number;
|
|
52
|
+
session_id: string;
|
|
53
|
+
correlation_id?: string;
|
|
54
|
+
message?: MessagePayload;
|
|
55
|
+
tool?: ToolPayload;
|
|
56
|
+
approval?: ApprovalPayload;
|
|
57
|
+
turn?: TurnPayload;
|
|
58
|
+
error?: ErrorPayload;
|
|
59
|
+
[key: string]: unknown;
|
|
60
|
+
};
|
|
61
|
+
export type AgentStatus = "idle" | "thinking" | "tool_use" | "streaming" | "error";
|
|
62
|
+
export type ToolCall = {
|
|
63
|
+
callId: string;
|
|
64
|
+
name: string;
|
|
65
|
+
args?: Record<string, unknown>;
|
|
66
|
+
status?: string;
|
|
67
|
+
result?: string;
|
|
68
|
+
};
|
|
69
|
+
export type ChatMessage = {
|
|
70
|
+
role: "user" | "assistant" | "system";
|
|
71
|
+
content: string;
|
|
72
|
+
reasoning?: string;
|
|
73
|
+
streaming?: boolean;
|
|
74
|
+
toolCalls?: ToolCall[];
|
|
75
|
+
correlationId?: string;
|
|
76
|
+
ts: number;
|
|
77
|
+
};
|
|
78
|
+
export type Approval = {
|
|
79
|
+
id: string;
|
|
80
|
+
kind: string;
|
|
81
|
+
payload?: Record<string, unknown>;
|
|
82
|
+
status?: string;
|
|
83
|
+
};
|
|
84
|
+
export type State = {
|
|
85
|
+
connected: boolean;
|
|
86
|
+
status: AgentStatus;
|
|
87
|
+
messages: ChatMessage[];
|
|
88
|
+
turnActive: boolean;
|
|
89
|
+
approvals: Approval[];
|
|
90
|
+
events: Envelope[];
|
|
91
|
+
lastSeq: number;
|
|
92
|
+
error: string | null;
|
|
93
|
+
};
|
|
94
|
+
export declare const initialState: State;
|