@rozenite/vite-plugin 1.12.0 → 2.0.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 (47) hide show
  1. package/README.md +12 -0
  2. package/dist/bundle-dts.d.ts.map +1 -1
  3. package/dist/client-plugin.d.ts.map +1 -1
  4. package/dist/dev-config-module.d.ts.map +1 -1
  5. package/dist/dev-host/assets/index-DU3zXL2c.css +1 -0
  6. package/dist/dev-host/assets/index-Xsb6uYhI.js +13 -0
  7. package/dist/dev-host/index.html +3 -3
  8. package/dist/dev-host/manifest.json +2 -2
  9. package/dist/index.cjs +24 -8
  10. package/dist/index.d.ts +9 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +24 -8
  13. package/dist/react-native-plugin.d.ts.map +1 -1
  14. package/dist/sdk-plugin.d.ts.map +1 -1
  15. package/dist/server-plugin.d.ts.map +1 -1
  16. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  17. package/package.json +10 -10
  18. package/src/bundle-dts.ts +126 -0
  19. package/src/client-plugin.ts +394 -0
  20. package/src/dev-config-module.ts +32 -0
  21. package/src/dev-host/App.tsx +373 -0
  22. package/src/dev-host/components/DispatchForm.tsx +155 -0
  23. package/src/dev-host/components/FlowList.tsx +122 -0
  24. package/src/dev-host/components/MessageDetailsPane.tsx +82 -0
  25. package/src/dev-host/components/MessageLogPane.tsx +102 -0
  26. package/src/dev-host/components/PanelTabs.tsx +41 -0
  27. package/src/dev-host/config.ts +111 -0
  28. package/src/dev-host/constants.ts +12 -0
  29. package/src/dev-host/flow-runtime.ts +318 -0
  30. package/src/dev-host/index.html +12 -0
  31. package/src/dev-host/main.tsx +22 -0
  32. package/src/dev-host/server.ts +61 -0
  33. package/src/dev-host/styles.css +301 -0
  34. package/src/dev-host/types.ts +56 -0
  35. package/src/dev-host/utils.ts +99 -0
  36. package/src/dev-host/vite.config.mts +22 -0
  37. package/src/index.ts +124 -0
  38. package/src/load-config.ts +117 -0
  39. package/src/package-json.ts +16 -0
  40. package/src/react-native-plugin.ts +52 -0
  41. package/src/require-plugin.ts +221 -0
  42. package/src/sdk-plugin.ts +47 -0
  43. package/src/server-plugin.ts +39 -0
  44. package/src/utils.ts +31 -0
  45. package/src/virtual-modules.d.ts +7 -0
  46. package/dist/dev-host/assets/index-CkxvBMpp.css +0 -1
  47. package/dist/dev-host/assets/index-cNKHRwej.js +0 -11
