@flowdular/sdk 0.1.0 → 0.2.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 (174) 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 +1 -1
  28. package/packages/database-testing/package.json +1 -1
  29. package/packages/dev-console/package.json +1 -1
  30. package/packages/harness/package.json +1 -1
  31. package/packages/kernel/package.json +1 -1
  32. package/packages/server/package.json +1 -1
  33. package/packages/server/src/application-routes.ts +80 -8
  34. package/packages/server/src/index.ts +4 -1
  35. package/packages/server/src/web-html.ts +9 -5
  36. package/packages/server/src/web.ts +59 -7
  37. package/packages/ui/package.json +1 -1
  38. package/packages/coding-agent/package.json +0 -44
  39. package/packages/coding-agent/src/drivers/byok.ts +0 -322
  40. package/packages/coding-agent/src/drivers/claude-code.ts +0 -290
  41. package/packages/coding-agent/src/drivers/codex.ts +0 -281
  42. package/packages/coding-agent/src/index.ts +0 -56
  43. package/packages/coding-agent/src/registry.ts +0 -119
  44. package/packages/coding-agent/src/roles/contract.ts +0 -60
  45. package/packages/coding-agent/src/roles/defaults.ts +0 -134
  46. package/packages/coding-agent/src/roles/handoff.ts +0 -41
  47. package/packages/coding-agent/src/roles/registry.ts +0 -156
  48. package/packages/coding-agent/src/roles/skills.ts +0 -72
  49. package/packages/coding-agent/src/roles/sync.ts +0 -88
  50. package/packages/coding-agent/src/types.ts +0 -102
  51. package/packages/coding-agent/src/workspace.ts +0 -275
  52. package/packages/sandbox/README.md +0 -691
  53. package/packages/sandbox/bin/flowdular-sandbox.mjs +0 -393
  54. package/packages/sandbox/dist/client/assets/App-ClMOrpyg.js +0 -11
  55. package/packages/sandbox/dist/client/assets/PreviewHost-BHiHrfbQ.js +0 -1
  56. package/packages/sandbox/dist/client/assets/auto-buGEq6de.js +0 -3
  57. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  58. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  59. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  60. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  61. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  62. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  63. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  64. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  65. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  66. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  67. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  68. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  69. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  70. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  71. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  72. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  73. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  74. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  75. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  76. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  77. package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
  78. package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
  79. package/packages/sandbox/dist/client/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
  80. package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
  81. package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
  82. package/packages/sandbox/dist/client/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
  83. package/packages/sandbox/dist/client/assets/index-CvwhlQ87.js +0 -4
  84. package/packages/sandbox/dist/client/assets/styles-BPWaGf55.js +0 -3
  85. package/packages/sandbox/dist/client/assets/styles-Dx1_KOag.css +0 -1
  86. package/packages/sandbox/dist/client/favicon.svg +0 -1
  87. package/packages/sandbox/dist/server/assets/auto-CVGNYyb3.js +0 -12785
  88. package/packages/sandbox/dist/server/assets/chunk-DDJLRBDX-3OUNo51g.js +0 -4125
  89. package/packages/sandbox/dist/server/assets/nodefs-DVSI7kG0.js +0 -24
  90. package/packages/sandbox/dist/server/assets/opfs-ahp-D56iokt1.js +0 -388
  91. package/packages/sandbox/dist/server/assets/token-Bvaq7Anz.js +0 -55
  92. package/packages/sandbox/dist/server/assets/token-util-BqSvdIPQ.js +0 -3
  93. package/packages/sandbox/dist/server/assets/token-util-CktFFT4z.js +0 -494
  94. package/packages/sandbox/dist/server/entry.js +0 -91671
  95. package/packages/sandbox/dist/server/favicon.svg +0 -1
  96. package/packages/sandbox/dist/server/index.html +0 -161
  97. package/packages/sandbox/index.html +0 -159
  98. package/packages/sandbox/octane.config.ts +0 -36
  99. package/packages/sandbox/package.json +0 -83
  100. package/packages/sandbox/public/favicon.svg +0 -1
  101. package/packages/sandbox/public/og.png +0 -0
  102. package/packages/sandbox/src/App.tsrx +0 -924
  103. package/packages/sandbox/src/client/ApprovalHandoff.tsrx +0 -39
  104. package/packages/sandbox/src/client/ChatPane.tsrx +0 -694
  105. package/packages/sandbox/src/client/ConnectView.tsrx +0 -86
  106. package/packages/sandbox/src/client/DiffPane.tsrx +0 -150
  107. package/packages/sandbox/src/client/EjectModal.tsrx +0 -571
  108. package/packages/sandbox/src/client/GateResults.tsrx +0 -60
  109. package/packages/sandbox/src/client/GitHubSettingsModal.tsrx +0 -219
  110. package/packages/sandbox/src/client/NewSessionForm.tsrx +0 -214
  111. package/packages/sandbox/src/client/SessionBar.tsrx +0 -151
  112. package/packages/sandbox/src/client/SessionPicker.tsrx +0 -472
  113. package/packages/sandbox/src/client/SpecEditor.tsrx +0 -109
  114. package/packages/sandbox/src/client/SpecReviewCard.tsrx +0 -321
  115. package/packages/sandbox/src/client/WorkCard.tsrx +0 -128
  116. package/packages/sandbox/src/client/WorkbenchModal.tsrx +0 -161
  117. package/packages/sandbox/src/client/WorkspaceMenu.tsrx +0 -100
  118. package/packages/sandbox/src/client/api.ts +0 -797
  119. package/packages/sandbox/src/client/attachments.ts +0 -73
  120. package/packages/sandbox/src/client/dashboard.ts +0 -46
  121. package/packages/sandbox/src/client/github-settings.ts +0 -48
  122. package/packages/sandbox/src/client/highlight.ts +0 -105
  123. package/packages/sandbox/src/client/i18n.ts +0 -32
  124. package/packages/sandbox/src/client/locales/en.json +0 -466
  125. package/packages/sandbox/src/client/locales/pl.json +0 -466
  126. package/packages/sandbox/src/client/session-labels.ts +0 -77
  127. package/packages/sandbox/src/client/state.ts +0 -108
  128. package/packages/sandbox/src/index.ts +0 -10
  129. package/packages/sandbox/src/preview/PreviewHost.tsrx +0 -373
  130. package/packages/sandbox/src/preview/load-module.ts +0 -16
  131. package/packages/sandbox/src/server/attachments.ts +0 -308
  132. package/packages/sandbox/src/server/auto-review.ts +0 -181
  133. package/packages/sandbox/src/server/checkpoints.ts +0 -90
  134. package/packages/sandbox/src/server/config.ts +0 -460
  135. package/packages/sandbox/src/server/dashboard.ts +0 -245
  136. package/packages/sandbox/src/server/delivery/configuration.ts +0 -193
  137. package/packages/sandbox/src/server/delivery/git-pr.ts +0 -1473
  138. package/packages/sandbox/src/server/delivery/index.ts +0 -83
  139. package/packages/sandbox/src/server/delivery/local.ts +0 -151
  140. package/packages/sandbox/src/server/delivery/official-modules.ts +0 -516
  141. package/packages/sandbox/src/server/delivery/plan.ts +0 -218
  142. package/packages/sandbox/src/server/delivery/policies.ts +0 -239
  143. package/packages/sandbox/src/server/delivery/record.ts +0 -41
  144. package/packages/sandbox/src/server/delivery/steps.ts +0 -464
  145. package/packages/sandbox/src/server/delivery/types.ts +0 -158
  146. package/packages/sandbox/src/server/dependencies.ts +0 -88
  147. package/packages/sandbox/src/server/diff.ts +0 -230
  148. package/packages/sandbox/src/server/gates.ts +0 -388
  149. package/packages/sandbox/src/server/index.ts +0 -139
  150. package/packages/sandbox/src/server/path-guard.ts +0 -282
  151. package/packages/sandbox/src/server/planning.ts +0 -749
  152. package/packages/sandbox/src/server/platform-client.ts +0 -189
  153. package/packages/sandbox/src/server/preview-database-host.ts +0 -340
  154. package/packages/sandbox/src/server/preview-database-protocol.ts +0 -163
  155. package/packages/sandbox/src/server/preview-database-proxy.ts +0 -356
  156. package/packages/sandbox/src/server/preview-database.ts +0 -109
  157. package/packages/sandbox/src/server/preview-ipc.ts +0 -15
  158. package/packages/sandbox/src/server/preview-modules.ts +0 -83
  159. package/packages/sandbox/src/server/preview-revision.ts +0 -44
  160. package/packages/sandbox/src/server/preview-runtime.ts +0 -467
  161. package/packages/sandbox/src/server/preview-worker-manager.ts +0 -436
  162. package/packages/sandbox/src/server/preview-worker.ts +0 -168
  163. package/packages/sandbox/src/server/reference.ts +0 -251
  164. package/packages/sandbox/src/server/routes.ts +0 -2258
  165. package/packages/sandbox/src/server/runtime.ts +0 -188
  166. package/packages/sandbox/src/server/session-owner.ts +0 -23
  167. package/packages/sandbox/src/server/sessions.ts +0 -849
  168. package/packages/sandbox/src/server/spec.ts +0 -471
  169. package/packages/sandbox/src/server/turn-lifecycle.ts +0 -56
  170. package/packages/sandbox/src/server/turns.ts +0 -993
  171. package/packages/sandbox/src/server/workspace-install.ts +0 -266
  172. package/packages/sandbox/src/server/workspace-root.ts +0 -73
  173. package/packages/sandbox/src/styles.css +0 -1573
  174. 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
- }