@flowdular/sandbox 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.
- package/LICENSE +21 -0
- package/README.md +696 -0
- package/bin/flowdular-sandbox.mjs +394 -0
- package/bin/register-types.mjs +53 -0
- package/dist/client/assets/App-ClMOrpyg.js +11 -0
- package/dist/client/assets/PreviewHost-BHiHrfbQ.js +1 -0
- package/dist/client/assets/auto-buGEq6de.js +3 -0
- package/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/dist/client/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dist/client/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dist/client/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dist/client/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/dist/client/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
- package/dist/client/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
- package/dist/client/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
- package/dist/client/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
- package/dist/client/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
- package/dist/client/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
- package/dist/client/assets/index-CvwhlQ87.js +4 -0
- package/dist/client/assets/styles-BPWaGf55.js +3 -0
- package/dist/client/assets/styles-Dx1_KOag.css +1 -0
- package/dist/client/favicon.svg +1 -0
- package/dist/server/assets/auto-CVGNYyb3.js +12785 -0
- package/dist/server/assets/chunk-DDJLRBDX-3OUNo51g.js +4125 -0
- package/dist/server/assets/nodefs-DVSI7kG0.js +24 -0
- package/dist/server/assets/opfs-ahp-D56iokt1.js +388 -0
- package/dist/server/assets/token-Bvaq7Anz.js +55 -0
- package/dist/server/assets/token-util-BqSvdIPQ.js +3 -0
- package/dist/server/assets/token-util-CktFFT4z.js +494 -0
- package/dist/server/entry.js +91671 -0
- package/dist/server/favicon.svg +1 -0
- package/dist/server/index.html +161 -0
- package/index.html +159 -0
- package/internal/coding-agent/package.json +44 -0
- package/internal/coding-agent/src/drivers/byok.ts +322 -0
- package/internal/coding-agent/src/drivers/claude-code.ts +290 -0
- package/internal/coding-agent/src/drivers/codex.ts +281 -0
- package/internal/coding-agent/src/index.ts +56 -0
- package/internal/coding-agent/src/registry.ts +119 -0
- package/internal/coding-agent/src/roles/contract.ts +60 -0
- package/internal/coding-agent/src/roles/defaults.ts +134 -0
- package/internal/coding-agent/src/roles/handoff.ts +41 -0
- package/internal/coding-agent/src/roles/registry.ts +156 -0
- package/internal/coding-agent/src/roles/skills.ts +72 -0
- package/internal/coding-agent/src/roles/sync.ts +88 -0
- package/internal/coding-agent/src/types.ts +102 -0
- package/internal/coding-agent/src/workspace.ts +275 -0
- package/octane.config.ts +36 -0
- package/package.json +68 -0
- package/public/favicon.svg +1 -0
- package/public/og.png +0 -0
- package/src/App.tsrx +924 -0
- package/src/client/ApprovalHandoff.tsrx +39 -0
- package/src/client/ChatPane.tsrx +694 -0
- package/src/client/ConnectView.tsrx +86 -0
- package/src/client/DiffPane.tsrx +150 -0
- package/src/client/EjectModal.tsrx +571 -0
- package/src/client/GateResults.tsrx +60 -0
- package/src/client/GitHubSettingsModal.tsrx +219 -0
- package/src/client/NewSessionForm.tsrx +214 -0
- package/src/client/SessionBar.tsrx +151 -0
- package/src/client/SessionPicker.tsrx +472 -0
- package/src/client/SpecEditor.tsrx +109 -0
- package/src/client/SpecReviewCard.tsrx +321 -0
- package/src/client/WorkCard.tsrx +128 -0
- package/src/client/WorkbenchModal.tsrx +161 -0
- package/src/client/WorkspaceMenu.tsrx +100 -0
- package/src/client/api.ts +797 -0
- package/src/client/attachments.ts +73 -0
- package/src/client/dashboard.ts +46 -0
- package/src/client/github-settings.ts +48 -0
- package/src/client/highlight.ts +105 -0
- package/src/client/i18n.ts +32 -0
- package/src/client/locales/en.json +466 -0
- package/src/client/locales/pl.json +466 -0
- package/src/client/session-labels.ts +77 -0
- package/src/client/state.ts +108 -0
- package/src/index.ts +10 -0
- package/src/preview/PreviewHost.tsrx +373 -0
- package/src/preview/load-module.ts +16 -0
- package/src/server/attachments.ts +308 -0
- package/src/server/auto-review.ts +181 -0
- package/src/server/checkpoints.ts +90 -0
- package/src/server/config.ts +460 -0
- package/src/server/dashboard.ts +245 -0
- package/src/server/delivery/configuration.ts +193 -0
- package/src/server/delivery/git-pr.ts +1473 -0
- package/src/server/delivery/index.ts +83 -0
- package/src/server/delivery/local.ts +151 -0
- package/src/server/delivery/official-modules.ts +516 -0
- package/src/server/delivery/plan.ts +218 -0
- package/src/server/delivery/policies.ts +239 -0
- package/src/server/delivery/record.ts +41 -0
- package/src/server/delivery/steps.ts +464 -0
- package/src/server/delivery/types.ts +158 -0
- package/src/server/dependencies.ts +88 -0
- package/src/server/diff.ts +230 -0
- package/src/server/gates.ts +388 -0
- package/src/server/index.ts +139 -0
- package/src/server/path-guard.ts +282 -0
- package/src/server/planning.ts +749 -0
- package/src/server/platform-client.ts +189 -0
- package/src/server/preview-database-host.ts +340 -0
- package/src/server/preview-database-protocol.ts +163 -0
- package/src/server/preview-database-proxy.ts +356 -0
- package/src/server/preview-database.ts +109 -0
- package/src/server/preview-ipc.ts +15 -0
- package/src/server/preview-modules.ts +83 -0
- package/src/server/preview-revision.ts +44 -0
- package/src/server/preview-runtime.ts +467 -0
- package/src/server/preview-worker-manager.ts +477 -0
- package/src/server/preview-worker.ts +168 -0
- package/src/server/reference.ts +251 -0
- package/src/server/routes.ts +2258 -0
- package/src/server/runtime.ts +188 -0
- package/src/server/session-owner.ts +23 -0
- package/src/server/sessions.ts +849 -0
- package/src/server/spec.ts +471 -0
- package/src/server/turn-lifecycle.ts +56 -0
- package/src/server/turns.ts +993 -0
- package/src/server/workspace-install.ts +266 -0
- package/src/server/workspace-root.ts +73 -0
- package/src/styles.css +1573 -0
- package/vite.config.ts +134 -0
package/src/App.tsrx
ADDED
|
@@ -0,0 +1,924 @@
|
|
|
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
|
+
}
|