@flowdular/sdk 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/.ai/skills/test-hardening/SKILL.md +1 -1
  2. package/README.md +69 -12
  3. package/assets/flowdular-banner.png +0 -0
  4. package/modules/agents/package.json +1 -1
  5. package/modules/auth/package.json +1 -1
  6. package/modules/auth/src/client/AuthenticationCore.tsrx +3 -1
  7. package/modules/auth/src/server/endpoints.ts +3 -1
  8. package/modules/auth/src/server/runtime.ts +4 -0
  9. package/modules/automations/package.json +1 -1
  10. package/modules/automations-workflows-integration/package.json +1 -1
  11. package/modules/profile/package.json +1 -1
  12. package/modules/sandbox/package.json +1 -1
  13. package/modules/system/package.json +1 -1
  14. package/modules/users/package.json +1 -1
  15. package/modules/workflows/package.json +1 -1
  16. package/package.json +5 -13
  17. package/packages/ai-provider/package.json +1 -1
  18. package/packages/cli-protocol/package.json +1 -1
  19. package/packages/client/package.json +1 -1
  20. package/packages/client/src/routing.ts +39 -2
  21. package/packages/client/src/shell/BrandMark.tsrx +5 -1
  22. package/packages/client/src/shell/navigation.ts +3 -1
  23. package/packages/client/src/state.ts +6 -2
  24. package/packages/contracts/package.json +1 -1
  25. package/packages/contracts/schemas/project.schema.json +16 -42
  26. package/packages/database/package.json +1 -1
  27. package/packages/database-pglite/package.json +2 -2
  28. package/packages/database-pglite/src/driver.ts +21 -3
  29. package/packages/database-testing/package.json +1 -1
  30. package/packages/dev-console/package.json +1 -1
  31. package/packages/harness/package.json +1 -1
  32. package/packages/kernel/package.json +1 -1
  33. package/packages/server/package.json +1 -1
  34. package/packages/server/src/application-routes.ts +80 -8
  35. package/packages/server/src/index.ts +4 -1
  36. package/packages/server/src/web-html.ts +9 -5
  37. package/packages/server/src/web.ts +59 -7
  38. package/packages/ui/package.json +1 -1
  39. package/packages/coding-agent/package.json +0 -44
  40. package/packages/coding-agent/src/drivers/byok.ts +0 -322
  41. package/packages/coding-agent/src/drivers/claude-code.ts +0 -290
  42. package/packages/coding-agent/src/drivers/codex.ts +0 -281
  43. package/packages/coding-agent/src/index.ts +0 -56
  44. package/packages/coding-agent/src/registry.ts +0 -119
  45. package/packages/coding-agent/src/roles/contract.ts +0 -60
  46. package/packages/coding-agent/src/roles/defaults.ts +0 -134
  47. package/packages/coding-agent/src/roles/handoff.ts +0 -41
  48. package/packages/coding-agent/src/roles/registry.ts +0 -156
  49. package/packages/coding-agent/src/roles/skills.ts +0 -72
  50. package/packages/coding-agent/src/roles/sync.ts +0 -88
  51. package/packages/coding-agent/src/types.ts +0 -102
  52. package/packages/coding-agent/src/workspace.ts +0 -275
  53. package/packages/sandbox/README.md +0 -691
  54. package/packages/sandbox/bin/flowdular-sandbox.mjs +0 -393
  55. package/packages/sandbox/dist/client/assets/App-ClMOrpyg.js +0 -11
  56. package/packages/sandbox/dist/client/assets/PreviewHost-BHiHrfbQ.js +0 -1
  57. package/packages/sandbox/dist/client/assets/auto-buGEq6de.js +0 -3
  58. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  59. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  60. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  61. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  62. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  63. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  64. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  65. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  66. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  67. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  68. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  69. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  70. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  71. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  72. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  73. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  74. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  75. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  76. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  77. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  78. package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
  79. package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
  80. package/packages/sandbox/dist/client/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
  81. package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
  82. package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
  83. package/packages/sandbox/dist/client/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
  84. package/packages/sandbox/dist/client/assets/index-CvwhlQ87.js +0 -4
  85. package/packages/sandbox/dist/client/assets/styles-BPWaGf55.js +0 -3
  86. package/packages/sandbox/dist/client/assets/styles-Dx1_KOag.css +0 -1
  87. package/packages/sandbox/dist/client/favicon.svg +0 -1
  88. package/packages/sandbox/dist/server/assets/auto-CVGNYyb3.js +0 -12785
  89. package/packages/sandbox/dist/server/assets/chunk-DDJLRBDX-3OUNo51g.js +0 -4125
  90. package/packages/sandbox/dist/server/assets/nodefs-DVSI7kG0.js +0 -24
  91. package/packages/sandbox/dist/server/assets/opfs-ahp-D56iokt1.js +0 -388
  92. package/packages/sandbox/dist/server/assets/token-Bvaq7Anz.js +0 -55
  93. package/packages/sandbox/dist/server/assets/token-util-BqSvdIPQ.js +0 -3
  94. package/packages/sandbox/dist/server/assets/token-util-CktFFT4z.js +0 -494
  95. package/packages/sandbox/dist/server/entry.js +0 -91671
  96. package/packages/sandbox/dist/server/favicon.svg +0 -1
  97. package/packages/sandbox/dist/server/index.html +0 -161
  98. package/packages/sandbox/index.html +0 -159
  99. package/packages/sandbox/octane.config.ts +0 -36
  100. package/packages/sandbox/package.json +0 -83
  101. package/packages/sandbox/public/favicon.svg +0 -1
  102. package/packages/sandbox/public/og.png +0 -0
  103. package/packages/sandbox/src/App.tsrx +0 -924
  104. package/packages/sandbox/src/client/ApprovalHandoff.tsrx +0 -39
  105. package/packages/sandbox/src/client/ChatPane.tsrx +0 -694
  106. package/packages/sandbox/src/client/ConnectView.tsrx +0 -86
  107. package/packages/sandbox/src/client/DiffPane.tsrx +0 -150
  108. package/packages/sandbox/src/client/EjectModal.tsrx +0 -571
  109. package/packages/sandbox/src/client/GateResults.tsrx +0 -60
  110. package/packages/sandbox/src/client/GitHubSettingsModal.tsrx +0 -219
  111. package/packages/sandbox/src/client/NewSessionForm.tsrx +0 -214
  112. package/packages/sandbox/src/client/SessionBar.tsrx +0 -151
  113. package/packages/sandbox/src/client/SessionPicker.tsrx +0 -472
  114. package/packages/sandbox/src/client/SpecEditor.tsrx +0 -109
  115. package/packages/sandbox/src/client/SpecReviewCard.tsrx +0 -321
  116. package/packages/sandbox/src/client/WorkCard.tsrx +0 -128
  117. package/packages/sandbox/src/client/WorkbenchModal.tsrx +0 -161
  118. package/packages/sandbox/src/client/WorkspaceMenu.tsrx +0 -100
  119. package/packages/sandbox/src/client/api.ts +0 -797
  120. package/packages/sandbox/src/client/attachments.ts +0 -73
  121. package/packages/sandbox/src/client/dashboard.ts +0 -46
  122. package/packages/sandbox/src/client/github-settings.ts +0 -48
  123. package/packages/sandbox/src/client/highlight.ts +0 -105
  124. package/packages/sandbox/src/client/i18n.ts +0 -32
  125. package/packages/sandbox/src/client/locales/en.json +0 -466
  126. package/packages/sandbox/src/client/locales/pl.json +0 -466
  127. package/packages/sandbox/src/client/session-labels.ts +0 -77
  128. package/packages/sandbox/src/client/state.ts +0 -108
  129. package/packages/sandbox/src/index.ts +0 -10
  130. package/packages/sandbox/src/preview/PreviewHost.tsrx +0 -373
  131. package/packages/sandbox/src/preview/load-module.ts +0 -16
  132. package/packages/sandbox/src/server/attachments.ts +0 -308
  133. package/packages/sandbox/src/server/auto-review.ts +0 -181
  134. package/packages/sandbox/src/server/checkpoints.ts +0 -90
  135. package/packages/sandbox/src/server/config.ts +0 -460
  136. package/packages/sandbox/src/server/dashboard.ts +0 -245
  137. package/packages/sandbox/src/server/delivery/configuration.ts +0 -193
  138. package/packages/sandbox/src/server/delivery/git-pr.ts +0 -1473
  139. package/packages/sandbox/src/server/delivery/index.ts +0 -83
  140. package/packages/sandbox/src/server/delivery/local.ts +0 -151
  141. package/packages/sandbox/src/server/delivery/official-modules.ts +0 -516
  142. package/packages/sandbox/src/server/delivery/plan.ts +0 -218
  143. package/packages/sandbox/src/server/delivery/policies.ts +0 -239
  144. package/packages/sandbox/src/server/delivery/record.ts +0 -41
  145. package/packages/sandbox/src/server/delivery/steps.ts +0 -464
  146. package/packages/sandbox/src/server/delivery/types.ts +0 -158
  147. package/packages/sandbox/src/server/dependencies.ts +0 -88
  148. package/packages/sandbox/src/server/diff.ts +0 -230
  149. package/packages/sandbox/src/server/gates.ts +0 -388
  150. package/packages/sandbox/src/server/index.ts +0 -139
  151. package/packages/sandbox/src/server/path-guard.ts +0 -282
  152. package/packages/sandbox/src/server/planning.ts +0 -749
  153. package/packages/sandbox/src/server/platform-client.ts +0 -189
  154. package/packages/sandbox/src/server/preview-database-host.ts +0 -340
  155. package/packages/sandbox/src/server/preview-database-protocol.ts +0 -163
  156. package/packages/sandbox/src/server/preview-database-proxy.ts +0 -356
  157. package/packages/sandbox/src/server/preview-database.ts +0 -109
  158. package/packages/sandbox/src/server/preview-ipc.ts +0 -15
  159. package/packages/sandbox/src/server/preview-modules.ts +0 -83
  160. package/packages/sandbox/src/server/preview-revision.ts +0 -44
  161. package/packages/sandbox/src/server/preview-runtime.ts +0 -467
  162. package/packages/sandbox/src/server/preview-worker-manager.ts +0 -436
  163. package/packages/sandbox/src/server/preview-worker.ts +0 -168
  164. package/packages/sandbox/src/server/reference.ts +0 -251
  165. package/packages/sandbox/src/server/routes.ts +0 -2258
  166. package/packages/sandbox/src/server/runtime.ts +0 -188
  167. package/packages/sandbox/src/server/session-owner.ts +0 -23
  168. package/packages/sandbox/src/server/sessions.ts +0 -849
  169. package/packages/sandbox/src/server/spec.ts +0 -471
  170. package/packages/sandbox/src/server/turn-lifecycle.ts +0 -56
  171. package/packages/sandbox/src/server/turns.ts +0 -993
  172. package/packages/sandbox/src/server/workspace-install.ts +0 -266
  173. package/packages/sandbox/src/server/workspace-root.ts +0 -73
  174. package/packages/sandbox/src/styles.css +0 -1573
  175. package/packages/sandbox/vite.config.ts +0 -133