@@ -0,0 +1,373 @@
1
+ import { useEffect, useRef, useState, type FormEvent } from 'react';
2
+ import { PluginHeader, PluginShell, Split, Tabs } from '@rozenite/ui';
3
+ import type {
4
+ DevHostFlowEntry,
5
+ DevHostPanelEntry,
6
+ DevHostPresetEntry,
7
+ DevHostState,
8
+ MessageEntry,
9
+ } from './types.js';
10
+ import { useFlowRunner } from './flow-runtime.js';
11
+ import {
12
+ createMessageEntry,
13
+ formatPayloadForCommandInput,
14
+ getInitialPanel,
15
+ isPluginMessage,
16
+ } from './utils.js';
17
+ import { DispatchForm } from './components/DispatchForm.js';
18
+ import {
19
+ MessageDetailsPane,
20
+ getDispatcherValuesFromMessage,
21
+ } from './components/MessageDetailsPane.js';
22
+ import { MessageLogPane } from './components/MessageLogPane.js';
23
+ import { PanelTabs } from './components/PanelTabs.js';
24
+ import './styles.css';
25
+
26
+ type AppProps = DevHostState & {
27
+ flows: DevHostFlowEntry[];
28
+ presets: DevHostPresetEntry[];
29
+ };
30
+
31
+ type MobileDevtoolsTab = 'log' | 'actions';
32
+
33
+ const useNarrowViewport = () => {
34
+ const [isNarrow, setIsNarrow] = useState(
35
+ () => window.matchMedia('(max-width: 960px)').matches,
36
+ );
37
+
38
+ useEffect(() => {
39
+ const mediaQuery = window.matchMedia('(max-width: 960px)');
40
+ const handleChange = (event: MediaQueryListEvent) => {
41
+ setIsNarrow(event.matches);
42
+ };
43
+
44
+ setIsNarrow(mediaQuery.matches);
45
+ mediaQuery.addEventListener('change', handleChange);
46
+
47
+ return () => mediaQuery.removeEventListener('change', handleChange);
48
+ }, []);
49
+
50
+ return isNarrow;
51
+ };
52
+
53
+ export const App = ({
54
+ packageName,
55
+ packageDescription,
56
+ panels,
57
+ flows,
58
+ presets,
59
+ }: AppProps) => {
60
+ const [activePanel, setActivePanel] = useState<DevHostPanelEntry | null>(() =>
61
+ getInitialPanel(panels),
62
+ );
63
+ const [commandType, setCommandType] = useState('');
64
+ const [commandPayload, setCommandPayload] = useState('');
65
+ const [messages, setMessages] = useState<MessageEntry[]>([]);
66
+ const [selectedMessageId, setSelectedMessageId] = useState<string | null>(
67
+ null,
68
+ );
69
+ const [isDetailsOpen, setIsDetailsOpen] = useState(false);
70
+ const [activeMobileTab, setActiveMobileTab] =
71
+ useState<MobileDevtoolsTab>('log');
72
+ const [iframeLoadNonce, setIframeLoadNonce] = useState(0);
73
+ const iframeRef = useRef<HTMLIFrameElement | null>(null);
74
+ const lastAutoRunLoadRef = useRef(0);
75
+ const isNarrowViewport = useNarrowViewport();
76
+ const {
77
+ flowRuns,
78
+ runFlow,
79
+ stopFlow,
80
+ hasRunningFlow,
81
+ registerMessage,
82
+ resetMessages,
83
+ } = useFlowRunner({
84
+ sendMessage: (type, payload) => {
85
+ iframeRef.current?.contentWindow?.postMessage(
86
+ { pluginId: packageName, type, payload },
87
+ '*',
88
+ );
89
+ appendMessage({ direction: 'in', type, payload });
90
+ },
91
+ });
92
+
93
+ const activeSource = activePanel?.source ?? '';
94
+ const activeLabel = activePanel?.label ?? '';
95
+ const selectedMessage =
96
+ messages.find((message) => message.id === selectedMessageId) ?? null;
97
+ const panelDescription = packageDescription.trim();
98
+ const trimmedCommandType = commandType.trim();
99
+ const trimmedCommandPayload = commandPayload.trim();
100
+ const canDispatch = (() => {
101
+ if (!trimmedCommandType || !trimmedCommandPayload) {
102
+ return false;
103
+ }
104
+
105
+ try {
106
+ JSON.parse(trimmedCommandPayload);
107
+ return true;
108
+ } catch {
109
+ return false;
110
+ }
111
+ })();
112
+
113
+ useEffect(() => {
114
+ document.title = `${packageName} Dev Host`;
115
+ }, [packageName]);
116
+
117
+ useEffect(() => {
118
+ if (iframeLoadNonce === 0) {
119
+ return;
120
+ }
121
+
122
+ if (lastAutoRunLoadRef.current === iframeLoadNonce) {
123
+ return;
124
+ }
125
+
126
+ lastAutoRunLoadRef.current = iframeLoadNonce;
127
+
128
+ flows.forEach((flow) => {
129
+ if (flow.autoRun) {
130
+ runFlow(flow, { autoRun: true });
131
+ }
132
+ });
133
+ }, [flows, iframeLoadNonce, runFlow]);
134
+
135
+ useEffect(() => {
136
+ const handleMessage = (event: MessageEvent) => {
137
+ if (event.source !== iframeRef.current?.contentWindow) {
138
+ return;
139
+ }
140
+
141
+ if (
142
+ typeof event.data !== 'object' ||
143
+ event.data === null ||
144
+ event.data.type !== 'rozenite-message' ||
145
+ !('payload' in event.data) ||
146
+ !isPluginMessage(event.data.payload)
147
+ ) {
148
+ return;
149
+ }
150
+
151
+ const payload = event.data.payload;
152
+ appendMessage({
153
+ direction: 'out',
154
+ type: payload.type,
155
+ payload: payload.payload,
156
+ });
157
+ };
158
+
159
+ window.addEventListener('message', handleMessage);
160
+ return () => window.removeEventListener('message', handleMessage);
161
+ }, []);
162
+
163
+ const appendMessage = (input: Omit<MessageEntry, 'id' | 'date'>) => {
164
+ const nextEntry = createMessageEntry(input);
165
+ registerMessage(nextEntry);
166
+ setMessages((current) => [nextEntry, ...current]);
167
+ };
168
+
169
+ const selectPanel = (value: string) => {
170
+ const nextPanel = panels.find((panel) => panel.source === value);
171
+ if (!nextPanel) {
172
+ return;
173
+ }
174
+
175
+ setActivePanel(nextPanel);
176
+ const nextUrl = new URL(window.location.href);
177
+ nextUrl.searchParams.set('panel', nextPanel.label);
178
+ window.history.replaceState(null, '', nextUrl);
179
+ };
180
+
181
+ const clearMessages = () => {
182
+ resetMessages();
183
+ setMessages([]);
184
+ setSelectedMessageId(null);
185
+ setIsDetailsOpen(false);
186
+ setActiveMobileTab('log');
187
+ };
188
+
189
+ const resetForm = () => {
190
+ setCommandType('');
191
+ setCommandPayload('');
192
+ };
193
+
194
+ const applyPreset = (preset: DevHostPresetEntry) => {
195
+ setCommandType(preset.type);
196
+ setCommandPayload(formatPayloadForCommandInput(preset.payload));
197
+ };
198
+
199
+ const handleDispatch = (event: FormEvent<HTMLFormElement>) => {
200
+ event.preventDefault();
201
+ if (!canDispatch) {
202
+ return;
203
+ }
204
+
205
+ const payload = JSON.parse(trimmedCommandPayload) as unknown;
206
+ iframeRef.current?.contentWindow?.postMessage(
207
+ { pluginId: packageName, type: trimmedCommandType, payload },
208
+ '*',
209
+ );
210
+ appendMessage({
211
+ direction: 'in',
212
+ type: trimmedCommandType,
213
+ payload,
214
+ });
215
+ resetForm();
216
+ };
217
+
218
+ const handleMessageSelect = (messageId: string) => {
219
+ setSelectedMessageId(messageId);
220
+ setIsDetailsOpen(true);
221
+ if (isNarrowViewport) {
222
+ setActiveMobileTab('log');
223
+ }
224
+ };
225
+
226
+ const useMessage = (message: MessageEntry) => {
227
+ const nextValues = getDispatcherValuesFromMessage(message);
228
+ setCommandType(nextValues.commandType);
229
+ setCommandPayload(nextValues.commandPayload);
230
+ setIsDetailsOpen(false);
231
+ if (isNarrowViewport) {
232
+ setActiveMobileTab('actions');
233
+ }
234
+ };
235
+
236
+ const messageLog = (
237
+ <MessageLogPane
238
+ messages={messages}
239
+ onSelectMessage={handleMessageSelect}
240
+ onClearMessages={clearMessages}
241
+ />
242
+ );
243
+
244
+ const actions = (
245
+ <DispatchForm
246
+ commandType={commandType}
247
+ commandPayload={commandPayload}
248
+ flows={flows}
249
+ flowRuns={flowRuns}
250
+ hasRunningFlow={hasRunningFlow}
251
+ presets={presets}
252
+ canDispatch={canDispatch}
253
+ onRunFlow={runFlow}
254
+ onStopFlow={stopFlow}
255
+ onCommandTypeChange={setCommandType}
256
+ onCommandPayloadChange={setCommandPayload}
257
+ onApplyPreset={applyPreset}
258
+ onReset={resetForm}
259
+ onSubmit={handleDispatch}
260
+ />
261
+ );
262
+
263
+ const devtools = isNarrowViewport ? (
264
+ <Tabs
265
+ className="dev-host-mobile-tabs"
266
+ value={activeMobileTab}
267
+ onValueChange={(value) => setActiveMobileTab(value as MobileDevtoolsTab)}
268
+ >
269
+ <Tabs.List
270
+ className="dev-host-mobile-tab-list"
271
+ aria-label="DevTools sections"
272
+ >
273
+ <Tabs.Tab value="log">Log</Tabs.Tab>
274
+ <Tabs.Tab value="actions">Actions</Tabs.Tab>
275
+ </Tabs.List>
276
+ <Tabs.Panel value="log" className="dev-host-mobile-tab-panel">
277
+ {isDetailsOpen && selectedMessage ? (
278
+ <MessageDetailsPane
279
+ selectedMessage={selectedMessage}
280
+ onClose={() => setIsDetailsOpen(false)}
281
+ onUseMessage={useMessage}
282
+ />
283
+ ) : (
284
+ messageLog
285
+ )}
286
+ </Tabs.Panel>
287
+ <Tabs.Panel value="actions" className="dev-host-mobile-tab-panel">
288
+ {actions}
289
+ </Tabs.Panel>
290
+ </Tabs>
291
+ ) : (
292
+ <Split direction="horizontal" className="dev-host-devtools">
293
+ <Split.Pane minSize={35}>
294
+ {isDetailsOpen && selectedMessage ? (
295
+ <Split direction="horizontal">
296
+ <Split.Pane minSize={55}>{messageLog}</Split.Pane>
297
+ <Split.Handle />
298
+ <Split.Pane defaultSize={30} minSize={20}>
299
+ <MessageDetailsPane
300
+ selectedMessage={selectedMessage}
301
+ onClose={() => setIsDetailsOpen(false)}
302
+ onUseMessage={useMessage}
303
+ />
304
+ </Split.Pane>
305
+ </Split>
306
+ ) : (
307
+ messageLog
308
+ )}
309
+ </Split.Pane>
310
+ <Split.Handle />
311
+ <Split.Pane defaultSize={28} minSize={20}>
312
+ {actions}
313
+ </Split.Pane>
314
+ </Split>
315
+ );
316
+
317
+ return (
318
+ <PluginShell className="dev-host">
319
+ <PluginHeader className="dev-host-header">
320
+ <div className="dev-host-header-title">
321
+ <PluginHeader.Title>Rozenite Dev Host</PluginHeader.Title>
322
+ <PluginHeader.Subtitle
323
+ className="dev-host-header-subtitle"
324
+ title={panelDescription}
325
+ >
326
+ {packageName}
327
+ </PluginHeader.Subtitle>
328
+ </div>
329
+ <PluginHeader.Actions>
330
+ <PluginHeader.ThemeSwitcher />
331
+ <PanelTabs
332
+ panels={panels}
333
+ activeSource={activeSource}
334
+ onValueChange={selectPanel}
335
+ />
336
+ </PluginHeader.Actions>
337
+ </PluginHeader>
338
+
339
+ <PluginShell.Body className="dev-host-body">
340
+ <Split
341
+ direction="vertical"
342
+ className="dev-host-main-split"
343
+ autoSaveId="dev-host"
344
+ >
345
+ <Split.Pane
346
+ defaultSize={64}
347
+ minSize={30}
348
+ className="dev-host-preview"
349
+ >
350
+ {panels.length === 0 ? (
351
+ <div className="flex h-full items-center justify-center p-8 text-center text-sm text-muted-foreground">
352
+ No panels were defined in rozenite.config.ts.
353
+ </div>
354
+ ) : (
355
+ <iframe
356
+ key={activeSource}
357
+ ref={iframeRef}
358
+ title={activeLabel || 'Rozenite panel preview'}
359
+ src={activeSource}
360
+ className="dev-host-iframe"
361
+ onLoad={() => setIframeLoadNonce((value) => value + 1)}
362
+ />
363
+ )}
364
+ </Split.Pane>
365
+ <Split.Handle />
366
+ <Split.Pane defaultSize={36} minSize={25}>
367
+ {devtools}
368
+ </Split.Pane>
369
+ </Split>
370
+ </PluginShell.Body>
371
+ </PluginShell>
372
+ );
373
+ };
@@ -0,0 +1,155 @@
1
+ import { Button, Input, Select, Tabs, Textarea } from '@rozenite/ui';
2
+ import { RotateCcw, Send } from 'lucide-react';
3
+ import { useState, type FormEvent } from 'react';
4
+ import type {
5
+ DevHostFlowEntry,
6
+ DevHostFlowRunState,
7
+ DevHostPresetEntry,
8
+ } from '../types.js';
9
+ import { FlowList } from './FlowList.js';
10
+
11
+ type DispatchFormProps = {
12
+ commandType: string;
13
+ commandPayload: string;
14
+ flows: DevHostFlowEntry[];
15
+ flowRuns: DevHostFlowRunState[];
16
+ hasRunningFlow: (flowName: string) => boolean;
17
+ presets: DevHostPresetEntry[];
18
+ canDispatch: boolean;
19
+ onRunFlow: (flow: DevHostFlowEntry) => void;
20
+ onStopFlow: (runId: string) => void;
21
+ onCommandTypeChange: (value: string) => void;
22
+ onCommandPayloadChange: (value: string) => void;
23
+ onApplyPreset: (preset: DevHostPresetEntry) => void;
24
+ onReset: () => void;
25
+ onSubmit: (event: FormEvent<HTMLFormElement>) => void;
26
+ };
27
+
28
+ export const DispatchForm = ({
29
+ commandType,
30
+ commandPayload,
31
+ flows,
32
+ flowRuns,
33
+ hasRunningFlow,
34
+ presets,
35
+ canDispatch,
36
+ onRunFlow,
37
+ onStopFlow,
38
+ onCommandTypeChange,
39
+ onCommandPayloadChange,
40
+ onApplyPreset,
41
+ onReset,
42
+ onSubmit,
43
+ }: DispatchFormProps) => {
44
+ const [activeTab, setActiveTab] = useState<'dispatch' | 'flows'>('dispatch');
45
+
46
+ return (
47
+ <section className="dev-host-pane">
48
+ <header className="dev-host-pane-header">
49
+ <h2 className="dev-host-pane-title">Actions</h2>
50
+ </header>
51
+
52
+ <Tabs
53
+ className="flex min-h-0 flex-1 flex-col"
54
+ value={activeTab}
55
+ onValueChange={(value) => setActiveTab(value as 'dispatch' | 'flows')}
56
+ >
57
+ <div className="dev-host-tabs-header">
58
+ <Tabs.List aria-label="Action modes">
59
+ <Tabs.Tab value="dispatch">Dispatch</Tabs.Tab>
60
+ <Tabs.Tab value="flows">Flows</Tabs.Tab>
61
+ </Tabs.List>
62
+
63
+ {activeTab === 'dispatch' ? (
64
+ <Select
65
+ value={null}
66
+ onValueChange={(value) => {
67
+ const preset = presets.find((item) => item.name === value);
68
+ if (preset) {
69
+ onApplyPreset(preset);
70
+ }
71
+ }}
72
+ >
73
+ <Select.Trigger
74
+ className="w-auto"
75
+ aria-label={
76
+ presets.length > 0 ? 'Open presets' : 'No presets available'
77
+ }
78
+ disabled={presets.length === 0}
79
+ title={presets.length > 0 ? 'Presets' : 'No presets available'}
80
+ >
81
+ <Select.Value>Presets</Select.Value>
82
+ </Select.Trigger>
83
+ <Select.Content align="end">
84
+ {presets.map((preset) => (
85
+ <Select.Item key={preset.name} value={preset.name}>
86
+ {preset.displayName}
87
+ </Select.Item>
88
+ ))}
89
+ </Select.Content>
90
+ </Select>
91
+ ) : null}
92
+ </div>
93
+
94
+ <Tabs.Panel value="dispatch" className="dev-host-tab-panel">
95
+ <form className="dev-host-form" onSubmit={onSubmit}>
96
+ <label className="dev-host-field">
97
+ <span className="dev-host-label">Command</span>
98
+ <Input
99
+ value={commandType}
100
+ onChange={(event) =>
101
+ onCommandTypeChange(event.currentTarget.value)
102
+ }
103
+ placeholder="get-snapshot"
104
+ spellCheck={false}
105
+ />
106
+ </label>
107
+
108
+ <label className="dev-host-field">
109
+ <span className="dev-host-label">Payload</span>
110
+ <Textarea
111
+ value={commandPayload}
112
+ onChange={(event) =>
113
+ onCommandPayloadChange(event.currentTarget.value)
114
+ }
115
+ placeholder='{"example": true}'
116
+ spellCheck={false}
117
+ />
118
+ </label>
119
+
120
+ <div className="dev-host-actions">
121
+ <Button
122
+ variant="outline"
123
+ size="icon"
124
+ onClick={onReset}
125
+ aria-label="Reset dispatcher"
126
+ title="Reset dispatcher"
127
+ >
128
+ <RotateCcw />
129
+ </Button>
130
+ <Button
131
+ type="submit"
132
+ size="icon"
133
+ disabled={!canDispatch}
134
+ aria-label="Dispatch message"
135
+ title="Dispatch message"
136
+ >
137
+ <Send />
138
+ </Button>
139
+ </div>
140
+ </form>
141
+ </Tabs.Panel>
142
+
143
+ <Tabs.Panel value="flows" className="dev-host-tab-panel">
144
+ <FlowList
145
+ flows={flows}
146
+ flowRuns={flowRuns}
147
+ hasRunningFlow={hasRunningFlow}
148
+ onRunFlow={onRunFlow}
149
+ onStopFlow={onStopFlow}
150
+ />
151
+ </Tabs.Panel>
152
+ </Tabs>
153
+ </section>
154
+ );
155
+ };
@@ -0,0 +1,122 @@
1
+ import { Badge, Button, EmptyState } from '@rozenite/ui';
2
+ import { Play, Square } from 'lucide-react';
3
+ import { formatPayloadForCommandInput } from '../utils.js';
4
+ import type { DevHostFlowEntry, DevHostFlowRunState } from '../types.js';
5
+
6
+ type FlowListProps = {
7
+ flows: DevHostFlowEntry[];
8
+ flowRuns: DevHostFlowRunState[];
9
+ hasRunningFlow: (flowName: string) => boolean;
10
+ onRunFlow: (flow: DevHostFlowEntry) => void;
11
+ onStopFlow: (runId: string) => void;
12
+ };
13
+
14
+ const getStatusLabel = (status: DevHostFlowRunState['status']) => {
15
+ switch (status) {
16
+ case 'running':
17
+ return 'Running';
18
+ case 'succeeded':
19
+ return 'Completed';
20
+ case 'failed':
21
+ return 'Failed';
22
+ case 'aborted':
23
+ return 'Stopped';
24
+ }
25
+ };
26
+
27
+ export const FlowList = ({
28
+ flows,
29
+ flowRuns,
30
+ hasRunningFlow,
31
+ onRunFlow,
32
+ onStopFlow,
33
+ }: FlowListProps) => {
34
+ if (flows.length === 0) {
35
+ return (
36
+ <EmptyState
37
+ className="h-full"
38
+ title="No flows configured"
39
+ description="Add flows to rozenite.config.ts to run them here."
40
+ />
41
+ );
42
+ }
43
+
44
+ return (
45
+ <div className="dev-host-flow-list">
46
+ <div className="dev-host-field">
47
+ <span className="dev-host-label">Available flows</span>
48
+ <div className="grid gap-2">
49
+ {flows.map((flow) => {
50
+ const isRunning = hasRunningFlow(flow.name);
51
+
52
+ return (
53
+ <Button
54
+ key={flow.name}
55
+ variant={isRunning ? 'secondary' : 'outline'}
56
+ className="dev-host-flow-button"
57
+ onClick={() => onRunFlow(flow)}
58
+ aria-label={`Run ${flow.displayName}`}
59
+ >
60
+ <span className="dev-host-flow-name">
61
+ <Play />
62
+ {flow.displayName}
63
+ {flow.autoRun ? <Badge variant="outline">Auto</Badge> : null}
64
+ </span>
65
+ <span className="text-xs text-muted-foreground">
66
+ {isRunning ? 'Running' : 'Run'}
67
+ </span>
68
+ </Button>
69
+ );
70
+ })}
71
+ </div>
72
+ </div>
73
+
74
+ {flowRuns.length > 0 ? (
75
+ <div className="dev-host-field">
76
+ <span className="dev-host-label">Flow runs</span>
77
+ <div className="dev-host-flow-runs">
78
+ {flowRuns.map((flowRun) => (
79
+ <div
80
+ key={flowRun.id}
81
+ className="dev-host-flow-state"
82
+ data-status={flowRun.status}
83
+ >
84
+ <div className="dev-host-flow-state-header">
85
+ <span className="dev-host-flow-title">
86
+ {flowRun.flowDisplayName}
87
+ {flowRun.autoRun ? (
88
+ <Badge variant="outline">Auto</Badge>
89
+ ) : null}
90
+ </span>
91
+ {flowRun.status === 'running' ? (
92
+ <Button
93
+ variant="ghost"
94
+ size="compact"
95
+ onClick={() => onStopFlow(flowRun.id)}
96
+ >
97
+ <Square />
98
+ Stop
99
+ </Button>
100
+ ) : null}
101
+ </div>
102
+ <span className="text-xs text-muted-foreground">
103
+ {getStatusLabel(flowRun.status)}
104
+ </span>
105
+ {flowRun.error ? (
106
+ <div className="dev-host-flow-state-error">
107
+ {flowRun.error}
108
+ </div>
109
+ ) : null}
110
+ {flowRun.result != null ? (
111
+ <pre className="dev-host-flow-state-result">
112
+ {formatPayloadForCommandInput(flowRun.result)}
113
+ </pre>
114
+ ) : null}
115
+ </div>
116
+ ))}
117
+ </div>
118
+ </div>
119
+ ) : null}
120
+ </div>
121
+ );
122
+ };