@aion0/forge 0.4.16 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +27 -2
  2. package/RELEASE_NOTES.md +21 -14
  3. package/app/api/agents/route.ts +17 -0
  4. package/app/api/delivery/[id]/route.ts +62 -0
  5. package/app/api/delivery/route.ts +40 -0
  6. package/app/api/mobile-chat/route.ts +13 -7
  7. package/app/api/monitor/route.ts +10 -6
  8. package/app/api/pipelines/[id]/route.ts +16 -3
  9. package/app/api/tasks/route.ts +2 -1
  10. package/app/api/workspace/[id]/agents/route.ts +35 -0
  11. package/app/api/workspace/[id]/memory/route.ts +23 -0
  12. package/app/api/workspace/[id]/smith/route.ts +22 -0
  13. package/app/api/workspace/[id]/stream/route.ts +28 -0
  14. package/app/api/workspace/route.ts +100 -0
  15. package/app/global-error.tsx +10 -4
  16. package/app/icon.ico +0 -0
  17. package/app/layout.tsx +2 -2
  18. package/app/login/LoginForm.tsx +96 -0
  19. package/app/login/page.tsx +7 -98
  20. package/app/page.tsx +2 -2
  21. package/bin/forge-server.mjs +13 -1
  22. package/check-forge-status.sh +9 -0
  23. package/components/ConversationEditor.tsx +411 -0
  24. package/components/ConversationGraphView.tsx +347 -0
  25. package/components/ConversationTerminalView.tsx +303 -0
  26. package/components/Dashboard.tsx +36 -39
  27. package/components/DashboardWrapper.tsx +9 -0
  28. package/components/DeliveryFlowEditor.tsx +491 -0
  29. package/components/DeliveryList.tsx +230 -0
  30. package/components/DeliveryWorkspace.tsx +589 -0
  31. package/components/DocTerminal.tsx +10 -2
  32. package/components/DocsViewer.tsx +10 -2
  33. package/components/HelpTerminal.tsx +11 -6
  34. package/components/InlinePipelineView.tsx +111 -0
  35. package/components/MobileView.tsx +20 -0
  36. package/components/MonitorPanel.tsx +9 -4
  37. package/components/NewTaskModal.tsx +32 -0
  38. package/components/PipelineEditor.tsx +49 -6
  39. package/components/PipelineView.tsx +482 -64
  40. package/components/ProjectDetail.tsx +314 -56
  41. package/components/ProjectManager.tsx +49 -4
  42. package/components/SessionView.tsx +27 -13
  43. package/components/SettingsModal.tsx +790 -124
  44. package/components/SkillsPanel.tsx +31 -8
  45. package/components/TaskBoard.tsx +3 -0
  46. package/components/WebTerminal.tsx +257 -43
  47. package/components/WorkspaceTree.tsx +221 -0
  48. package/components/WorkspaceView.tsx +2245 -0
  49. package/install.sh +2 -2
  50. package/lib/agents/claude-adapter.ts +104 -0
  51. package/lib/agents/generic-adapter.ts +64 -0
  52. package/lib/agents/index.ts +242 -0
  53. package/lib/agents/types.ts +70 -0
  54. package/lib/artifacts.ts +106 -0
  55. package/lib/delivery.ts +787 -0
  56. package/lib/forge-skills/forge-inbox.md +37 -0
  57. package/lib/forge-skills/forge-send.md +40 -0
  58. package/lib/forge-skills/forge-status.md +32 -0
  59. package/lib/forge-skills/forge-workspace-sync.md +37 -0
  60. package/lib/help-docs/00-overview.md +7 -1
  61. package/lib/help-docs/01-settings.md +159 -2
  62. package/lib/help-docs/05-pipelines.md +89 -0
  63. package/lib/help-docs/07-projects.md +35 -1
  64. package/lib/help-docs/11-workspace.md +254 -0
  65. package/lib/help-docs/CLAUDE.md +7 -2
  66. package/lib/init.ts +60 -10
  67. package/lib/pipeline.ts +537 -1
  68. package/lib/settings.ts +115 -22
  69. package/lib/skills.ts +249 -372
  70. package/lib/task-manager.ts +113 -33
  71. package/lib/telegram-bot.ts +33 -1
  72. package/lib/workspace/__tests__/state-machine.test.ts +388 -0
  73. package/lib/workspace/__tests__/workspace.test.ts +311 -0
  74. package/lib/workspace/agent-bus.ts +416 -0
  75. package/lib/workspace/agent-worker.ts +667 -0
  76. package/lib/workspace/backends/api-backend.ts +262 -0
  77. package/lib/workspace/backends/cli-backend.ts +479 -0
  78. package/lib/workspace/index.ts +82 -0
  79. package/lib/workspace/manager.ts +136 -0
  80. package/lib/workspace/orchestrator.ts +1914 -0
  81. package/lib/workspace/persistence.ts +310 -0
  82. package/lib/workspace/presets.ts +170 -0
  83. package/lib/workspace/skill-installer.ts +188 -0
  84. package/lib/workspace/smith-memory.ts +498 -0
  85. package/lib/workspace/types.ts +231 -0
  86. package/lib/workspace/watch-manager.ts +288 -0
  87. package/lib/workspace-standalone.ts +814 -0
  88. package/middleware.ts +1 -0
  89. package/next-env.d.ts +1 -1
  90. package/package.json +4 -1
  91. package/src/config/index.ts +12 -1
  92. package/src/core/db/database.ts +1 -0
  93. package/start.sh +7 -0