@@ -1,924 +0,0 @@
1
- import { useEffect, useMemo } from 'octane';
2
- import { Head, Seo } from '@octanejs/seo';
3
- import { Alert, BrandMark, Button, Icon, Tag } from '@flowdular/sdk/ui';
4
- import { useValue } from 'segment-state';
5
- import type { RenderRouteProps } from '@octanejs/vite-plugin';
6
- import type {
7
- ChatEntry,
8
- HandoffPlan,
9
- SandboxSession,
10
- } from './server/sessions.ts';
11
- import {
12
- addSessionModule,
13
- approveSpecification,
14
- archiveSandboxSession,
15
- rejectSandboxSession,
16
- connectBrowser,
17
- createSandboxSession,
18
- deleteSandboxSession,
19
- followTurn,
20
- loadSandboxState,
21
- loadSession,
22
- formatSession,
23
- planEject,
24
- requestSpecChanges,
25
- restoreSandboxSession,
26
- runGates,
27
- saveConfiguration,
28
- setAutoContinue,
29
- stopTurn,
30
- streamEject,
31
- streamTurn,
32
- uploadAttachment,
33
- type EjectPlanView,
34
- type EjectTargetId,
35
- type TurnHandlers,
36
- } from './client/api.ts';
37
- import { ChatPane } from './client/ChatPane.tsrx';
38
- import { ConnectView } from './client/ConnectView.tsrx';
39
- import { SessionBar } from './client/SessionBar.tsrx';
40
- import { GateResults } from './client/GateResults.tsrx';
41
- import { WorkspaceMenu } from './client/WorkspaceMenu.tsrx';
42
- import { DiffPane } from './client/DiffPane.tsrx';
43
- import { WorkCard, type WorkCardMode } from './client/WorkCard.tsrx';
44
- import {
45
- WorkbenchModal,
46
- type WorkbenchMode,
47
- } from './client/WorkbenchModal.tsrx';
48
- import { SessionPicker } from './client/SessionPicker.tsrx';
49
- import { EjectModal } from './client/EjectModal.tsrx';
50
- import {
51
- GitHubSettingsModal,
52
- type GitHubSettingsInput,
53
- } from './client/GitHubSettingsModal.tsrx';
54
- import {
55
- createSandboxClientState,
56
- mergeEntries,
57
- mergeSteps,
58
- signalReady,
59
- sessionIdFromUrl,
60
- type PreviewSelection,
61
- } from './client/state.ts';
62
- import {
63
- registerSandboxTranslations,
64
- restoreSandboxLocale,
65
- useTranslation,
66
- } from './client/i18n.ts';
67
- import '@flowdular/sdk/ui/styles';
68
- import './styles.css';
69
-
70
- registerSandboxTranslations();
71
-
72
- function message(error: unknown, fallback: string): string {
73
- return error instanceof Error ? error.message : fallback;
74
- }
75
-
76
- /* A session that was created but never ran a turn still knows what it was asked
77
- for, so it can be started instead of retyped. */
78
- function startingPoint(
79
- session: SandboxSession,
80
- entries: readonly ChatEntry[],
81
- ): string {
82
- if (entries.some((entry) => entry.kind === 'user')) return '';
83
- return session.brief ||
84
- entries.find((entry) => entry.kind === 'system')?.text ||
85
- session.title;
86
- }
87
-
88
- export function App(props?: RenderRouteProps) @{
89
- const { t } = useTranslation();
90
- const sandbox = useMemo(() => createSandboxClientState(), []);
91
- const [state] = useValue(sandbox.state.state);
92
- const [view] = useValue(sandbox.state.view);
93
- const [entries] = useValue(sandbox.state.liveEntries);
94
- const [selection, setSelection] = useValue(sandbox.state.selection);
95
- const [activeSessionId, setActiveSessionId] = useValue(
96
- sandbox.state.activeSessionId,
97
- );
98
- const [role, setRole] = useValue(sandbox.state.role);
99
- const [activeModule, setActiveModule] = useValue(sandbox.state.activeModule);
100
- const [turnModule, setTurnModule] = useValue(sandbox.state.turnModule);
101
- const [driver, setDriver] = useValue(sandbox.state.driver);
102
- const [draft, setDraft] = useValue(sandbox.state.message);
103
- const [workbench, setWorkbench] = useValue(sandbox.state.workbench);
104
- const [cardMode, setCardMode] = useValue(sandbox.state.cardMode);
105
- const [nonce, setNonce] = useValue(sandbox.state.previewNonce);
106
- const [selecting, setSelecting] = useValue(sandbox.state.selecting);
107
- const [running, setRunning] = useValue(sandbox.state.running);
108
- const [busy, setBusy] = useValue(sandbox.state.busy);
109
- const [error, setError] = useValue(sandbox.state.error);
110
- const [notice, setNotice] = useValue(sandbox.state.notice);
111
- const [gateResults, setGateResults] = useValue(sandbox.state.gateResults);
112
- const [gatesOpen, setGatesOpen] = useValue(sandbox.state.gatesOpen);
113
- const [gatesRunning, setGatesRunning] = useValue(sandbox.state.gatesRunning);
114
- const [gatesError, setGatesError] = useValue(sandbox.state.gatesError);
115
- const [plan, setPlan] = useValue(sandbox.state.ejectPlan);
116
- const [ejectSteps, setEjectSteps] = useValue(sandbox.state.ejectSteps);
117
- const [ejectSummary, setEjectSummary] = useValue(sandbox.state.ejectSummary);
118
- const [ejectError, setEjectError] = useValue(sandbox.state.ejectError);
119
- const [ejecting, setEjecting] = useValue(sandbox.state.ejecting);
120
- const [ejectBuild, setEjectBuild] = useValue(sandbox.state.ejectBuild);
121
- const [menuOpen, setMenuOpen] = useValue(sandbox.state.menuOpen);
122
- const [githubSettingsOpen, setGitHubSettingsOpen] = useValue(
123
- sandbox.state.githubSettingsOpen,
124
- );
125
- const [connecting, setConnecting] = useValue(sandbox.state.connecting);
126
- const [sessionMenu, setSessionMenu] = useValue(sandbox.state.sessionMenu);
127
- const [confirmDelete, setConfirmDelete] = useValue(
128
- sandbox.state.confirmDelete,
129
- );
130
- const [showArchived, setShowArchived] = useValue(sandbox.state.showArchived);
131
- /* The stream that follows the running turn outlives the render that opened
132
- it, so its controller lives in a box instead of a render-scoped variable. */
133
- const turn = useMemo(
134
- () => ({ controller: null as AbortController | null }),
135
- [],
136
- );
137
- const currentSession = () =>
138
- sandbox.store.get(sandbox.state.view)?.session ?? null;
139
-
140
- const refreshState = async () => {
141
- const next = await loadSandboxState();
142
- sandbox.store.act((transaction) => {
143
- transaction.set(sandbox.state.state, next);
144
- transaction.set(sandbox.state.sessions, next.sessions);
145
- }, 'sandbox/state');
146
- if (!driver) setDriver(next.configuration.driver);
147
- return next;
148
- };
149
-
150
- /* Reloads the transcript and diff of the open session without touching the
151
- running flag, which the turn stream owns while it is attached. */
152
- const refreshView = async (id: string) => {
153
- const loaded = await loadSession(id).catch(() => null);
154
- if (!loaded || sandbox.store.get(sandbox.state.activeSessionId) !== id) {
155
- return;
156
- }
157
- sandbox.store.act((transaction) => {
158
- transaction.set(sandbox.state.view, loaded);
159
- transaction.set(sandbox.state.liveEntries, loaded.chat);
160
- }, 'sandbox/refresh');
161
- };
162
-
163
- const detach = () => {
164
- turn.controller?.abort();
165
- turn.controller = null;
166
- };
167
-
168
- /* Whatever stream carries the turn, the screen reacts the same way: entries
169
- land as they happen, each finished turn refreshes the diff, and the end of
170
- the chain frees the composer. */
171
- const handlersFor = (id: string): TurnHandlers => ({
172
- onEntry: (entry) => {
173
- if (sandbox.store.get(sandbox.state.activeSessionId) !== id) return;
174
- sandbox.store.update(
175
- sandbox.state.liveEntries,
176
- (current) => mergeEntries(current, [entry]),
177
- 'sandbox/entry',
178
- );
179
- },
180
- onCompleted: () => {
181
- void refreshView(id);
182
- },
183
- onEnded: () => {
184
- if (sandbox.store.get(sandbox.state.activeSessionId) !== id) return;
185
- turn.controller = null;
186
- setRunning(false);
187
- setSelection(null);
188
- void refreshView(id).then(() => {
189
- setNonce(sandbox.store.get(sandbox.state.previewNonce) + 1);
190
- });
191
- void refreshState().catch((failure) => {
192
- if (!sandbox.store.get(sandbox.state.error)) {
193
- setError(message(failure, t('sandbox.app.error.state')));
194
- }
195
- });
196
- },
197
- onFailed: (failure) => {
198
- if (sandbox.store.get(sandbox.state.activeSessionId) !== id) return;
199
- setError(failure);
200
- },
201
- });
202
-
203
- /* A turn keeps running when the browser reloads or another tab started it.
204
- This browser attaches to it and follows until the sandbox reports the end. */
205
- const follow = async (id: string) => {
206
- if (turn.controller !== null) return;
207
- const following = followTurn(id, handlersFor(id));
208
- turn.controller = following.controller;
209
- const attached = await following.attached;
210
- if (!attached) {
211
- if (turn.controller === following.controller) turn.controller = null;
212
- setRunning(false);
213
- }
214
- };
215
-
216
- const openSession = async (id: string) => {
217
- setBusy(true);
218
- try {
219
- const loaded = await loadSession(id);
220
- sandbox.store.act((transaction) => {
221
- transaction.set(sandbox.state.view, loaded);
222
- transaction.set(sandbox.state.liveEntries, loaded.chat);
223
- transaction.set(sandbox.state.activeSessionId, id);
224
- /* Module selections belong to the session that is open. */
225
- transaction.set(sandbox.state.activeModule, '');
226
- transaction.set(sandbox.state.turnModule, '');
227
- }, 'sandbox/session');
228
- setDriver(loaded.session.driver);
229
- setNonce(nonce + 1);
230
- history.pushState(null, '', '/sessions/' + id);
231
- if (turn.controller === null) {
232
- setRunning(loaded.running);
233
- if (loaded.running) void follow(id);
234
- }
235
- } catch (loadError) {
236
- setError(message(loadError, t('sandbox.app.error.openSession')));
237
- } finally {
238
- setBusy(false);
239
- }
240
- };
241
-
242
- useEffect(() => {
243
- restoreSandboxLocale();
244
- void (async () => {
245
- try {
246
- await refreshState();
247
- const initial = sessionIdFromUrl(
248
- new URL(props?.url ??
249
- window.location.href, 'http://sandbox.local').pathname,
250
- );
251
- if (initial) await openSession(initial);
252
- } catch (loadError) {
253
- setError(message(loadError, t('sandbox.app.error.state')));
254
- } finally {
255
- signalReady();
256
- }
257
- })();
258
-
259
- const onMessage = (event: MessageEvent) => {
260
- const data = event.data as PreviewSelection & { type?: string };
261
- if (data?.type !== 'flowdular:selection') return;
262
- setSelection({
263
- label: data.label,
264
- selector: data.selector,
265
- view: data.view,
266
- text: data.text,
267
- });
268
- setSelecting(false);
269
- };
270
- window.addEventListener('message', onMessage);
271
- return () => window.removeEventListener('message', onMessage);
272
- }, []);
273
-
274
- /* One turn, wherever it came from: an operator message or a handoff the
275
- operator confirmed. Automatic handoffs are chained by the server on the
276
- same stream, so this browser only has to follow it. */
277
- const dispatch = (
278
- session: SandboxSession,
279
- text: string,
280
- roleId: string,
281
- moduleDirectory = sandbox.store.get(sandbox.state.turnModule),
282
- submittedDraft?: string,
283
- ) => {
284
- detach();
285
- setRunning(true);
286
- setError('');
287
- const handlers = handlersFor(session.id);
288
- turn.controller = streamTurn(
289
- session.id,
290
- {
291
- message: text,
292
- role: roleId,
293
- ...(moduleDirectory ? { module: moduleDirectory } : {}),
294
- driver: sandbox.store.get(sandbox.state.driver) || session.driver,
295
- },
296
- {
297
- ...handlers,
298
- onEntry: (entry) => {
299
- if (
300
- sandbox.store.get(sandbox.state.activeSessionId) !== session.id
301
- ) return;
302
- // Clear only after the server has recorded this message, preserving
303
- // a failed submission and any newer draft typed while awaiting it.
304
- if (
305
- entry.kind === 'user' && submittedDraft !== undefined &&
306
- sandbox.store.get(sandbox.state.message) === submittedDraft
307
- ) setDraft('');
308
- handlers.onEntry(entry);
309
- },
310
- },
311
- );
312
- };
313
-
314
- /* One brief starts everything: the planner names the modules and the first
315
- specialist, then the same brief becomes the first turn, so a new session
316
- is already working when it opens. */
317
- const startSession = async (
318
- brief: string,
319
- chosenDriver: string,
320
- attachments: readonly File[],
321
- ) => {
322
- setBusy(true);
323
- setError('');
324
- try {
325
- const created = await createSandboxSession(brief, chosenDriver);
326
- await refreshState();
327
- await openSession(created.session.id);
328
- /* The files go up before the first turn, so its prompt already names
329
- them. A failed upload keeps the created session and stops here
330
- instead of starting a turn that cannot see the file. */
331
- const failed: string[] = [];
332
- for (const file of attachments) {
333
- try {
334
- await uploadAttachment(created.session.id, file);
335
- } catch (uploadError) {
336
- failed.push(
337
- `${file.name}: ${message(
338
- uploadError,
339
- t('sandbox.attachments.uploadFailed'),
340
- )}`,
341
- );
342
- }
343
- }
344
- if (failed.length > 0) {
345
- setError(
346
- t('sandbox.app.error.attachments', { failures: failed.join('; ') }),
347
- );
348
- return;
349
- }
350
- dispatch(created.session, brief, 'auto', '');
351
- } catch (createError) {
352
- setError(message(createError, t('sandbox.app.error.startSession')));
353
- } finally {
354
- setBusy(false);
355
- }
356
- };
357
-
358
- const send = (explicit?: string) => {
359
- const session = currentSession();
360
- if (!session || sandbox.store.get(sandbox.state.running)) return;
361
- const text = (explicit ?? draft).trim();
362
- if (!text) return;
363
- const withSelection = selection
364
- ? `${text}\n\nSelected element in the preview: ${selection.label}\nPath: ${selection.selector}${
365
- selection.view ? `\nView: ${selection.view}` : ''
366
- }`
367
- : text;
368
- dispatch(
369
- session,
370
- withSelection,
371
- role,
372
- undefined,
373
- explicit === undefined ? draft : undefined,
374
- );
375
- };
376
-
377
- const continueWith = (handoff: HandoffPlan) => {
378
- const session = currentSession();
379
- if (!session || running) return;
380
- dispatch(session, handoff.prompt, handoff.role, handoff.module ?? '');
381
- };
382
-
383
- const approveAndContinue = async (handoff: HandoffPlan) => {
384
- const session = currentSession();
385
- if (!session || running) return;
386
- setBusy(true);
387
- setError('');
388
- try {
389
- await approveSpecification(session.id, handoff.module);
390
- await openSession(session.id);
391
- dispatch(session, handoff.prompt, handoff.role, handoff.module ?? '');
392
- } catch (approveError) {
393
- setError(message(approveError, t('sandbox.app.error.approveSpec')));
394
- } finally {
395
- setBusy(false);
396
- }
397
- };
398
-
399
- /* The operator steers the specification in words: the comment is recorded
400
- and becomes the business manager's next turn. */
401
- const askForSpecChanges = async (module: string, comment: string) => {
402
- const session = currentSession();
403
- if (!session || running) return;
404
- setBusy(true);
405
- setError('');
406
- try {
407
- const asked = await requestSpecChanges(session.id, module, comment);
408
- await openSession(session.id);
409
- dispatch(session, asked.handoff.prompt, asked.handoff.role, module);
410
- } catch (askError) {
411
- setError(message(askError, t('sandbox.app.error.requestChanges')));
412
- } finally {
413
- setBusy(false);
414
- }
415
- };
416
-
417
- const addModule = async (moduleId: string) => {
418
- const session = currentSession();
419
- if (!session) return;
420
- setBusy(true);
421
- setError('');
422
- try {
423
- await addSessionModule(session.id, moduleId);
424
- await openSession(session.id);
425
- await refreshState();
426
- } catch (addError) {
427
- setError(message(addError, t('sandbox.app.error.addModule')));
428
- } finally {
429
- setBusy(false);
430
- }
431
- };
432
-
433
- const toggleAutoContinue = async (enabled: boolean) => {
434
- const session = currentSession();
435
- if (!session) return;
436
- try {
437
- await setAutoContinue(session.id, enabled);
438
- await refreshView(session.id);
439
- } catch (settingError) {
440
- setError(message(settingError, t('sandbox.app.error.handoff')));
441
- }
442
- };
443
-
444
- const stop = () => {
445
- const session = currentSession();
446
- detach();
447
- setRunning(false);
448
- if (session) {
449
- void stopTurn(session.id).then(() => openSession(session.id));
450
- }
451
- };
452
-
453
- const validate = async () => {
454
- const session = view?.session;
455
- if (!session) return;
456
- setBusy(true);
457
- setGatesOpen(true);
458
- setGatesRunning(true);
459
- setGatesError('');
460
- setGateResults([]);
461
- try {
462
- await formatSession(session.id);
463
- const result = await runGates(session.id);
464
- setGateResults(result.gates);
465
- await refreshView(session.id);
466
- } catch (gateError) {
467
- setGatesError(message(gateError, t('sandbox.app.error.gates')));
468
- } finally {
469
- setBusy(false);
470
- setGatesRunning(false);
471
- }
472
- };
473
-
474
- const startEject = async (target?: EjectTargetId) => {
475
- const session = view?.session;
476
- if (!session) return;
477
- setBusy(true);
478
- setError('');
479
- try {
480
- const result = await planEject(session.id, target);
481
- setPlan(result.plan);
482
- setNotice('');
483
- } catch (planError) {
484
- setError(message(planError, t('sandbox.app.error.ejectPlan')));
485
- } finally {
486
- setBusy(false);
487
- }
488
- };
489
-
490
- const confirmEject = () => {
491
- const session = view?.session;
492
- if (!session || ejecting) return;
493
- sandbox.store.act((transaction) => {
494
- transaction.set(sandbox.state.ejecting, true);
495
- transaction.set(sandbox.state.ejectSteps, []);
496
- transaction.set(sandbox.state.ejectSummary, null);
497
- transaction.set(sandbox.state.ejectError, '');
498
- }, 'sandbox/eject-start');
499
- streamEject(
500
- session.id,
501
- {
502
- onStep: (step) => sandbox.store.update(
503
- sandbox.state.ejectSteps,
504
- (current) => mergeSteps(current, step),
505
- 'sandbox/eject-step',
506
- ),
507
- onDone: (summary) => {
508
- setEjecting(false);
509
- setEjectSummary(summary);
510
- void openSession(session.id);
511
- void refreshState();
512
- },
513
- onFailed: (failure) => {
514
- setEjecting(false);
515
- setEjectError(failure);
516
- },
517
- },
518
- { build: ejectBuild, target: plan?.target },
519
- );
520
- };
521
-
522
- const closeEject = () => {
523
- sandbox.store.act((transaction) => {
524
- transaction.set(sandbox.state.ejectPlan, null);
525
- transaction.set(sandbox.state.ejectSteps, []);
526
- transaction.set(sandbox.state.ejectSummary, null);
527
- transaction.set(sandbox.state.ejectError, '');
528
- transaction.set(sandbox.state.ejecting, false);
529
- }, 'sandbox/eject-close');
530
- };
531
-
532
- const backToSessions = () => {
533
- detach();
534
- setRunning(false);
535
- sandbox.store.act((transaction) => {
536
- transaction.set(sandbox.state.activeSessionId, '');
537
- transaction.set(sandbox.state.view, null);
538
- transaction.set(sandbox.state.liveEntries, []);
539
- transaction.set(sandbox.state.workbench, 'none');
540
- transaction.set(sandbox.state.activeModule, '');
541
- transaction.set(sandbox.state.turnModule, '');
542
- transaction.set(sandbox.state.notice, '');
543
- transaction.set(sandbox.state.error, '');
544
- }, 'sandbox/sessions');
545
- history.pushState(null, '', '/');
546
- void refreshState();
547
- };
548
-
549
- /* Session lifecycle from the list. Every action closes the menu and reloads
550
- the list, so the card reflects what the server did. */
551
- const sessionAction = async (
552
- id: string,
553
- action: () => Promise<unknown>,
554
- failure: string,
555
- ) => {
556
- setBusy(true);
557
- setError('');
558
- sandbox.store.act((transaction) => {
559
- transaction.set(sandbox.state.sessionMenu, '');
560
- transaction.set(sandbox.state.confirmDelete, '');
561
- }, 'sandbox/session-menu');
562
- try {
563
- await action();
564
- await refreshState();
565
- } catch (actionError) {
566
- setError(message(actionError, failure));
567
- } finally {
568
- setBusy(false);
569
- }
570
- };
571
-
572
- const disconnect = async () => {
573
- setBusy(true);
574
- try {
575
- await saveConfiguration({ disconnect: true });
576
- await refreshState();
577
- backToSessions();
578
- } catch (disconnectError) {
579
- setError(message(disconnectError, t('sandbox.app.error.disconnect')));
580
- } finally {
581
- setBusy(false);
582
- }
583
- };
584
-
585
- const setPreviewData = async (mode: 'fixtures' | 'bridge') => {
586
- try {
587
- await saveConfiguration({ previewData: mode });
588
- await refreshState();
589
- setNonce(nonce + 1);
590
- } catch (configError) {
591
- setError(message(configError, t('sandbox.app.error.previewMode')));
592
- }
593
- };
594
-
595
- const saveGitHub = async (input: GitHubSettingsInput) => {
596
- setBusy(true);
597
- setError('');
598
- try {
599
- await saveConfiguration(input);
600
- await refreshState();
601
- setGitHubSettingsOpen(false);
602
- setNotice(t('sandbox.app.github.saved'));
603
- } catch (configurationError) {
604
- setError(message(configurationError, t('sandbox.app.error.github')));
605
- } finally {
606
- setBusy(false);
607
- }
608
- };
609
-
610
- /* A self-hosted sandbox signs this browser in first; the configuration call
611
- that follows is then an authenticated one. */
612
- const connect = async (input: {
613
- platformUrl: string;
614
- platformToken: string;
615
- }) => {
616
- setBusy(true);
617
- setError('');
618
- try {
619
- if (state?.configuration.mode === 'self-hosted') {
620
- await connectBrowser({
621
- token: input.platformToken,
622
- platformUrl: input.platformUrl,
623
- });
624
- }
625
- const result = await saveConfiguration(input);
626
- await refreshState();
627
- setConnecting(false);
628
- if (!result.connection.connected) {
629
- setError(
630
- result.connection.error?.message ?? t('sandbox.app.error.connect'),
631
- );
632
- }
633
- } catch (connectError) {
634
- setError(message(connectError, t('sandbox.app.error.saveConnection')));
635
- } finally {
636
- setBusy(false);
637
- }
638
- };
639
-
640
- <Head>
641
- <Seo
642
- title={t('sandbox.app.seo.title')}
643
- titleTemplate="%s · Flowdular"
644
- description={t('sandbox.app.seo.description')}
645
- robots={{ index: false, follow: false, noarchive: true }}
646
- openGraph={{
647
- type: 'website',
648
- siteName: t('sandbox.app.seo.imageAlt'),
649
- images: [
650
- {
651
- url: '/og.png',
652
- width: 1730,
653
- height: 909,
654
- alt: t('sandbox.app.seo.imageAlt'),
655
- },
656
- ],
657
- }}
658
- twitter={{ card: 'summary_large_image', image: '/og.png' }}
659
- />
660
- <div>
661
- @if (!state) {
662
- <div class="connect">
663
- @if (error) {
664
- <>
665
- <Alert>{error}</Alert>
666
- <Button
667
- onClick={() => {
668
- setError('');
669
- void refreshState().catch(
670
- (error) => setError(
671
- message(error, t('sandbox.app.error.state')),
672
- ),
673
- );
674
- }}
675
- >{t('sandbox.dashboard.refresh')}</Button>
676
- </>
677
- } @else {
678
- <p class="ui-help">{t('sandbox.app.starting')}</p>
679
- }
680
- </div>
681
- } @else if (!state.connection.connected || connecting) {
682
- <ConnectView
683
- state={state}
684
- busy={busy}
685
- error={error}
686
- onConnect={(input) => void connect(input)}
687
- />
688
- } @else {
689
- <div class="sandbox">
690
- <header class="sandbox__bar">
691
- <button
692
- class="sandbox__brand"
693
- type="button"
694
- onClick={() => backToSessions()}
695
- >
696
- <BrandMark size={20} />
697
- {t('sandbox.app.brand')}
698
- </button>
699
- <Tag mono>{state.configuration.mode}</Tag>
700
- <span class="sandbox__spacer"></span>
701
- <WorkspaceMenu
702
- state={state}
703
- open={menuOpen}
704
- onToggle={() => setMenuOpen(!menuOpen)}
705
- onChangeConnection={() => {
706
- setMenuOpen(false);
707
- setConnecting(true);
708
- }}
709
- onDisconnect={() => {
710
- setMenuOpen(false);
711
- void disconnect();
712
- }}
713
- />
714
- </header>
715
-
716
- <div class="sandbox__notice">
717
- @if (error) {
718
- <Alert>{error}</Alert>
719
- } @else if (notice) {
720
- <Alert tone="info">{notice}</Alert>
721
- }
722
- </div>
723
- @if (plan) {
724
- <EjectModal
725
- plan={plan}
726
- steps={ejectSteps}
727
- summary={ejectSummary}
728
- error={ejectError}
729
- running={ejecting}
730
- build={ejectBuild}
731
- onBuild={setEjectBuild}
732
- onTarget={(target) => void startEject(target)}
733
- onConfirm={confirmEject}
734
- onClose={closeEject}
735
- />
736
- }
737
- @if (githubSettingsOpen) {
738
- <GitHubSettingsModal
739
- configuration={state.configuration}
740
- busy={busy}
741
- error={error}
742
- onSave={(input) => void saveGitHub(input)}
743
- onClose={() => {
744
- setGitHubSettingsOpen(false);
745
- setError('');
746
- }}
747
- />
748
- }
749
-
750
- @if (!activeSessionId || !view) {
751
- <div class="sandbox__scroll">
752
- <SessionPicker
753
- onDeliverySettings={() => {
754
- setError('');
755
- setGitHubSettingsOpen(true);
756
- }}
757
- dashboard={state.dashboard}
758
- onRefresh={() => void refreshState().catch(
759
- (error) => setError(
760
- message(error, t('sandbox.app.error.state')),
761
- ),
762
- )}
763
- onReject={(id) => void sessionAction(
764
- id,
765
- () => rejectSandboxSession(id),
766
- t('sandbox.dashboard.rejectError'),
767
- )}
768
- sessions={state.sessions}
769
- running={state.running}
770
- roles={state.roles}
771
- drivers={state.drivers}
772
- driver={driver || state.configuration.driver}
773
- busy={busy}
774
- openMenu={sessionMenu}
775
- confirmDelete={confirmDelete}
776
- showArchived={showArchived}
777
- onDriver={setDriver}
778
- onOpen={(id) => void openSession(id)}
779
- onCreate={(brief, chosen, attachments) => void startSession(
780
- brief,
781
- chosen,
782
- attachments,
783
- )}
784
- onMenu={(id) => {
785
- setSessionMenu(id);
786
- setConfirmDelete('');
787
- }}
788
- onShowArchived={setShowArchived}
789
- onArchive={(id) => void sessionAction(
790
- id,
791
- () => archiveSandboxSession(id),
792
- t('sandbox.app.error.archiveSession'),
793
- )}
794
- onRestore={(id) => void sessionAction(
795
- id,
796
- () => restoreSandboxSession(id),
797
- t('sandbox.app.error.restoreSession'),
798
- )}
799
- onConfirmDelete={setConfirmDelete}
800
- onDelete={(id) => void sessionAction(
801
- id,
802
- () => deleteSandboxSession(id),
803
- t('sandbox.app.error.deleteSession'),
804
- )}
805
- />
806
- </div>
807
- } @else {
808
- <>
809
- <SessionBar
810
- session={view.session}
811
- changes={view.diffs.length}
812
- busy={busy}
813
- activeModule={activeModule}
814
- workspaceModules={state.workspaceModules}
815
- onModule={setActiveModule}
816
- onAddModule={(moduleId) => void addModule(moduleId)}
817
- onBack={backToSessions}
818
- onGates={() => void validate()}
819
- onChanges={() => setWorkbench('diff')}
820
- onPreview={() => setWorkbench('preview')}
821
- onEject={() => void startEject()}
822
- />
823
- <div class="sandbox__body">
824
- <ChatPane
825
- entries={entries}
826
- delivered={Boolean(view.session.ejectedAt)}
827
- roles={state.roles}
828
- drivers={state.drivers}
829
- modules={view.session.modules}
830
- specs={view.specs}
831
- role={role}
832
- module={turnModule}
833
- driver={driver}
834
- message={draft}
835
- running={running}
836
- busy={busy}
837
- selection={selection}
838
- autoContinue={view.session.autoContinue}
839
- pendingBrief={startingPoint(view.session, entries)}
840
- onStart={() => send(startingPoint(view.session, entries))}
841
- onContinue={continueWith}
842
- onApprove={(handoff) => void approveAndContinue(handoff)}
843
- onRequestChanges={(module, comment) => void askForSpecChanges(
844
- module,
845
- comment,
846
- )}
847
- onEditSpec={(module) => {
848
- setActiveModule(module);
849
- setWorkbench('spec');
850
- }}
851
- onAutoContinue={(enabled) => void toggleAutoContinue(enabled)}
852
- onRole={setRole}
853
- onModule={setTurnModule}
854
- onDriver={setDriver}
855
- onMessage={setDraft}
856
- onSend={() => send()}
857
- onStop={stop}
858
- onClearSelection={() => setSelection(null)}
859
- />
860
- <aside class="sandbox__side">
861
- <WorkCard
862
- session={view.session}
863
- diffs={view.diffs}
864
- mode={cardMode as WorkCardMode}
865
- nonce={nonce}
866
- activeModule={activeModule}
867
- previewData={state.configuration.previewData}
868
- onMode={(mode) => setCardMode(mode)}
869
- onOpen={() => setWorkbench(
870
- cardMode === 'code' ? 'diff' : 'preview',
871
- )}
872
- onReload={() => setNonce(nonce + 1)}
873
- onAskAbout={(path) => setDraft(
874
- draft
875
- ? draft + '\n\n' + t('sandbox.app.askAbout', { path })
876
- : t('sandbox.app.askAbout', { path }),
877
- )}
878
- />
879
- </aside>
880
- </div>
881
- </>
882
- }
883
- @if (gatesOpen) {
884
- <GateResults
885
- results={gateResults}
886
- running={gatesRunning}
887
- error={gatesError}
888
- onClose={() => setGatesOpen(false)}
889
- />
890
- }
891
- @if (workbench !== 'none') {
892
- <WorkbenchModal
893
- mode={workbench as WorkbenchMode}
894
- session={view!.session}
895
- diffs={view!.diffs}
896
- nonce={nonce}
897
- selecting={selecting}
898
- activeModule={activeModule}
899
- previewData={state.configuration.previewData}
900
- onModule={setActiveModule}
901
- onSpecSaved={() => void refreshView(view!.session.id)}
902
- onMode={(mode) => setWorkbench(mode)}
903
- onToggleSelect={() => {
904
- setSelecting(!selecting);
905
- setNonce(nonce + 1);
906
- }}
907
- onReload={() => setNonce(nonce + 1)}
908
- onPreviewData={(mode) => void setPreviewData(mode)}
909
- onAskAbout={(path) => {
910
- setDraft(
911
- draft
912
- ? draft + '\n\n' + t('sandbox.app.askAbout', { path })
913
- : t('sandbox.app.askAbout', { path }),
914
- );
915
- setWorkbench('none');
916
- }}
917
- onClose={() => setWorkbench('none')}
918
- />
919
- }
920
- </div>
921
- }
922
- </div>
923
- </Head>
924
- }