@meshxdata/fops 0.1.51 → 0.1.53

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 (90) hide show
  1. package/CHANGELOG.md +207 -21
  2. package/package.json +2 -6
  3. package/src/agent/agent.js +6 -0
  4. package/src/commands/setup.js +34 -0
  5. package/src/doctor.js +11 -8
  6. package/src/fleet-registry.js +38 -2
  7. package/src/plugins/__test-fixtures__/fake-plugin.js +2 -0
  8. package/src/plugins/__test-fixtures__/no-register-plugin.js +2 -0
  9. package/src/plugins/__test-fixtures__/with-register/index.js +2 -0
  10. package/src/plugins/__test-fixtures__/without-register/index.js +2 -0
  11. package/src/plugins/api.js +4 -0
  12. package/src/plugins/builtins/docker-compose.js +59 -0
  13. package/src/plugins/bundled/fops-plugin-azure/index.js +4 -0
  14. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-core.js +53 -53
  15. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-secrets.js +151 -0
  16. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-storage.js +2 -2
  17. package/src/plugins/bundled/fops-plugin-azure/lib/azure-cost.js +52 -22
  18. package/src/plugins/bundled/fops-plugin-azure/lib/azure-fleet.js +12 -4
  19. package/src/plugins/bundled/fops-plugin-azure/lib/azure-helpers.js +6 -2
  20. package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops.js +113 -7
  21. package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision-init.js +13 -4
  22. package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision.js +91 -14
  23. package/src/plugins/bundled/fops-plugin-azure/lib/azure-service.js +507 -0
  24. package/src/plugins/bundled/fops-plugin-azure/lib/azure-sync.js +146 -7
  25. package/src/plugins/bundled/fops-plugin-azure/lib/azure.js +1 -1
  26. package/src/plugins/bundled/fops-plugin-azure/lib/commands/test-cmds.js +28 -0
  27. package/src/plugins/bundled/fops-plugin-azure/lib/commands/vm-cmds.js +61 -0
  28. package/src/plugins/bundled/fops-plugin-cloud/api.js +712 -0
  29. package/src/plugins/bundled/fops-plugin-cloud/fops.plugin.json +6 -0
  30. package/src/plugins/bundled/fops-plugin-cloud/index.js +208 -0
  31. package/src/plugins/bundled/fops-plugin-cloud/lib/azure-provider.js +81 -0
  32. package/src/plugins/bundled/fops-plugin-cloud/lib/provider.js +50 -0
  33. package/src/plugins/bundled/fops-plugin-cloud/ui/dist/assets/favicon-C49brna2.svg +15 -0
  34. package/src/plugins/bundled/fops-plugin-cloud/ui/dist/assets/index-CVqQ_kKW.js +65 -0
  35. package/src/plugins/bundled/fops-plugin-cloud/ui/dist/assets/index-DZetahP3.css +1 -0
  36. package/src/plugins/bundled/fops-plugin-cloud/ui/dist/index.html +28 -0
  37. package/src/plugins/bundled/fops-plugin-cloud/ui/index.html +27 -0
  38. package/src/plugins/bundled/fops-plugin-cloud/ui/package-lock.json +2634 -0
  39. package/src/plugins/bundled/fops-plugin-cloud/ui/package.json +29 -0
  40. package/src/plugins/bundled/fops-plugin-cloud/ui/postcss.config.cjs +5 -0
  41. package/src/plugins/bundled/fops-plugin-cloud/ui/src/App.jsx +32 -0
  42. package/src/plugins/bundled/fops-plugin-cloud/ui/src/api/client.js +114 -0
  43. package/src/plugins/bundled/fops-plugin-cloud/ui/src/api/queries.js +111 -0
  44. package/src/plugins/bundled/fops-plugin-cloud/ui/src/components/LogPanel.jsx +162 -0
  45. package/src/plugins/bundled/fops-plugin-cloud/ui/src/components/ThemeToggle.jsx +46 -0
  46. package/src/plugins/bundled/fops-plugin-cloud/ui/src/css/additional-styles/utility-patterns.css +147 -0
  47. package/src/plugins/bundled/fops-plugin-cloud/ui/src/css/style.css +138 -0
  48. package/src/plugins/bundled/fops-plugin-cloud/ui/src/favicon.svg +15 -0
  49. package/src/plugins/bundled/fops-plugin-cloud/ui/src/lib/utils.ts +19 -0
  50. package/src/plugins/bundled/fops-plugin-cloud/ui/src/main.jsx +25 -0
  51. package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/Audit.jsx +164 -0
  52. package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/Costs.jsx +305 -0
  53. package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/CreateResource.jsx +285 -0
  54. package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/Fleet.jsx +307 -0
  55. package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/Resources.jsx +229 -0
  56. package/src/plugins/bundled/fops-plugin-cloud/ui/src/partials/Header.jsx +132 -0
  57. package/src/plugins/bundled/fops-plugin-cloud/ui/src/partials/Sidebar.jsx +174 -0
  58. package/src/plugins/bundled/fops-plugin-cloud/ui/src/partials/SidebarLinkGroup.jsx +21 -0
  59. package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/AuthContext.jsx +170 -0
  60. package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/Info.jsx +49 -0
  61. package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/ThemeContext.jsx +37 -0
  62. package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/Transition.jsx +116 -0
  63. package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/Utils.js +63 -0
  64. package/src/plugins/bundled/fops-plugin-cloud/ui/vite.config.js +23 -0
  65. package/src/plugins/bundled/fops-plugin-foundation/test-helpers.js +65 -0
  66. package/src/plugins/loader.js +34 -1
  67. package/src/plugins/registry.js +15 -0
  68. package/src/plugins/schemas.js +17 -0
  69. package/src/project.js +1 -1
  70. package/src/serve.js +196 -2
  71. package/src/shell.js +21 -1
  72. package/src/web/admin.html.js +236 -0
  73. package/src/web/api.js +73 -0
  74. package/src/web/dist/assets/index-BphVaAUd.css +1 -0
  75. package/src/web/dist/assets/index-CSckLzuG.js +129 -0
  76. package/src/web/dist/index.html +2 -2
  77. package/src/web/frontend/index.html +16 -0
  78. package/src/web/frontend/src/App.jsx +445 -0
  79. package/src/web/frontend/src/components/ChatView.jsx +910 -0
  80. package/src/web/frontend/src/components/InputBox.jsx +523 -0
  81. package/src/web/frontend/src/components/Sidebar.jsx +410 -0
  82. package/src/web/frontend/src/components/StatusBar.jsx +37 -0
  83. package/src/web/frontend/src/components/TabBar.jsx +87 -0
  84. package/src/web/frontend/src/hooks/useWebSocket.js +412 -0
  85. package/src/web/frontend/src/index.css +78 -0
  86. package/src/web/frontend/src/main.jsx +6 -0
  87. package/src/web/frontend/vite.config.js +21 -0
  88. package/src/web/server.js +64 -1
  89. package/src/web/dist/assets/index-NXC8Hvnp.css +0 -1
  90. package/src/web/dist/assets/index-QH1N4ejK.js +0 -112