@@ -0,0 +1,411 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useMemo } from 'react';
4
+ import {
5
+ ReactFlow,
6
+ Background,
7
+ Controls,
8
+ Handle,
9
+ Position,
10
+ useNodesState,
11
+ useEdgesState,
12
+ type Node,
13
+ type Edge,
14
+ type NodeProps,
15
+ MarkerType,
16
+ } from '@xyflow/react';
17
+ import '@xyflow/react/dist/style.css';
18
+ import YAML from 'yaml';
19
+
20
+ // ─── Color palette ────────────────────────────────────────
21
+
22
+ const AGENT_PALETTE = [
23
+ { bg: '#1e2a4a', border: '#3b5998', accent: '#6b8cce', badge: 'bg-blue-500/20 text-blue-400' },
24
+ { bg: '#2a1e4a', border: '#6b3fa0', accent: '#a07bd6', badge: 'bg-purple-500/20 text-purple-400' },
25
+ { bg: '#1e3a2a', border: '#3a8a5a', accent: '#5ebd7e', badge: 'bg-green-500/20 text-green-400' },
26
+ { bg: '#3a2a1e', border: '#a06030', accent: '#d09060', badge: 'bg-orange-500/20 text-orange-400' },
27
+ { bg: '#3a1e2a', border: '#a03060', accent: '#d06090', badge: 'bg-pink-500/20 text-pink-400' },
28
+ ];
29
+
30
+ // ─── Custom Nodes ─────────────────────────────────────────
31
+
32
+ interface PromptNodeData { label: string; prompt: string; [key: string]: unknown }
33
+ interface AgentNodeData { label: string; agentId: string; agent: string; role: string; colorIndex: number; [key: string]: unknown }
34
+ interface StopNodeData { label: string; condition: string; maxRounds: number; [key: string]: unknown }
35
+ interface ForgeNodeData { label: string; [key: string]: unknown }
36
+
37
+ function PromptNode({ data }: NodeProps<Node<PromptNodeData>>) {
38
+ return (
39
+ <div className="bg-[#1a2a1a] border-2 border-green-500/50 rounded-xl shadow-lg min-w-[220px] max-w-[300px]">
40
+ <div className="px-4 py-2 border-b border-green-500/30 flex items-center gap-2">
41
+ <span className="text-green-400 text-sm">▶</span>
42
+ <span className="text-xs font-bold text-green-300">{data.label}</span>
43
+ </div>
44
+ <div className="px-4 py-2">
45
+ <div className="text-[9px] text-gray-400 whitespace-pre-wrap line-clamp-3">{data.prompt || 'No prompt'}</div>
46
+ </div>
47
+ <Handle type="source" position={Position.Bottom} className="!bg-green-400 !w-3 !h-3" />
48
+ </div>
49
+ );
50
+ }
51
+
52
+ function AgentNode({ data }: NodeProps<Node<AgentNodeData>>) {
53
+ const palette = AGENT_PALETTE[data.colorIndex % AGENT_PALETTE.length];
54
+ return (
55
+ <div
56
+ className="rounded-xl shadow-lg min-w-[200px] max-w-[260px]"
57
+ style={{ background: palette.bg, border: `2px solid ${palette.border}` }}
58
+ >
59
+ <Handle type="target" position={Position.Top} className="!w-3 !h-3" style={{ background: palette.accent }} />
60
+
61
+ <div className="px-4 py-2 flex items-center gap-2" style={{ borderBottom: `1px solid ${palette.border}` }}>
62
+ <span className={`text-[8px] px-1.5 py-0.5 rounded font-bold ${palette.badge}`}>{data.agent}</span>
63
+ <span className="text-xs font-bold text-white">{data.agentId}</span>
64
+ </div>
65
+ <div className="px-4 py-2">
66
+ <div className="text-[9px] text-gray-400 line-clamp-3">{data.role || 'No role defined'}</div>
67
+ </div>
68
+
69
+ <Handle type="source" position={Position.Bottom} className="!w-3 !h-3" style={{ background: palette.accent }} />
70
+ </div>
71
+ );
72
+ }
73
+
74
+ function ForgeNode({ data }: NodeProps<Node<ForgeNodeData>>) {
75
+ return (
76
+ <div className="bg-[#1a1a3a] border-2 border-[#7c5bf0]/60 rounded-xl shadow-lg min-w-[180px]">
77
+ <Handle type="target" position={Position.Top} className="!bg-[#7c5bf0] !w-3 !h-3" />
78
+ <div className="px-4 py-3 flex items-center gap-2 justify-center">
79
+ <span className="text-[#7c5bf0] text-sm">⚡</span>
80
+ <span className="text-xs font-bold text-[#7c5bf0]">{data.label}</span>
81
+ </div>
82
+ <Handle type="source" position={Position.Bottom} className="!bg-[#7c5bf0] !w-3 !h-3" />
83
+ </div>
84
+ );
85
+ }
86
+
87
+ function StopNode({ data }: NodeProps<Node<StopNodeData>>) {
88
+ return (
89
+ <div className="bg-[#2a1a1a] border-2 border-red-500/50 rounded-xl shadow-lg min-w-[200px]">
90
+ <Handle type="target" position={Position.Top} className="!bg-red-400 !w-3 !h-3" />
91
+ <div className="px-4 py-2 border-b border-red-500/30 flex items-center gap-2">
92
+ <span className="text-red-400 text-sm">■</span>
93
+ <span className="text-xs font-bold text-red-300">{data.label}</span>
94
+ </div>
95
+ <div className="px-4 py-2 space-y-0.5">
96
+ {data.condition && <div className="text-[9px] text-gray-400">{data.condition}</div>}
97
+ <div className="text-[8px] text-gray-500">Max {data.maxRounds} rounds</div>
98
+ </div>
99
+ </div>
100
+ );
101
+ }
102
+
103
+ const nodeTypes = {
104
+ prompt: PromptNode,
105
+ agent: AgentNode,
106
+ forge: ForgeNode,
107
+ stop: StopNode,
108
+ };
109
+
110
+ // ─── Parse YAML → ReactFlow nodes/edges ───────────────────
111
+
112
+ interface ConvParsed {
113
+ name: string;
114
+ description?: string;
115
+ input?: Record<string, string>;
116
+ agents: { id: string; agent: string; role: string; project?: string }[];
117
+ maxRounds: number;
118
+ stopCondition?: string;
119
+ initialPrompt: string;
120
+ }
121
+
122
+ function parseConvYaml(raw: string): ConvParsed | null {
123
+ try {
124
+ const p = YAML.parse(raw);
125
+ if (!p || p.type !== 'conversation') return null;
126
+ return {
127
+ name: p.name || 'unnamed',
128
+ description: p.description,
129
+ input: p.input,
130
+ agents: p.agents || [],
131
+ maxRounds: p.max_rounds || p.maxRounds || 10,
132
+ stopCondition: p.stop_condition || p.stopCondition || '',
133
+ initialPrompt: p.initial_prompt || p.initialPrompt || '',
134
+ };
135
+ } catch { return null; }
136
+ }
137
+
138
+ function buildFlowGraph(conv: ConvParsed): { nodes: Node[]; edges: Edge[] } {
139
+ const nodes: Node[] = [];
140
+ const edges: Edge[] = [];
141
+ const agentCount = conv.agents.length;
142
+
143
+ // Layout constants
144
+ const centerX = 300;
145
+ const startY = 30;
146
+ const agentSpacing = 220;
147
+ const verticalGap = 140;
148
+
149
+ // 1. Initial Prompt node
150
+ nodes.push({
151
+ id: 'prompt',
152
+ type: 'prompt',
153
+ position: { x: centerX - 110, y: startY },
154
+ data: { label: 'Initial Prompt', prompt: conv.initialPrompt },
155
+ draggable: true,
156
+ });
157
+
158
+ // 2. Forge broker node
159
+ const forgeY = startY + verticalGap;
160
+ nodes.push({
161
+ id: 'forge',
162
+ type: 'forge',
163
+ position: { x: centerX - 90, y: forgeY },
164
+ data: { label: 'Forge Broker' },
165
+ draggable: true,
166
+ });
167
+
168
+ edges.push({
169
+ id: 'prompt-forge',
170
+ source: 'prompt',
171
+ target: 'forge',
172
+ markerEnd: { type: MarkerType.ArrowClosed, color: '#5ebd7e' },
173
+ style: { stroke: '#5ebd7e', strokeWidth: 2 },
174
+ animated: true,
175
+ label: 'start',
176
+ labelStyle: { fill: '#888', fontSize: 9 },
177
+ });
178
+
179
+ // 3. Agent nodes — spread horizontally
180
+ const agentY = forgeY + verticalGap;
181
+ const totalWidth = (agentCount - 1) * agentSpacing;
182
+ const agentStartX = centerX - totalWidth / 2;
183
+
184
+ conv.agents.forEach((a, i) => {
185
+ const x = agentStartX + i * agentSpacing - 100;
186
+ nodes.push({
187
+ id: `agent-${a.id}`,
188
+ type: 'agent',
189
+ position: { x, y: agentY },
190
+ data: { label: a.id, agentId: a.id, agent: a.agent, role: a.role, colorIndex: i },
191
+ draggable: true,
192
+ });
193
+
194
+ // Forge → Agent (send prompt)
195
+ edges.push({
196
+ id: `forge-agent-${a.id}`,
197
+ source: 'forge',
198
+ target: `agent-${a.id}`,
199
+ markerEnd: { type: MarkerType.ArrowClosed, color: AGENT_PALETTE[i % AGENT_PALETTE.length].accent },
200
+ style: { stroke: AGENT_PALETTE[i % AGENT_PALETTE.length].accent, strokeWidth: 2 },
201
+ animated: true,
202
+ label: `send R${i + 1}`,
203
+ labelStyle: { fill: '#888', fontSize: 8 },
204
+ });
205
+
206
+ // Agent → Forge (response back) — curved
207
+ edges.push({
208
+ id: `agent-${a.id}-forge`,
209
+ source: `agent-${a.id}`,
210
+ target: 'forge',
211
+ markerEnd: { type: MarkerType.ArrowClosed, color: AGENT_PALETTE[i % AGENT_PALETTE.length].accent },
212
+ style: { stroke: AGENT_PALETTE[i % AGENT_PALETTE.length].accent, strokeWidth: 1, strokeDasharray: '6 3' },
213
+ animated: true,
214
+ label: 'response',
215
+ labelStyle: { fill: '#666', fontSize: 8 },
216
+ type: 'smoothstep',
217
+ });
218
+ });
219
+
220
+ // 4. Inter-agent data flow edges (Agent A output → Agent B input via Forge)
221
+ if (agentCount >= 2) {
222
+ for (let i = 0; i < agentCount - 1; i++) {
223
+ const from = conv.agents[i];
224
+ const to = conv.agents[i + 1];
225
+ edges.push({
226
+ id: `flow-${from.id}-${to.id}`,
227
+ source: `agent-${from.id}`,
228
+ target: `agent-${to.id}`,
229
+ markerEnd: { type: MarkerType.ArrowClosed, color: '#7c5bf0' },
230
+ style: { stroke: '#7c5bf0', strokeWidth: 2, strokeDasharray: '4 4' },
231
+ animated: true,
232
+ label: 'context →',
233
+ labelStyle: { fill: '#7c5bf0', fontSize: 9, fontWeight: 600 },
234
+ type: 'smoothstep',
235
+ });
236
+ }
237
+ // Loop back: last agent → first agent (next round)
238
+ if (conv.maxRounds > 1) {
239
+ edges.push({
240
+ id: `loop-${conv.agents[agentCount - 1].id}-${conv.agents[0].id}`,
241
+ source: `agent-${conv.agents[agentCount - 1].id}`,
242
+ target: `agent-${conv.agents[0].id}`,
243
+ markerEnd: { type: MarkerType.ArrowClosed, color: '#d09060' },
244
+ style: { stroke: '#d09060', strokeWidth: 2, strokeDasharray: '8 4' },
245
+ animated: true,
246
+ label: `next round`,
247
+ labelStyle: { fill: '#d09060', fontSize: 9, fontWeight: 600 },
248
+ type: 'smoothstep',
249
+ });
250
+ }
251
+ }
252
+
253
+ // 5. Stop condition node
254
+ const stopY = agentY + verticalGap + 20;
255
+ nodes.push({
256
+ id: 'stop',
257
+ type: 'stop',
258
+ position: { x: centerX - 100, y: stopY },
259
+ data: { label: 'Stop Condition', condition: conv.stopCondition || 'max rounds reached', maxRounds: conv.maxRounds },
260
+ draggable: true,
261
+ });
262
+
263
+ // All agents → stop
264
+ conv.agents.forEach((a, i) => {
265
+ edges.push({
266
+ id: `agent-${a.id}-stop`,
267
+ source: `agent-${a.id}`,
268
+ target: 'stop',
269
+ markerEnd: { type: MarkerType.ArrowClosed, color: '#ef4444' },
270
+ style: { stroke: '#ef4444', strokeWidth: 1, opacity: 0.4 },
271
+ label: 'DONE?',
272
+ labelStyle: { fill: '#666', fontSize: 7 },
273
+ type: 'smoothstep',
274
+ });
275
+ });
276
+
277
+ return { nodes, edges };
278
+ }
279
+
280
+ // ─── Main Component ───────────────────────────────────────
281
+
282
+ export default function ConversationEditor({ initialYaml, onSave, onClose }: {
283
+ initialYaml: string;
284
+ onSave: (yaml: string) => void;
285
+ onClose: () => void;
286
+ }) {
287
+ const [yamlText, setYamlText] = useState(initialYaml);
288
+ const [error, setError] = useState('');
289
+ const [showYaml, setShowYaml] = useState(false);
290
+ const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
291
+ const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
292
+
293
+ const parsed = useMemo(() => parseConvYaml(yamlText), [yamlText]);
294
+
295
+ // Rebuild graph when YAML changes
296
+ useEffect(() => {
297
+ if (!parsed) { setNodes([]); setEdges([]); return; }
298
+ const graph = buildFlowGraph(parsed);
299
+ setNodes(graph.nodes);
300
+ setEdges(graph.edges);
301
+ }, [parsed, setNodes, setEdges]);
302
+
303
+ const validate = (text: string): string => {
304
+ try {
305
+ const p = YAML.parse(text);
306
+ if (!p.name) return 'Missing "name"';
307
+ if (p.type !== 'conversation') return 'type must be "conversation"';
308
+ if (!p.agents || !Array.isArray(p.agents) || p.agents.length < 2) return 'Need at least 2 agents';
309
+ for (const a of p.agents) {
310
+ if (!a.id) return 'Agent missing "id"';
311
+ if (!a.agent) return `Agent "${a.id}" missing "agent"`;
312
+ }
313
+ if (!p.initial_prompt && !p.initialPrompt) return 'Missing "initial_prompt"';
314
+ return '';
315
+ } catch (e: any) {
316
+ return `YAML error: ${e.message}`;
317
+ }
318
+ };
319
+
320
+ const handleSave = () => {
321
+ const err = validate(yamlText);
322
+ if (err) { setError(err); return; }
323
+ onSave(yamlText);
324
+ };
325
+
326
+ return (
327
+ <div className="flex-1 flex flex-col min-h-0" style={{ background: '#0a0a1a' }}>
328
+ {/* Top bar */}
329
+ <div className="h-10 border-b border-[#3a3a5a] flex items-center px-4 gap-3 shrink-0">
330
+ <span className="text-xs font-bold text-white">Conversation Editor</span>
331
+ {parsed && <span className="text-[10px] text-gray-400 font-mono">{parsed.name}</span>}
332
+ {parsed && (
333
+ <span className="text-[8px] px-1.5 py-0.5 rounded bg-purple-500/20 text-purple-400">
334
+ {parsed.agents.length} agents · {parsed.maxRounds} rounds
335
+ </span>
336
+ )}
337
+ <div className="flex-1" />
338
+ {error && <span className="text-[9px] text-red-400 truncate max-w-[250px]">{error}</span>}
339
+ <button
340
+ onClick={() => setShowYaml(v => !v)}
341
+ className={`text-[10px] px-2 py-0.5 rounded border ${showYaml ? 'border-[#7c5bf0] text-[#7c5bf0]' : 'border-[#3a3a5a] text-gray-400'} hover:text-white`}
342
+ >{showYaml ? 'Graph' : 'YAML'}</button>
343
+ <button onClick={handleSave} className="text-xs px-3 py-1 bg-green-600 text-white rounded hover:opacity-90">Save</button>
344
+ <button
345
+ onClick={() => { if (!yamlText || yamlText === initialYaml || confirm('Discard changes?')) onClose(); }}
346
+ className="text-xs px-3 py-1 text-gray-400 hover:text-white"
347
+ >Close</button>
348
+ </div>
349
+
350
+ {/* Content */}
351
+ {showYaml ? (
352
+ <textarea
353
+ value={yamlText}
354
+ onChange={e => { setYamlText(e.target.value); setError(''); }}
355
+ className="flex-1 p-4 text-xs font-mono bg-[#0a0a1a] text-gray-300 resize-none focus:outline-none leading-relaxed"
356
+ spellCheck={false}
357
+ />
358
+ ) : (
359
+ <div className="flex-1 relative">
360
+ {parsed ? (
361
+ <ReactFlow
362
+ nodes={nodes}
363
+ edges={edges}
364
+ onNodesChange={onNodesChange}
365
+ onEdgesChange={onEdgesChange}
366
+ nodeTypes={nodeTypes}
367
+ fitView
368
+ fitViewOptions={{ padding: 0.3 }}
369
+ nodesConnectable={false}
370
+ style={{ background: '#0a0a1a' }}
371
+ minZoom={0.3}
372
+ maxZoom={2}
373
+ >
374
+ <Background color="#1a1a3a" gap={20} />
375
+ <Controls />
376
+ </ReactFlow>
377
+ ) : (
378
+ <div className="flex-1 flex items-center justify-center h-full">
379
+ <div className="text-center space-y-2">
380
+ <div className="text-sm text-gray-500">Invalid or empty conversation YAML</div>
381
+ <button
382
+ onClick={() => setShowYaml(true)}
383
+ className="text-xs px-3 py-1 bg-[#7c5bf0] text-white rounded hover:opacity-90"
384
+ >Edit YAML</button>
385
+ </div>
386
+ </div>
387
+ )}
388
+
389
+ {/* Floating legend */}
390
+ {parsed && (
391
+ <div className="absolute bottom-4 left-4 bg-[#0a0a1a]/90 border border-[#3a3a5a] rounded-lg p-3 space-y-1.5 backdrop-blur-sm">
392
+ <div className="text-[8px] font-bold text-gray-400 uppercase">Legend</div>
393
+ <div className="flex items-center gap-2 text-[8px] text-gray-400">
394
+ <span className="w-3 h-0.5 bg-green-500 inline-block" /> Initial prompt
395
+ </div>
396
+ <div className="flex items-center gap-2 text-[8px] text-gray-400">
397
+ <span className="w-3 h-0.5 bg-[#7c5bf0] inline-block" style={{ borderBottom: '2px dashed #7c5bf0' }} /> Context flow
398
+ </div>
399
+ <div className="flex items-center gap-2 text-[8px] text-gray-400">
400
+ <span className="w-3 h-0.5 bg-orange-500 inline-block" style={{ borderBottom: '2px dashed #d09060' }} /> Next round loop
401
+ </div>
402
+ <div className="flex items-center gap-2 text-[8px] text-gray-400">
403
+ <span className="w-3 h-0.5 bg-red-500/40 inline-block" /> Stop check
404
+ </div>
405
+ </div>
406
+ )}
407
+ </div>
408
+ )}
409
+ </div>
410
+ );
411
+ }