@meshxdata/fops 0.1.47 → 0.1.49
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/CHANGELOG.md +368 -0
- package/package.json +6 -1
- package/src/commands/lifecycle.js +30 -11
- package/src/plugins/bundled/fops-plugin-azure/lib/commands/test-cmds.js +19 -27
- package/src/plugins/bundled/fops-plugin-azure/lib/pytest-parse.js +21 -6
- package/src/plugins/__test-fixtures__/fake-plugin.js +0 -2
- package/src/plugins/__test-fixtures__/no-register-plugin.js +0 -2
- package/src/plugins/__test-fixtures__/with-register/index.js +0 -2
- package/src/plugins/__test-fixtures__/without-register/index.js +0 -2
- package/src/plugins/bundled/fops-plugin-foundation/test-helpers.js +0 -65
- package/src/web/frontend/index.html +0 -16
- package/src/web/frontend/src/App.jsx +0 -445
- package/src/web/frontend/src/components/ChatView.jsx +0 -910
- package/src/web/frontend/src/components/InputBox.jsx +0 -523
- package/src/web/frontend/src/components/Sidebar.jsx +0 -410
- package/src/web/frontend/src/components/StatusBar.jsx +0 -37
- package/src/web/frontend/src/components/TabBar.jsx +0 -87
- package/src/web/frontend/src/hooks/useWebSocket.js +0 -412
- package/src/web/frontend/src/index.css +0 -78
- package/src/web/frontend/src/main.jsx +0 -6
- package/src/web/frontend/vite.config.js +0 -21
|
@@ -1,410 +0,0 @@
|
|
|
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"}`}>▶</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">◆</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
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
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
|
-
⬆ v{updateAvailable}
|
|
24
|
-
<button
|
|
25
|
-
onClick={() => setDismissed(true)}
|
|
26
|
-
className="text-[#4e4e63] hover:text-[#8b8b9e] ml-0.5 transition-colors"
|
|
27
|
-
>
|
|
28
|
-
✕
|
|
29
|
-
</button>
|
|
30
|
-
</span>
|
|
31
|
-
)}
|
|
32
|
-
<span>Shift+⏎ newline</span>
|
|
33
|
-
<span>^B sidebar</span>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
);
|
|
37
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
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
|
-
✕
|
|
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">❯</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">❯</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
|
-
}
|