@@ -0,0 +1,410 @@
1
+ import React, { useState, useRef, useCallback } from "react";
2
+
3
+ function ServiceDot({ status }) {
4
+ const color = status === "running" ? "bg-emerald-400" : status === "exited" ? "bg-red-400/60" : "bg-[#3a3a50]";
5
+ return <span className={`w-[6px] h-[6px] rounded-full inline-block ${color}`} title={status} />;
6
+ }
7
+
8
+ function SectionHeader({ title, collapsed, onToggle, badge, badgeColor, count }) {
9
+ return (
10
+ <button
11
+ onClick={onToggle}
12
+ className="w-full flex items-center justify-between text-[10px] font-semibold text-[#4e4e63] uppercase tracking-widest mb-2 hover:text-[#8b8b9e] transition-colors"
13
+ >
14
+ <span className="flex items-center gap-1.5">
15
+ <span className={`text-[8px] transition-transform duration-200 ${collapsed ? "" : "rotate-90"}`}>&#x25B6;</span>
16
+ {title}
17
+ </span>
18
+ {badge != null ? (
19
+ <span className={`text-[9px] font-mono font-normal normal-case tracking-normal ${badgeColor || "text-[#3a3a50]"}`}>
20
+ {badge}
21
+ </span>
22
+ ) : (collapsed && count > 0 ? (
23
+ <span className="text-[9px] font-mono font-normal normal-case tracking-normal text-[#3a3a50]">
24
+ {count}
25
+ </span>
26
+ ) : null)}
27
+ </button>
28
+ );
29
+ }
30
+
31
+ function ServicesSection({ services }) {
32
+ const [collapsed, setCollapsed] = useState(true);
33
+ const runningCount = services.filter((s) => s.status === "running").length;
34
+ const badgeColor = services.length === 0 ? "text-[#3a3a50]"
35
+ : runningCount === services.length ? "text-emerald-400"
36
+ : runningCount === 0 ? "text-red-400"
37
+ : "text-yellow-400";
38
+
39
+ return (
40
+ <div className="px-3 py-2.5">
41
+ <SectionHeader
42
+ title="Services"
43
+ collapsed={collapsed}
44
+ onToggle={() => setCollapsed((c) => !c)}
45
+ badge={services.length > 0 ? `${runningCount}/${services.length}` : null}
46
+ badgeColor={badgeColor}
47
+ count={runningCount}
48
+ />
49
+ {!collapsed && (
50
+ <div className="space-y-0.5 animate-fade-in">
51
+ {services.length > 0 ? (
52
+ services.map((svc) => (
53
+ <div key={svc.name} className="flex items-center gap-2 px-1 py-[3px] rounded text-[11px] font-mono">
54
+ <ServiceDot status={svc.status} />
55
+ <span className={svc.status === "running" ? "text-[#8b8b9e]" : "text-[#3a3a50]"}>
56
+ {svc.name}
57
+ </span>
58
+ </div>
59
+ ))
60
+ ) : (
61
+ <div className="px-1 py-0.5 text-[11px] text-[#2e2e40] font-mono italic">no services</div>
62
+ )}
63
+ </div>
64
+ )}
65
+ </div>
66
+ );
67
+ }
68
+
69
+ function TreeGroup({ label, icon, color, count }) {
70
+ return (
71
+ <div className="flex items-center gap-1.5 px-2 py-[2px] text-[11px] font-mono">
72
+ <span className={color}>{icon}</span>
73
+ <span className="text-[#8b8b9e]">{label}</span>
74
+ {count > 0 && <span className="text-[#3a3a50] text-[10px]">({count})</span>}
75
+ </div>
76
+ );
77
+ }
78
+
79
+ function TreeNode({ label, color, isLast, isExpanded, onClick }) {
80
+ const connector = isLast && !isExpanded ? "\u2514 " : "\u251C ";
81
+ const chevron = isExpanded ? "\u25BE " : "\u25B8 ";
82
+ return (
83
+ <button
84
+ onClick={onClick}
85
+ className="w-full flex items-center gap-1 pl-5 py-[1px] text-[11px] font-mono text-left hover:bg-[#12121a] rounded transition-colors"
86
+ >
87
+ <span className="text-[#2e2e40]">{connector}</span>
88
+ <span className={`${isExpanded ? color.replace("/70", "") : color} ${isExpanded ? "font-semibold" : ""}`}>
89
+ {isExpanded ? chevron : ""}{label}
90
+ </span>
91
+ </button>
92
+ );
93
+ }
94
+
95
+ function EntityDetail({ detail: rawDetail, entityType, borderColor }) {
96
+ if (!rawDetail) {
97
+ return (
98
+ <div className="ml-8 mr-2 my-0.5 pl-2 border-l border-[#2e2e40] text-[10px] font-mono text-[#3a3a50] italic">
99
+ loading...
100
+ </div>
101
+ );
102
+ }
103
+ const d = rawDetail.entity || rawDetail;
104
+ const info = rawDetail.entity_info || {};
105
+ const trunc = (s, n) => s && s.length > n ? s.slice(0, n) + "\u2026" : s;
106
+ const lines = [];
107
+
108
+ const str = (v) => typeof v === "object" && v !== null ? (v.code || v.reason || JSON.stringify(v)) : String(v ?? "");
109
+
110
+ if (entityType === "mesh") {
111
+ if (d.state) lines.push({ label: "state", value: str(d.state) });
112
+ if (d.description) lines.push({ label: "desc", value: trunc(d.description, 40) });
113
+ if (d.member_count != null) lines.push({ label: "members", value: String(d.member_count) });
114
+ if (d.purpose) lines.push({ label: "purpose", value: trunc(d.purpose, 40) });
115
+ if (info.owner) lines.push({ label: "owner", value: trunc(info.owner, 25) });
116
+ } else if (entityType === "data_product") {
117
+ if (d.data_product_type) lines.push({ label: "type", value: d.data_product_type });
118
+ if (d.description) lines.push({ label: "desc", value: trunc(d.description, 40) });
119
+ if (info.owner) lines.push({ label: "owner", value: trunc(info.owner, 25) });
120
+ } else if (entityType === "data_source") {
121
+ if (d.source_type) lines.push({ label: "type", value: d.source_type });
122
+ if (d.description) lines.push({ label: "desc", value: trunc(d.description, 40) });
123
+ if (info.owner) lines.push({ label: "owner", value: trunc(info.owner, 25) });
124
+ }
125
+
126
+ if (lines.length === 0) {
127
+ const id = d.identifier || rawDetail.identifier;
128
+ lines.push({ label: "id", value: trunc(id || "\u2014", 20) });
129
+ }
130
+
131
+ return (
132
+ <div className={`ml-8 mr-2 my-0.5 pl-2 bg-[#12121a] rounded-sm border-l ${borderColor || "border-[#2e2e40]"}`}>
133
+ {lines.map((l, i) => (
134
+ <div key={i} className="text-[10px] font-mono py-[1px]">
135
+ <span className="text-[#3a3a50]">{l.label}: </span>
136
+ <span className="text-[#6e7a8a]">{l.value}</span>
137
+ </div>
138
+ ))}
139
+ </div>
140
+ );
141
+ }
142
+
143
+ function DataSection({ foundationStats, onFetchEntity }) {
144
+ const [collapsed, setCollapsed] = useState(false);
145
+ const [expandedKey, setExpandedKey] = useState(null);
146
+ const [entityDetails, setEntityDetails] = useState({});
147
+ const detailsCacheRef = useRef({});
148
+
149
+ const meshItems = foundationStats?.meshes?.items || [];
150
+ const dpItems = foundationStats?.dataProducts?.items || [];
151
+ const dsItems = foundationStats?.dataSources?.items || [];
152
+ const activeRuns = foundationStats?.activeRuns?.items || [];
153
+ const totalCount = meshItems.length + dpItems.length + dsItems.length + activeRuns.length;
154
+
155
+ const handleNodeClick = useCallback(async (type, identifier) => {
156
+ const key = `${type}:${identifier}`;
157
+ if (expandedKey === key) {
158
+ setExpandedKey(null);
159
+ return;
160
+ }
161
+ setExpandedKey(key);
162
+ if (detailsCacheRef.current[key]) {
163
+ setEntityDetails((prev) => ({ ...prev, [key]: detailsCacheRef.current[key] }));
164
+ return;
165
+ }
166
+ if (onFetchEntity) {
167
+ try {
168
+ const detail = await onFetchEntity(type, identifier);
169
+ detailsCacheRef.current[key] = detail;
170
+ setEntityDetails((prev) => ({ ...prev, [key]: detail }));
171
+ } catch {
172
+ const fallback = { identifier };
173
+ detailsCacheRef.current[key] = fallback;
174
+ setEntityDetails((prev) => ({ ...prev, [key]: fallback }));
175
+ }
176
+ }
177
+ }, [expandedKey, onFetchEntity]);
178
+
179
+ if (totalCount === 0) return null;
180
+
181
+ const renderGroup = (items, label, icon, colorClass, nodeColorClass, type, borderColor) => {
182
+ if (items.length === 0) return null;
183
+ return (
184
+ <React.Fragment key={label}>
185
+ <TreeGroup label={label} icon={icon} color={colorClass} count={items.length} />
186
+ {items.map((item, i) => {
187
+ const nk = `${type}:${item.identifier}`;
188
+ const isExpanded = expandedKey === nk;
189
+ return (
190
+ <React.Fragment key={item.identifier}>
191
+ <TreeNode
192
+ label={item.name}
193
+ color={nodeColorClass}
194
+ isLast={i === items.length - 1}
195
+ isExpanded={isExpanded}
196
+ onClick={() => handleNodeClick(type, item.identifier)}
197
+ />
198
+ {isExpanded && (
199
+ <EntityDetail detail={entityDetails[nk] || null} borderColor={borderColor} />
200
+ )}
201
+ </React.Fragment>
202
+ );
203
+ })}
204
+ </React.Fragment>
205
+ );
206
+ };
207
+
208
+ return (
209
+ <div className="px-3 py-2.5">
210
+ <SectionHeader
211
+ title="Data"
212
+ collapsed={collapsed}
213
+ onToggle={() => setCollapsed((c) => !c)}
214
+ count={totalCount}
215
+ />
216
+ {!collapsed && (
217
+ <div className="animate-fade-in">
218
+ {renderGroup(meshItems, "Meshes", "\u25C8", "text-cyan-400", "text-cyan-300/70", "mesh", "border-cyan-400/30")}
219
+ {renderGroup(dpItems, "Products", "\u25C6", "text-emerald-400", "text-emerald-300/70", "data_product", "border-emerald-400/30")}
220
+ {renderGroup(dsItems, "Sources", "\u25CB", "text-yellow-400", "text-yellow-300/70", "data_source", "border-yellow-400/30")}
221
+ {activeRuns.length > 0 && (
222
+ <React.Fragment key="active-runs">
223
+ <TreeGroup label="Active runs" icon="\u25B6" color="text-fuchsia-400" count={activeRuns.length} />
224
+ {activeRuns.map((run, i) => {
225
+ const label = run.application_name || (run.name && run.suffix ? `${run.name}-${run.suffix}` : run.name || "job");
226
+ const status = run.status ? ` · ${run.status}` : "";
227
+ return (
228
+ <div
229
+ key={run.id ?? i}
230
+ className="pl-4 py-0.5 text-[11px] font-mono text-fuchsia-300/80 truncate"
231
+ title={`${label}${status}`}
232
+ >
233
+ {label}{status}
234
+ </div>
235
+ );
236
+ })}
237
+ </React.Fragment>
238
+ )}
239
+ </div>
240
+ )}
241
+ </div>
242
+ );
243
+ }
244
+
245
+ function AgentsSection({ agents, onCreateSession }) {
246
+ const [collapsed, setCollapsed] = useState(true);
247
+
248
+ return (
249
+ <div className="px-3 py-2.5">
250
+ <SectionHeader
251
+ title="Agents"
252
+ collapsed={collapsed}
253
+ onToggle={() => setCollapsed((c) => !c)}
254
+ count={agents.length}
255
+ />
256
+ {!collapsed && (
257
+ <div className="space-y-0.5 animate-fade-in">
258
+ {agents.length > 0 ? (
259
+ agents.map((a) => (
260
+ <button
261
+ key={a.name}
262
+ onClick={() => onCreateSession(a.name)}
263
+ className="w-full text-left px-2 py-1.5 rounded-lg text-[11px] font-mono text-[#6e7a8a] hover:bg-[#18181f] hover:text-[#f97316] transition-all duration-150"
264
+ >
265
+ {a.name}
266
+ </button>
267
+ ))
268
+ ) : (
269
+ <div className="px-1 py-0.5 text-[11px] text-[#2e2e40] font-mono italic">none</div>
270
+ )}
271
+ </div>
272
+ )}
273
+ </div>
274
+ );
275
+ }
276
+
277
+ export default function Sidebar({
278
+ sessions,
279
+ activeId,
280
+ agents,
281
+ services,
282
+ foundationStats,
283
+ collapsed,
284
+ onToggle,
285
+ onSwitchSession,
286
+ onCreateSession,
287
+ onFetchEntity,
288
+ onRunCommand,
289
+ }) {
290
+ if (collapsed) {
291
+ return (
292
+ <div className="w-12 bg-[#08080c] border-r border-[#1a1a28] flex flex-col items-center py-3 gap-4">
293
+ <button
294
+ onClick={onToggle}
295
+ className="text-[#4e4e63] hover:text-[#f97316] text-sm transition-colors"
296
+ title="Expand sidebar (Ctrl+B)"
297
+ >
298
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
299
+ <line x1="2" y1="3" x2="14" y2="3" />
300
+ <line x1="2" y1="8" x2="14" y2="8" />
301
+ <line x1="2" y1="13" x2="14" y2="13" />
302
+ </svg>
303
+ </button>
304
+ {/* Mini service indicators */}
305
+ {services.length > 0 && (
306
+ <div className="flex flex-col items-center gap-1.5 mt-auto mb-2">
307
+ {services.filter((s) => s.status === "running").length > 0 && (
308
+ <span className="w-[5px] h-[5px] rounded-full bg-emerald-400" title="Services running" />
309
+ )}
310
+ {services.filter((s) => s.status !== "running").length > 0 && (
311
+ <span className="w-[5px] h-[5px] rounded-full bg-red-400/50" title="Services stopped" />
312
+ )}
313
+ </div>
314
+ )}
315
+ </div>
316
+ );
317
+ }
318
+
319
+ return (
320
+ <div className="w-60 bg-[#08080c] border-r border-[#1a1a28] flex flex-col overflow-y-auto shrink-0">
321
+ {/* Header */}
322
+ <div className="flex items-center justify-between px-4 py-3 border-b border-[#1a1a28]">
323
+ <div className="flex items-center gap-2">
324
+ <span className="text-[#f97316] text-sm">&#x25C6;</span>
325
+ <span className="text-xs font-bold text-[#6e7a8a] uppercase tracking-[0.2em]">meshx</span>
326
+ </div>
327
+ <button
328
+ onClick={onToggle}
329
+ className="text-[#3a3a50] hover:text-[#8b8b9e] text-xs transition-colors"
330
+ title="Collapse sidebar (Ctrl+B)"
331
+ >
332
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
333
+ <polyline points="9,3 5,7 9,11" />
334
+ </svg>
335
+ </button>
336
+ </div>
337
+
338
+ {/* Sessions */}
339
+ <div className="px-3 py-2.5">
340
+ <div className="text-[10px] font-semibold text-[#4e4e63] uppercase tracking-widest mb-2 px-1">
341
+ Sessions
342
+ </div>
343
+ <div className="space-y-0.5">
344
+ {sessions.map((s, i) => {
345
+ const isActive = s.id === activeId;
346
+ return (
347
+ <button
348
+ key={s.id}
349
+ onClick={() => onSwitchSession(s.id)}
350
+ className={`
351
+ w-full text-left flex items-center gap-2 px-2 py-1.5 rounded-lg text-[11px] font-mono
352
+ transition-all duration-150
353
+ ${isActive
354
+ ? "bg-[#f97316]/8 text-[#f97316] border-l-2 border-[#f97316] pl-[6px]"
355
+ : "text-[#6e7a8a] hover:bg-[#14141c] hover:text-[#8b8b9e] border-l-2 border-transparent pl-[6px]"
356
+ }
357
+ `}
358
+ >
359
+ <span className="truncate">{i + 1}:{s.agent || "general"}</span>
360
+ {s.status === "streaming" && (
361
+ <span className="w-1.5 h-1.5 rounded-full bg-[#f97316] animate-pulse ml-auto shrink-0" />
362
+ )}
363
+ </button>
364
+ );
365
+ })}
366
+ </div>
367
+ </div>
368
+
369
+ {/* Data tree (meshes, products, sources) */}
370
+ <DataSection foundationStats={foundationStats} onFetchEntity={onFetchEntity} />
371
+
372
+ {/* Agents */}
373
+ <AgentsSection agents={agents} onCreateSession={onCreateSession} />
374
+
375
+ {/* Quick commands */}
376
+ <div className="px-3 py-2.5">
377
+ <div className="text-[10px] font-semibold text-[#4e4e63] uppercase tracking-widest mb-2 px-1">
378
+ Commands
379
+ </div>
380
+ <div className="space-y-0.5">
381
+ <button
382
+ onClick={() => onRunCommand?.("/plan draft implementation steps for this task")}
383
+ className="w-full text-left px-2 py-1.5 rounded-lg text-[11px] font-mono text-[#6e7a8a] hover:bg-[#18181f] hover:text-[#f97316] transition-all duration-150"
384
+ >
385
+ /plan
386
+ </button>
387
+ <button
388
+ onClick={() => onRunCommand?.("/agents")}
389
+ className="w-full text-left px-2 py-1.5 rounded-lg text-[11px] font-mono text-[#6e7a8a] hover:bg-[#18181f] hover:text-[#f97316] transition-all duration-150"
390
+ >
391
+ /agents
392
+ </button>
393
+ </div>
394
+ </div>
395
+
396
+ {/* Spacer */}
397
+ <div className="flex-1" />
398
+
399
+ {/* Services */}
400
+ <div className="border-t border-[#1a1a28]">
401
+ <ServicesSection services={services} />
402
+ </div>
403
+
404
+ {/* Footer */}
405
+ <div className="px-4 py-2 border-t border-[#1a1a28] text-[9px] font-mono text-[#2e2e40] tracking-wide">
406
+ ^B toggle
407
+ </div>
408
+ </div>
409
+ );
410
+ }
@@ -0,0 +1,37 @@
1
+ import React, { useState } from "react";
2
+
3
+ export default function StatusBar({ connected, agentName, statusText, isStreaming, updateAvailable }) {
4
+ const [dismissed, setDismissed] = useState(false);
5
+
6
+ return (
7
+ <div className="bg-[#06060a] px-4 py-1.5 flex justify-between items-center text-[11px] font-mono border-t border-[#1a1a28]">
8
+ <div className="flex items-center gap-4">
9
+ <span className="flex items-center gap-1.5">
10
+ <span className={`w-1.5 h-1.5 rounded-full ${connected ? "bg-emerald-400" : "bg-red-400"}`} />
11
+ <span className={connected ? "text-[#6e7a8a]" : "text-red-400"}>{connected ? "connected" : "disconnected"}</span>
12
+ </span>
13
+ <span className="text-[#4e4e63]">
14
+ agent:<span className="text-[#8b8b9e]">{agentName || "general"}</span>
15
+ </span>
16
+ {isStreaming && statusText && (
17
+ <span className="text-[#f97316] animate-pulse">{statusText}</span>
18
+ )}
19
+ </div>
20
+ <div className="flex items-center gap-4 text-[#3a3a50]">
21
+ {updateAvailable && !dismissed && (
22
+ <span className="text-[#fbbf24] flex items-center gap-1.5">
23
+ &#x2B06; v{updateAvailable}
24
+ <button
25
+ onClick={() => setDismissed(true)}
26
+ className="text-[#4e4e63] hover:text-[#8b8b9e] ml-0.5 transition-colors"
27
+ >
28
+ &#x2715;
29
+ </button>
30
+ </span>
31
+ )}
32
+ <span>Shift+&#x23CE; newline</span>
33
+ <span>^B sidebar</span>
34
+ </div>
35
+ </div>
36
+ );
37
+ }
@@ -0,0 +1,87 @@
1
+ import React, { useState, useRef, useEffect } from "react";
2
+
3
+ export default function TabBar({ sessions, activeId, agents, onSwitch, onAdd, onClose }) {
4
+ const [showAgentPicker, setShowAgentPicker] = useState(false);
5
+ const pickerRef = useRef(null);
6
+
7
+ // Close picker on outside click
8
+ useEffect(() => {
9
+ if (!showAgentPicker) return;
10
+ const handler = (e) => {
11
+ if (pickerRef.current && !pickerRef.current.contains(e.target)) setShowAgentPicker(false);
12
+ };
13
+ document.addEventListener("mousedown", handler);
14
+ return () => document.removeEventListener("mousedown", handler);
15
+ }, [showAgentPicker]);
16
+
17
+ return (
18
+ <div className="flex items-center bg-[#0c0c12] px-1 py-0 relative">
19
+ {sessions.map((s, i) => {
20
+ const isActive = s.id === activeId;
21
+ return (
22
+ <div
23
+ key={s.id}
24
+ className={`
25
+ relative flex items-center gap-1.5 px-4 py-2 text-xs cursor-pointer
26
+ transition-colors duration-150 group
27
+ ${isActive ? "text-[#e0dfe4]" : "text-[#5a5a70] hover:text-[#8b8b9e]"}
28
+ `}
29
+ onClick={() => onSwitch(s.id)}
30
+ >
31
+ <span className="font-mono font-medium">{i + 1}:{s.agent || "general"}</span>
32
+ {s.status === "streaming" && (
33
+ <span className="w-1.5 h-1.5 rounded-full bg-[#f97316] animate-pulse" />
34
+ )}
35
+ {sessions.length > 1 && (
36
+ <button
37
+ className="ml-0.5 text-[#3a3a50] hover:text-[#f87171] text-[10px] opacity-0 group-hover:opacity-100 transition-opacity"
38
+ onClick={(e) => { e.stopPropagation(); onClose(s.id); }}
39
+ >
40
+ &#x2715;
41
+ </button>
42
+ )}
43
+ {/* Active indicator — bottom bar */}
44
+ {isActive && (
45
+ <div className="absolute bottom-0 left-2 right-2 h-[2px] bg-[#f97316] rounded-full" />
46
+ )}
47
+ </div>
48
+ );
49
+ })}
50
+
51
+ {/* Add session button */}
52
+ <div className="relative" ref={pickerRef}>
53
+ <button
54
+ className="text-[#4e4e63] hover:text-[#f97316] px-3 py-2 text-xs transition-colors"
55
+ onClick={() => setShowAgentPicker(!showAgentPicker)}
56
+ >
57
+ +
58
+ </button>
59
+
60
+ {showAgentPicker && (
61
+ <div className="absolute top-full left-0 mt-1 bg-[#14141c] border border-[#26263a] rounded-xl shadow-2xl z-50 min-w-[220px] py-1.5 animate-fade-in overflow-hidden">
62
+ <div
63
+ className="px-4 py-2.5 hover:bg-[#1c1c28] cursor-pointer text-sm text-[#8b8b9e] hover:text-[#e0dfe4] transition-colors"
64
+ onClick={() => { onAdd(null); setShowAgentPicker(false); }}
65
+ >
66
+ <span className="text-[#4e4e63] mr-2 font-mono text-xs">&#x276F;</span>
67
+ general
68
+ </div>
69
+ {agents.map((a) => (
70
+ <div
71
+ key={a.name}
72
+ className="px-4 py-2.5 hover:bg-[#1c1c28] cursor-pointer text-sm transition-colors"
73
+ onClick={() => { onAdd(a.name); setShowAgentPicker(false); }}
74
+ >
75
+ <span className="text-[#f97316] mr-2 font-mono text-xs">&#x276F;</span>
76
+ <span className="text-[#e0dfe4]">{a.name}</span>
77
+ {a.description && <span className="text-[#4e4e63] ml-2 text-xs">{a.description}</span>}
78
+ </div>
79
+ ))}
80
+ </div>
81
+ )}
82
+ </div>
83
+
84
+ <div className="ml-auto text-[#2e2e40] text-[10px] font-mono pr-3 tracking-wider uppercase">fops</div>
85
+ </div>
86
+ );
87
+ }