@seed-app-studio/cli 0.1.1-canary.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.
@@ -0,0 +1,440 @@
1
+ import { useEffect, useState } from "react";
2
+ import {
3
+ createSeedAppClient,
4
+ createWindowSeedAppTransport,
5
+ installSeedAppConsoleBridge,
6
+ type SeedAppAgentStreamEvent,
7
+ type SeedAppClient,
8
+ type SeedAppEvent,
9
+ type SeedAppError,
10
+ type SeedAppGrantResolution,
11
+ type SeedAppRpcResponse,
12
+ } from "@seed-app-studio/sdk";
13
+ import { createAgentReplayFixtureDownload } from "./replayFixture.js";
14
+ import {
15
+ APP_ID,
16
+ STORIES,
17
+ type CapabilityStory,
18
+ type StoryResult,
19
+ type StoryRuntimeKind,
20
+ } from "./storyData.js";
21
+
22
+ type EventEntry = {
23
+ id: string;
24
+ event: SeedAppEvent;
25
+ };
26
+
27
+ const initialResults = Object.fromEntries(
28
+ STORIES.map((story) => [story.id, { state: "idle" }]),
29
+ ) as Record<string, StoryResult>;
30
+
31
+ export function App() {
32
+ const [client, setClient] = useState<SeedAppClient>();
33
+ const [grants, setGrants] = useState<SeedAppGrantResolution[]>([]);
34
+ const [results, setResults] = useState<Record<string, StoryResult>>(initialResults);
35
+ const [events, setEvents] = useState<EventEntry[]>([]);
36
+ const [agentEvents, setAgentEvents] = useState<SeedAppAgentStreamEvent[]>([]);
37
+ const [openCodeStoryId, setOpenCodeStoryId] = useState<string>();
38
+
39
+ useEffect(() => {
40
+ let disposed = false;
41
+ const activeClient = createSeedAppClient({
42
+ transport: createWindowSeedAppTransport({ timeoutMs: 8000 }),
43
+ });
44
+ setClient(activeClient);
45
+ const consoleBridge = installSeedAppConsoleBridge();
46
+ console.log("[seed-app-story] mounting React SPA capability stories");
47
+ const unsubscribe = activeClient.events.subscribe((event) => {
48
+ console.info("[seed-app-story] host event", event);
49
+ setEvents((current) =>
50
+ [{ id: `${Date.now()}:${current.length}`, event }, ...current].slice(0, 8),
51
+ );
52
+ });
53
+ const unsubscribeAgent = activeClient.agent.subscribe((event) => {
54
+ setAgentEvents((current) => [event, ...current].slice(0, 16));
55
+ });
56
+ void runStoryWithClient(STORIES[0], activeClient, () => disposed);
57
+ return () => {
58
+ disposed = true;
59
+ console.log("[seed-app-story] unmounting React SPA capability stories");
60
+ consoleBridge.uninstall();
61
+ unsubscribe();
62
+ unsubscribeAgent();
63
+ activeClient.close();
64
+ };
65
+ }, []);
66
+
67
+ const runStoryWithClient = async (
68
+ story: CapabilityStory,
69
+ activeClient: SeedAppClient,
70
+ isStale: () => boolean = () => false,
71
+ ) => {
72
+ if (isStale()) return;
73
+ setResults((current) => ({ ...current, [story.id]: { state: "running" } }));
74
+ try {
75
+ console.log("[seed-app-story] running story", story.id);
76
+ const payload = await story.run(activeClient);
77
+ if (isStale()) return;
78
+ if (story.id === "handshake" && isGrantPayload(payload)) setGrants(payload.grants);
79
+ if (story.id === "capabilities" && Array.isArray(payload))
80
+ setGrants(payload as SeedAppGrantResolution[]);
81
+ if (isGrantPrecedencePayload(payload)) setGrants(payload.fixture);
82
+ if (isAgentStreamReplayPayload(payload)) {
83
+ setAgentEvents((current) => [...copyNewestFirst(payload.events), ...current].slice(0, 16));
84
+ }
85
+ setResults((current) => ({
86
+ ...current,
87
+ [story.id]: { state: "success", runtimeKind: classifySuccessPayload(payload), payload },
88
+ }));
89
+ console.info("[seed-app-story] story completed", story.id, payload);
90
+ } catch (error) {
91
+ if (isStale()) return;
92
+ const hostError = isHostBlock(error) ? error : undefined;
93
+ setResults((current) => ({
94
+ ...current,
95
+ [story.id]: {
96
+ state: hostError ? "blocked" : "failed",
97
+ runtimeKind: classifyError(hostError),
98
+ error: error instanceof Error ? error.message : String(error),
99
+ payload: hostError,
100
+ },
101
+ }));
102
+ console.warn("[seed-app-story] story blocked or failed", story.id, error);
103
+ }
104
+ };
105
+
106
+ const runStory = (story: CapabilityStory) => {
107
+ if (!client) return;
108
+ void runStoryWithClient(story, client);
109
+ };
110
+
111
+ const downloadReplayFixture = () => {
112
+ const download = createAgentReplayFixtureDownload();
113
+ const objectUrl = URL.createObjectURL(new Blob([download.text], { type: download.mimeType }));
114
+ const anchor = document.createElement("a");
115
+ anchor.href = objectUrl;
116
+ anchor.download = download.fileName;
117
+ anchor.click();
118
+ URL.revokeObjectURL(objectUrl);
119
+ setAgentEvents((current) =>
120
+ [...copyNewestFirst(download.fixture.events), ...current].slice(0, 16),
121
+ );
122
+ console.info("[seed-app-story] replay fixture downloaded", {
123
+ fileName: download.fileName,
124
+ eventCount: download.fixture.expectations.eventCount,
125
+ });
126
+ };
127
+
128
+ return (
129
+ <main>
130
+ <header className="hero">
131
+ <div>
132
+ <p className="eyebrow">SEED App Host acceptance SPA</p>
133
+ <h1>React SPA capability stories</h1>
134
+ <p className="intro">
135
+ Load this localhost app in App Studio to verify SDK handshake, brokered capabilities,
136
+ approval paths, and denied states.
137
+ </p>
138
+ <p className="intro zh">
139
+ 将这个本地 React SPA 接入 App Studio,用于验证 SDK
140
+ 握手、宿主代理能力、授权审批路径和拒绝态处理。
141
+ </p>
142
+ </div>
143
+ <div className="identity">
144
+ <span>App ID</span>
145
+ <strong>{APP_ID}</strong>
146
+ </div>
147
+ </header>
148
+
149
+ <section className="layout">
150
+ <div className="stories">
151
+ {STORIES.map((story) => (
152
+ <StoryCard
153
+ key={story.id}
154
+ story={story}
155
+ grant={findGrant(grants, story.capability)}
156
+ result={results[story.id] ?? { state: "idle" }}
157
+ disabled={!client}
158
+ codeVisible={openCodeStoryId === story.id}
159
+ onRun={() => runStory(story)}
160
+ onToggleCode={() =>
161
+ setOpenCodeStoryId((current) => (current === story.id ? undefined : story.id))
162
+ }
163
+ />
164
+ ))}
165
+ </div>
166
+
167
+ <aside>
168
+ <Panel title="Replay fixture">
169
+ <p className="muted">
170
+ Download the same Bridge-projected agent stream used by the replay story.
171
+ </p>
172
+ <p className="muted zh">下载与回放 story 使用的同一份 Bridge 投影 Agent 流样例。</p>
173
+ <div className="fixtureActions">
174
+ <button type="button" onClick={downloadReplayFixture}>
175
+ Download JSON
176
+ </button>
177
+ <span>
178
+ Includes message, thought, tool, approval, elicitation, subagent, backup, and turn
179
+ events.
180
+ </span>
181
+ <span className="zh">
182
+ 包含消息、思考、工具、授权、追问、子 Agent、备份和回合事件。
183
+ </span>
184
+ </div>
185
+ </Panel>
186
+
187
+ <Panel title="Capability grants">
188
+ {grants.length === 0 ? (
189
+ <>
190
+ <p className="muted">Run handshake or capability snapshot to populate grants.</p>
191
+ <p className="muted zh">
192
+ 运行握手或能力快照 story 后,这里会展示宿主返回的授权结果。
193
+ </p>
194
+ </>
195
+ ) : (
196
+ <div className="grantList">
197
+ {grants.map((grant) => (
198
+ <div key={grant.capability} className={`grant ${grant.policy}`}>
199
+ <span>{grant.capability}</span>
200
+ <strong>{grant.policy}</strong>
201
+ <small>{grant.source}</small>
202
+ </div>
203
+ ))}
204
+ </div>
205
+ )}
206
+ </Panel>
207
+
208
+ <Panel title="Agent stream lifecycle">
209
+ {agentEvents.length === 0 ? (
210
+ <>
211
+ <p className="muted">
212
+ Run the stream replay story or a live agent stream to populate lifecycle events.
213
+ </p>
214
+ <p className="muted zh">
215
+ 运行流式回放或真实 Agent 流后,这里会显示 Agent 生命周期事件。
216
+ </p>
217
+ </>
218
+ ) : (
219
+ <div className="agentTimeline">
220
+ {agentEvents.map((event, index) => (
221
+ <div key={`${event.type}:${index}`} className="agentEvent">
222
+ <strong>{event.type}</strong>
223
+ <span>{formatAgentEventSummary(event)}</span>
224
+ </div>
225
+ ))}
226
+ </div>
227
+ )}
228
+ </Panel>
229
+
230
+ <Panel title="Host events">
231
+ {events.length === 0 ? (
232
+ <>
233
+ <p className="muted">Waiting for host events.</p>
234
+ <p className="muted zh">等待宿主事件,用于观察 iframe 与 App Host 的通信结果。</p>
235
+ </>
236
+ ) : (
237
+ <pre>
238
+ {JSON.stringify(
239
+ events.map((entry) => entry.event),
240
+ null,
241
+ 2,
242
+ )}
243
+ </pre>
244
+ )}
245
+ </Panel>
246
+ </aside>
247
+ </section>
248
+ </main>
249
+ );
250
+ }
251
+
252
+ function StoryCard(props: {
253
+ story: CapabilityStory;
254
+ grant?: SeedAppGrantResolution;
255
+ result: StoryResult;
256
+ disabled?: boolean;
257
+ codeVisible: boolean;
258
+ onRun(): void;
259
+ onToggleCode(): void;
260
+ }) {
261
+ const { codeVisible, disabled = false, grant, onRun, onToggleCode, result, story } = props;
262
+ return (
263
+ <article className={`story ${result.state}`}>
264
+ <div className="storyHeader">
265
+ <div>
266
+ <span className="capability">{story.capability}</span>
267
+ <h2>{story.title}</h2>
268
+ </div>
269
+ <div className="storyActions">
270
+ <button type="button" className="secondaryButton" onClick={onToggleCode}>
271
+ {codeVisible ? "Hide code" : "Code"}
272
+ </button>
273
+ <button type="button" onClick={onRun} disabled={disabled || result.state === "running"}>
274
+ {result.state === "running" ? "Running" : "Run"}
275
+ </button>
276
+ </div>
277
+ </div>
278
+ <p>{story.intent}</p>
279
+ <p className="zh">{story.intentZh}</p>
280
+ {codeVisible ? <pre className="codeExample">{story.codeExample}</pre> : null}
281
+ <div className="meta">
282
+ <span>Grant: {grant ? `${grant.policy} from ${grant.source}` : "not loaded"}</span>
283
+ <span>State: {result.state}</span>
284
+ </div>
285
+ <div className="runtimeLine">
286
+ <span className={`runtimeBadge ${result.runtimeKind ?? "pending"}`}>
287
+ {runtimeKindLabel(result.runtimeKind ?? "pending")}
288
+ </span>
289
+ <small>{runtimeKindCopy(result.runtimeKind ?? "pending")}</small>
290
+ </div>
291
+ {result.error ? <p className="error">{result.error}</p> : null}
292
+ {result.payload ? <pre>{JSON.stringify(result.payload, null, 2)}</pre> : null}
293
+ </article>
294
+ );
295
+ }
296
+
297
+ function Panel(props: { title: string; children: React.ReactNode }) {
298
+ return (
299
+ <section className="panel">
300
+ <h2>{props.title}</h2>
301
+ {props.children}
302
+ </section>
303
+ );
304
+ }
305
+
306
+ function findGrant(
307
+ grants: SeedAppGrantResolution[],
308
+ capability: string,
309
+ ): SeedAppGrantResolution | undefined {
310
+ return grants.find((grant) => grant.capability === capability);
311
+ }
312
+
313
+ function copyNewestFirst(events: SeedAppAgentStreamEvent[]): SeedAppAgentStreamEvent[] {
314
+ const ordered: SeedAppAgentStreamEvent[] = [];
315
+ for (let index = events.length - 1; index >= 0; index -= 1) {
316
+ ordered.push(events[index]);
317
+ }
318
+ return ordered;
319
+ }
320
+
321
+ function isGrantPayload(value: unknown): value is { grants: SeedAppGrantResolution[] } {
322
+ return (
323
+ typeof value === "object" &&
324
+ value !== null &&
325
+ Array.isArray((value as { grants?: unknown }).grants)
326
+ );
327
+ }
328
+
329
+ function isAgentStreamReplayPayload(
330
+ value: unknown,
331
+ ): value is { events: SeedAppAgentStreamEvent[] } {
332
+ return (
333
+ typeof value === "object" &&
334
+ value !== null &&
335
+ Array.isArray((value as { events?: unknown }).events)
336
+ );
337
+ }
338
+
339
+ function isGrantPrecedencePayload(value: unknown): value is { fixture: SeedAppGrantResolution[] } {
340
+ return (
341
+ typeof value === "object" &&
342
+ value !== null &&
343
+ Array.isArray((value as { fixture?: unknown }).fixture)
344
+ );
345
+ }
346
+
347
+ function isHostBlock(value: unknown): value is SeedAppRpcResponse["error"] {
348
+ return typeof value === "object" && value !== null && "code" in value && "requestId" in value;
349
+ }
350
+
351
+ function classifySuccessPayload(payload: unknown): StoryRuntimeKind {
352
+ if (isRecord(payload) && isStoryRuntimeKind(payload.runtimeKind)) return payload.runtimeKind;
353
+ if (isRecord(payload) && hasPreviewOnlyOutput(payload)) return "mock";
354
+ return "live";
355
+ }
356
+
357
+ function isStoryRuntimeKind(value: unknown): value is StoryRuntimeKind {
358
+ return (
359
+ value === "pending" ||
360
+ value === "live" ||
361
+ value === "mock" ||
362
+ value === "policy-block" ||
363
+ value === "runtime-error"
364
+ );
365
+ }
366
+
367
+ function classifyError(error: SeedAppError | undefined): StoryRuntimeKind {
368
+ if (!error) return "runtime-error";
369
+ if (
370
+ error.code === "approval_required" ||
371
+ error.code === "approval_denied" ||
372
+ error.code === "capability_denied" ||
373
+ error.code === "tenant_policy_denied"
374
+ ) {
375
+ return "policy-block";
376
+ }
377
+ return "runtime-error";
378
+ }
379
+
380
+ function hasPreviewOnlyOutput(value: Record<string, unknown>): boolean {
381
+ const output = value.output;
382
+ return isRecord(output) && output.previewOnly === true;
383
+ }
384
+
385
+ function runtimeKindLabel(kind: StoryRuntimeKind): string {
386
+ switch (kind) {
387
+ case "live":
388
+ return "Live";
389
+ case "mock":
390
+ return "Mock";
391
+ case "policy-block":
392
+ return "Policy block";
393
+ case "runtime-error":
394
+ return "Runtime error";
395
+ default:
396
+ return "Pending";
397
+ }
398
+ }
399
+
400
+ function runtimeKindCopy(kind: StoryRuntimeKind): string {
401
+ switch (kind) {
402
+ case "live":
403
+ return "The story returned from a live host capability or brokered SDK path.";
404
+ case "mock":
405
+ return "The host returned a preview-only response. Use this for UI work, not capability verification.";
406
+ case "policy-block":
407
+ return "The broker rejected or paused the call because policy or approval requires it.";
408
+ case "runtime-error":
409
+ return "The host attempted the runtime path but the capability was unavailable or failed.";
410
+ default:
411
+ return "Run the story to classify the result.";
412
+ }
413
+ }
414
+
415
+ function formatAgentEventSummary(event: SeedAppAgentStreamEvent): string {
416
+ switch (event.type) {
417
+ case "agent.message.delta":
418
+ case "agent.thought.delta":
419
+ case "agent.user.delta":
420
+ return event.payload.text;
421
+ case "agent.tool.started":
422
+ case "agent.tool.updated":
423
+ return `${event.payload.toolCallId}${event.payload.status ? ` / ${event.payload.status}` : ""}`;
424
+ case "agent.permission.required":
425
+ case "agent.elicitation.required":
426
+ return event.payload.requestId;
427
+ case "agent.subagent.anchor":
428
+ case "agent.subagent.message.delta":
429
+ case "agent.subagent.status.updated":
430
+ return event.payload.childSessionId;
431
+ case "agent.turn.completed":
432
+ return event.payload.stopReason ?? "turn completed";
433
+ default:
434
+ return event.payload.runId;
435
+ }
436
+ }
437
+
438
+ function isRecord(value: unknown): value is Record<string, unknown> {
439
+ return typeof value === "object" && value !== null && !Array.isArray(value);
440
+ }
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { App } from "./App.js";
4
+ import "./styles.css";
5
+
6
+ createRoot(document.getElementById("root")!).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ );
@@ -0,0 +1,60 @@
1
+ import type { SeedAppAgentStreamEvent } from "@seed-app-studio/sdk";
2
+ import { AGENT_STREAM_REPLAY_EVENTS, APP_ID } from "./storyData.js";
3
+
4
+ export type AgentReplayFixtureExport = {
5
+ schemaVersion: 1;
6
+ fixtureId: string;
7
+ appId: string;
8
+ source: "app-dev-demo";
9
+ generatedAt: string;
10
+ events: SeedAppAgentStreamEvent[];
11
+ expectations: {
12
+ eventCount: number;
13
+ eventTypes: string[];
14
+ terminalEvent: string;
15
+ };
16
+ };
17
+
18
+ export type AgentReplayFixtureDownload = {
19
+ fileName: string;
20
+ mimeType: "application/json";
21
+ text: string;
22
+ fixture: AgentReplayFixtureExport;
23
+ };
24
+
25
+ export const AGENT_REPLAY_FIXTURE_MIME_TYPE = "application/json";
26
+
27
+ export function buildAgentReplayFixtureExport(
28
+ events: SeedAppAgentStreamEvent[] = AGENT_STREAM_REPLAY_EVENTS,
29
+ generatedAt = new Date("2026-01-01T00:00:00.000Z"),
30
+ ): AgentReplayFixtureExport {
31
+ if (events.length === 0) throw new Error("Replay fixture requires at least one event.");
32
+ const eventTypes = events.map((event) => event.type);
33
+ return {
34
+ schemaVersion: 1,
35
+ fixtureId: `${APP_ID}:agent-stream-replay`,
36
+ appId: APP_ID,
37
+ source: "app-dev-demo",
38
+ generatedAt: generatedAt.toISOString(),
39
+ events,
40
+ expectations: {
41
+ eventCount: events.length,
42
+ eventTypes,
43
+ terminalEvent: eventTypes[eventTypes.length - 1] ?? "unknown",
44
+ },
45
+ };
46
+ }
47
+
48
+ export function serializeAgentReplayFixture(fixture = buildAgentReplayFixtureExport()): string {
49
+ return `${JSON.stringify(fixture, null, 2)}\n`;
50
+ }
51
+
52
+ export function createAgentReplayFixtureDownload(): AgentReplayFixtureDownload {
53
+ const fixture = buildAgentReplayFixtureExport();
54
+ return {
55
+ fileName: `${APP_ID}-agent-replay.fixture.json`,
56
+ mimeType: AGENT_REPLAY_FIXTURE_MIME_TYPE,
57
+ text: serializeAgentReplayFixture(fixture),
58
+ fixture,
59
+ };
60
+ }