agent-hub-sdd 0.1.8 → 0.1.9
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/README.md +7 -5
- package/package.json +1 -1
- package/scripts/kill-hub.ps1 +1 -1
- package/src/app/gastos/page.tsx +5 -1
- package/src/app/shell.css +350 -6
- package/src/components/agent-dialog.tsx +1 -1
- package/src/components/chat-pane.tsx +102 -60
- package/src/components/composer.tsx +14 -5
- package/src/components/desktop-chrome.tsx +8 -1
- package/src/components/hub-shell.tsx +14 -16
- package/src/components/sidebar.tsx +47 -43
- package/src/components/spend-view.tsx +18 -15
- package/src/lib/labels.test.ts +21 -0
- package/src/lib/labels.ts +29 -0
package/README.md
CHANGED
|
@@ -15,10 +15,10 @@ npm run app
|
|
|
15
15
|
Pelo npm (precisa do Node 20+ no PATH):
|
|
16
16
|
|
|
17
17
|
```powershell
|
|
18
|
-
npx --yes agent-hub-sdd@0.1.
|
|
18
|
+
npx --yes agent-hub-sdd@0.1.9
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
Na primeira abertura a janela diz que está compilando. O **N** no canto é o Next.js em modo dev, não o app. Sem `@0.1.
|
|
21
|
+
Na primeira abertura a janela diz que está compilando. O **N** no canto é o Next.js em modo dev, não o app. Sem `@0.1.9`, o Windows pode reusar cache antigo.
|
|
22
22
|
|
|
23
23
|
Se ainda abrir a 0.1.2, ou se o npm disser `EBUSY` / `resource busy or locked`, o Next da tentativa antiga ainda está com o cache aberto. O `Stop-Process electron` não mata esse `node`:
|
|
24
24
|
|
|
@@ -27,13 +27,13 @@ Get-CimInstance Win32_Process | Where-Object {
|
|
|
27
27
|
$_.CommandLine -and ($_.CommandLine -match 'agent-hub|_npx|next dev|43147')
|
|
28
28
|
} | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
|
|
29
29
|
Remove-Item -Recurse -Force "$env:LOCALAPPDATA\npm-cache\_npx" -ErrorAction SilentlyContinue
|
|
30
|
-
npx --yes agent-hub-sdd@0.1.
|
|
30
|
+
npx --yes agent-hub-sdd@0.1.9
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
Instalar global evita o rename do `npx` nessa pasta:
|
|
34
34
|
|
|
35
35
|
```powershell
|
|
36
|
-
npm install -g agent-hub-sdd@0.1.
|
|
36
|
+
npm install -g agent-hub-sdd@0.1.9
|
|
37
37
|
agent-hub-sdd
|
|
38
38
|
```
|
|
39
39
|
|
|
@@ -76,7 +76,9 @@ O Hub não carrega skills, MCP, plugins, hooks nem regras. Quem carrega é a CLI
|
|
|
76
76
|
|
|
77
77
|
## Simulador
|
|
78
78
|
|
|
79
|
-
Se a CLI pedida não estiver no `PATH`, o Hub
|
|
79
|
+
Se a CLI pedida (`agent` / `cursor-agent` ou `claude`) não estiver no `PATH`, o Hub **cai no Simulador**: respostas fictícias com prefixo `[mock]`, custo zero, disco intocado. A faixa âmbar e o selo **Simulador · Cursor** (ou Claude) deixam isso explícito — o hub não finge que a CLI está viva.
|
|
80
|
+
|
|
81
|
+
Com a CLI da assinatura no PATH e login feito (`agent login` ou `claude auth login`), os turnos passam a ser reais. O Hub nunca envia chave de API.
|
|
80
82
|
|
|
81
83
|
## Dados
|
|
82
84
|
|
package/package.json
CHANGED
package/scripts/kill-hub.ps1
CHANGED
|
@@ -8,4 +8,4 @@ Get-CimInstance Win32_Process |
|
|
|
8
8
|
Stop-Process -Name electron -Force
|
|
9
9
|
$npx = Join-Path $env:LOCALAPPDATA "npm-cache\_npx"
|
|
10
10
|
if (Test-Path $npx) { Remove-Item -Recurse -Force $npx }
|
|
11
|
-
Write-Host "Cache do npx limpo. Rode: npx --yes agent-hub-sdd@0.1.
|
|
11
|
+
Write-Host "Cache do npx limpo. Rode: npx --yes agent-hub-sdd@0.1.9"
|
package/src/app/gastos/page.tsx
CHANGED
|
@@ -7,5 +7,9 @@ export const dynamic = "force-dynamic";
|
|
|
7
7
|
|
|
8
8
|
export default function GastosPage() {
|
|
9
9
|
const events = listUsage();
|
|
10
|
-
return
|
|
10
|
+
return (
|
|
11
|
+
<div className="min-h-0 flex-1 overflow-auto">
|
|
12
|
+
<SpendView events={events} agents={listAgents()} summary={aggregateSpend(events)} />
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
11
15
|
}
|
package/src/app/shell.css
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
html, body {
|
|
2
2
|
margin: 0;
|
|
3
3
|
min-height: 100%;
|
|
4
|
-
background: #
|
|
4
|
+
background: #09090b;
|
|
5
5
|
color: #f4f4f5;
|
|
6
6
|
font-family: "Segoe UI", ui-sans-serif, system-ui, sans-serif;
|
|
7
7
|
}
|
|
@@ -10,21 +10,23 @@ button, input, textarea { font: inherit; color: inherit; }
|
|
|
10
10
|
button {
|
|
11
11
|
cursor: pointer;
|
|
12
12
|
background: #18181b;
|
|
13
|
-
border: 1px solid #
|
|
13
|
+
border: 1px solid #3f3f46;
|
|
14
14
|
border-radius: 0.5rem;
|
|
15
|
-
padding: 0.
|
|
15
|
+
padding: 0.35rem 0.7rem;
|
|
16
16
|
}
|
|
17
17
|
input, textarea {
|
|
18
18
|
background: #18181b;
|
|
19
|
-
border: 1px solid #
|
|
19
|
+
border: 1px solid #3f3f46;
|
|
20
20
|
border-radius: 0.5rem;
|
|
21
|
-
padding: 0.
|
|
21
|
+
padding: 0.4rem 0.6rem;
|
|
22
22
|
}
|
|
23
23
|
.flex { display: flex; }
|
|
24
24
|
.flex-col { flex-direction: column; }
|
|
25
25
|
.flex-1 { flex: 1 1 0%; }
|
|
26
|
+
.flex-wrap { flex-wrap: wrap; }
|
|
26
27
|
.items-center { align-items: center; }
|
|
27
28
|
.justify-center { justify-content: center; }
|
|
29
|
+
.justify-between { justify-content: space-between; }
|
|
28
30
|
.min-h-0 { min-height: 0; }
|
|
29
31
|
.min-h-dvh { min-height: 100dvh; }
|
|
30
32
|
.min-w-0 { min-width: 0; }
|
|
@@ -33,22 +35,32 @@ input, textarea {
|
|
|
33
35
|
.h-9 { height: 2.25rem; }
|
|
34
36
|
.w-full { width: 100%; }
|
|
35
37
|
.w-\[280px\] { width: 280px; }
|
|
38
|
+
.w-\[300px\] { width: 300px; }
|
|
36
39
|
.max-w-md { max-width: 28rem; }
|
|
37
40
|
.max-w-\[280px\] { max-width: 280px; }
|
|
41
|
+
.max-w-\[300px\] { max-width: 300px; }
|
|
38
42
|
.shrink-0 { flex-shrink: 0; }
|
|
39
43
|
.gap-1 { gap: 0.25rem; }
|
|
44
|
+
.gap-1\.5 { gap: 0.375rem; }
|
|
40
45
|
.gap-2 { gap: 0.5rem; }
|
|
41
46
|
.gap-3 { gap: 0.75rem; }
|
|
42
47
|
.overflow-hidden { overflow: hidden; }
|
|
43
48
|
.overflow-auto { overflow: auto; }
|
|
44
49
|
.grid { display: grid; }
|
|
45
50
|
.hidden { display: none; }
|
|
51
|
+
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
46
52
|
.border { border: 1px solid #27272a; }
|
|
47
53
|
.border-r { border-right: 1px solid #27272a; }
|
|
48
54
|
.border-b { border-bottom: 1px solid #27272a; }
|
|
55
|
+
.border-t { border-top: 1px solid #27272a; }
|
|
49
56
|
.border-zinc-800 { border-color: #27272a; }
|
|
50
|
-
.bg-black { background: #
|
|
57
|
+
.bg-black { background: #09090b; }
|
|
58
|
+
.bg-zinc-950 { background: #09090b; }
|
|
59
|
+
.bg-zinc-900 { background: #18181b; }
|
|
60
|
+
.bg-zinc-800 { background: #27272a; }
|
|
61
|
+
.text-zinc-50 { color: #fafafa; }
|
|
51
62
|
.text-zinc-100 { color: #f4f4f5; }
|
|
63
|
+
.text-zinc-200 { color: #e4e4e7; }
|
|
52
64
|
.text-zinc-300 { color: #d4d4d8; }
|
|
53
65
|
.text-zinc-400 { color: #a1a1aa; }
|
|
54
66
|
.text-zinc-500 { color: #71717a; }
|
|
@@ -57,17 +69,349 @@ input, textarea {
|
|
|
57
69
|
.text-lg { font-size: 1.125rem; }
|
|
58
70
|
.font-medium { font-weight: 500; }
|
|
59
71
|
.font-semibold { font-weight: 600; }
|
|
72
|
+
.p-2 { padding: 0.5rem; }
|
|
60
73
|
.p-3 { padding: 0.75rem; }
|
|
61
74
|
.p-4 { padding: 1rem; }
|
|
75
|
+
.p-6 { padding: 1.5rem; }
|
|
62
76
|
.px-2 { padding-left: 0.5rem; padding-right: 0.5rem; }
|
|
77
|
+
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
|
|
78
|
+
.px-4 { padding-left: 1rem; padding-right: 1rem; }
|
|
79
|
+
.py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; }
|
|
80
|
+
.py-1\.5 { padding-top: 0.375rem; padding-bottom: 0.375rem; }
|
|
63
81
|
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
|
|
82
|
+
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
|
|
64
83
|
.pr-8 { padding-right: 2rem; }
|
|
84
|
+
.ml-auto { margin-left: auto; }
|
|
85
|
+
.mb-1 { margin-bottom: 0.25rem; }
|
|
86
|
+
.size-3 { width: 0.75rem; height: 0.75rem; }
|
|
87
|
+
.size-3\.5 { width: 0.875rem; height: 0.875rem; }
|
|
88
|
+
.rounded-md { border-radius: 0.375rem; }
|
|
65
89
|
.rounded-lg { border-radius: 0.5rem; }
|
|
90
|
+
.rounded-2xl { border-radius: 1rem; }
|
|
66
91
|
.rounded-full { border-radius: 9999px; }
|
|
67
92
|
.leading-6 { line-height: 1.5rem; }
|
|
68
93
|
.text-center { text-align: center; }
|
|
94
|
+
.text-left { text-align: left; }
|
|
95
|
+
.size-2 { width: 0.5rem; height: 0.5rem; }
|
|
69
96
|
.ah-drag { -webkit-app-region: drag; app-region: drag; }
|
|
70
97
|
.ah-no-drag { -webkit-app-region: no-drag; app-region: no-drag; }
|
|
98
|
+
|
|
99
|
+
.hub-sidebar {
|
|
100
|
+
background: #0a0a0a;
|
|
101
|
+
border-right: 1px solid #27272a;
|
|
102
|
+
}
|
|
103
|
+
.hub-brand {
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
gap: 0.5rem;
|
|
107
|
+
font-size: 0.95rem;
|
|
108
|
+
font-weight: 650;
|
|
109
|
+
letter-spacing: -0.03em;
|
|
110
|
+
}
|
|
111
|
+
.hub-mark {
|
|
112
|
+
width: 0.7rem;
|
|
113
|
+
height: 0.7rem;
|
|
114
|
+
border-radius: 0.2rem;
|
|
115
|
+
background: #f4f4f5;
|
|
116
|
+
box-shadow: 3px 3px 0 0 #52525b;
|
|
117
|
+
}
|
|
118
|
+
.hub-status {
|
|
119
|
+
border-radius: 0.6rem;
|
|
120
|
+
padding: 0.55rem 0.7rem;
|
|
121
|
+
font-size: 0.75rem;
|
|
122
|
+
line-height: 1.45;
|
|
123
|
+
}
|
|
124
|
+
.hub-status-mock {
|
|
125
|
+
background: rgba(245, 158, 11, 0.12);
|
|
126
|
+
color: #fde68a;
|
|
127
|
+
border: 1px solid rgba(245, 158, 11, 0.35);
|
|
128
|
+
}
|
|
129
|
+
.hub-status-live {
|
|
130
|
+
background: rgba(16, 185, 129, 0.1);
|
|
131
|
+
color: #a7f3d0;
|
|
132
|
+
border: 1px solid rgba(16, 185, 129, 0.28);
|
|
133
|
+
}
|
|
134
|
+
.hub-chip {
|
|
135
|
+
display: inline-flex;
|
|
136
|
+
align-items: center;
|
|
137
|
+
border-radius: 999px;
|
|
138
|
+
border: 1px solid #3f3f46;
|
|
139
|
+
padding: 0.1rem 0.5rem;
|
|
140
|
+
font-size: 0.7rem;
|
|
141
|
+
color: #a1a1aa;
|
|
142
|
+
}
|
|
143
|
+
.hub-chip-on {
|
|
144
|
+
border-color: rgba(16, 185, 129, 0.4);
|
|
145
|
+
background: rgba(16, 185, 129, 0.12);
|
|
146
|
+
color: #6ee7b7;
|
|
147
|
+
}
|
|
148
|
+
.hub-chip-off {
|
|
149
|
+
color: #71717a;
|
|
150
|
+
}
|
|
151
|
+
.hub-chip-mock {
|
|
152
|
+
border-color: rgba(245, 158, 11, 0.45);
|
|
153
|
+
background: rgba(245, 158, 11, 0.12);
|
|
154
|
+
color: #fde68a;
|
|
155
|
+
}
|
|
156
|
+
.hub-agent {
|
|
157
|
+
border: 1px solid #27272a;
|
|
158
|
+
border-radius: 0.7rem;
|
|
159
|
+
margin-bottom: 0.5rem;
|
|
160
|
+
background: #0c0c0e;
|
|
161
|
+
overflow: hidden;
|
|
162
|
+
}
|
|
163
|
+
.hub-agent-title {
|
|
164
|
+
display: flex;
|
|
165
|
+
min-width: 0;
|
|
166
|
+
flex: 1;
|
|
167
|
+
align-items: center;
|
|
168
|
+
gap: 0.5rem;
|
|
169
|
+
}
|
|
170
|
+
.hub-dot {
|
|
171
|
+
width: 0.5rem;
|
|
172
|
+
height: 0.5rem;
|
|
173
|
+
border-radius: 999px;
|
|
174
|
+
flex-shrink: 0;
|
|
175
|
+
}
|
|
176
|
+
.hub-stage {
|
|
177
|
+
margin-left: auto;
|
|
178
|
+
font-size: 0.65rem;
|
|
179
|
+
color: #a1a1aa;
|
|
180
|
+
background: #18181b;
|
|
181
|
+
border: 1px solid #27272a;
|
|
182
|
+
border-radius: 999px;
|
|
183
|
+
padding: 0.1rem 0.45rem;
|
|
184
|
+
flex-shrink: 0;
|
|
185
|
+
}
|
|
186
|
+
.hub-home {
|
|
187
|
+
width: 100%;
|
|
188
|
+
max-width: 36rem;
|
|
189
|
+
display: grid;
|
|
190
|
+
gap: 1rem;
|
|
191
|
+
}
|
|
192
|
+
.hub-home h1 {
|
|
193
|
+
margin: 0;
|
|
194
|
+
font-size: 1.25rem;
|
|
195
|
+
font-weight: 600;
|
|
196
|
+
letter-spacing: -0.03em;
|
|
197
|
+
}
|
|
198
|
+
.hub-card-grid {
|
|
199
|
+
display: grid;
|
|
200
|
+
gap: 0.6rem;
|
|
201
|
+
grid-template-columns: 1fr;
|
|
202
|
+
}
|
|
203
|
+
.hub-card {
|
|
204
|
+
display: flex;
|
|
205
|
+
flex-direction: column;
|
|
206
|
+
gap: 0.25rem;
|
|
207
|
+
width: 100%;
|
|
208
|
+
text-align: left;
|
|
209
|
+
background: #111113;
|
|
210
|
+
border: 1px solid #27272a;
|
|
211
|
+
border-radius: 0.75rem;
|
|
212
|
+
padding: 0.8rem 0.9rem;
|
|
213
|
+
}
|
|
214
|
+
.hub-card:hover {
|
|
215
|
+
border-color: #3f3f46;
|
|
216
|
+
background: #18181b;
|
|
217
|
+
}
|
|
218
|
+
.hub-banner-mock {
|
|
219
|
+
border-bottom: 1px solid rgba(245, 158, 11, 0.3);
|
|
220
|
+
background: rgba(245, 158, 11, 0.1);
|
|
221
|
+
color: #fde68a;
|
|
222
|
+
padding: 0.6rem 1rem;
|
|
223
|
+
font-size: 0.85rem;
|
|
224
|
+
line-height: 1.45;
|
|
225
|
+
}
|
|
226
|
+
.hub-kicker {
|
|
227
|
+
margin: 0;
|
|
228
|
+
font-size: 0.68rem;
|
|
229
|
+
font-weight: 600;
|
|
230
|
+
letter-spacing: 0.14em;
|
|
231
|
+
text-transform: uppercase;
|
|
232
|
+
color: #71717a;
|
|
233
|
+
}
|
|
234
|
+
.hub-card-row {
|
|
235
|
+
display: flex;
|
|
236
|
+
align-items: center;
|
|
237
|
+
gap: 0.5rem;
|
|
238
|
+
}
|
|
239
|
+
.hub-card-copy {
|
|
240
|
+
color: #a1a1aa;
|
|
241
|
+
font-size: 0.75rem;
|
|
242
|
+
line-height: 1.45;
|
|
243
|
+
}
|
|
244
|
+
.hub-topbar {
|
|
245
|
+
display: flex;
|
|
246
|
+
flex-wrap: wrap;
|
|
247
|
+
align-items: center;
|
|
248
|
+
gap: 0.5rem;
|
|
249
|
+
border-bottom: 1px solid #27272a;
|
|
250
|
+
padding: 0.7rem 1rem;
|
|
251
|
+
background: #0c0c0e;
|
|
252
|
+
}
|
|
253
|
+
.hub-composer {
|
|
254
|
+
display: flex;
|
|
255
|
+
align-items: flex-end;
|
|
256
|
+
gap: 0.5rem;
|
|
257
|
+
margin: 0.75rem;
|
|
258
|
+
padding: 0.55rem 0.6rem;
|
|
259
|
+
border: 1px solid #27272a;
|
|
260
|
+
border-radius: 1rem;
|
|
261
|
+
background: #111113;
|
|
262
|
+
}
|
|
263
|
+
.hub-msg {
|
|
264
|
+
max-width: 85%;
|
|
265
|
+
border-radius: 1rem;
|
|
266
|
+
padding: 0.55rem 0.85rem;
|
|
267
|
+
font-size: 0.875rem;
|
|
268
|
+
line-height: 1.5rem;
|
|
269
|
+
}
|
|
270
|
+
.hub-msg-user {
|
|
271
|
+
margin-left: auto;
|
|
272
|
+
background: #27272a;
|
|
273
|
+
color: #fafafa;
|
|
274
|
+
}
|
|
275
|
+
.hub-msg-agent {
|
|
276
|
+
margin-right: auto;
|
|
277
|
+
background: #18181b;
|
|
278
|
+
color: #f4f4f5;
|
|
279
|
+
border: 1px solid #27272a;
|
|
280
|
+
}
|
|
281
|
+
.hub-msg-tool {
|
|
282
|
+
margin-right: auto;
|
|
283
|
+
max-width: 90%;
|
|
284
|
+
border-radius: 0.5rem;
|
|
285
|
+
border: 1px solid #27272a;
|
|
286
|
+
background: #09090b;
|
|
287
|
+
padding: 0.3rem 0.55rem;
|
|
288
|
+
font-family: "Cascadia Mono", Consolas, ui-monospace, monospace;
|
|
289
|
+
font-size: 11px;
|
|
290
|
+
line-height: 1.25rem;
|
|
291
|
+
color: #a1a1aa;
|
|
292
|
+
}
|
|
293
|
+
.hub-msg-handoff {
|
|
294
|
+
margin-right: auto;
|
|
295
|
+
border-radius: 0;
|
|
296
|
+
border-left: 2px solid #fbbf24;
|
|
297
|
+
background: #18181b;
|
|
298
|
+
color: #f4f4f5;
|
|
299
|
+
}
|
|
300
|
+
.hub-msg-error {
|
|
301
|
+
margin-right: auto;
|
|
302
|
+
border: 1px solid rgba(239, 68, 68, 0.4);
|
|
303
|
+
background: rgba(239, 68, 68, 0.1);
|
|
304
|
+
color: #fecaca;
|
|
305
|
+
}
|
|
306
|
+
.hub-empty-chat {
|
|
307
|
+
max-width: 32rem;
|
|
308
|
+
margin: 1.5rem auto 0.5rem;
|
|
309
|
+
display: grid;
|
|
310
|
+
gap: 0.4rem;
|
|
311
|
+
text-align: center;
|
|
312
|
+
color: #a1a1aa;
|
|
313
|
+
font-size: 0.875rem;
|
|
314
|
+
line-height: 1.5rem;
|
|
315
|
+
}
|
|
316
|
+
.hub-empty-chat h2 {
|
|
317
|
+
margin: 0;
|
|
318
|
+
font-size: 1.15rem;
|
|
319
|
+
font-weight: 600;
|
|
320
|
+
color: #fafafa;
|
|
321
|
+
letter-spacing: -0.03em;
|
|
322
|
+
}
|
|
323
|
+
.hub-session {
|
|
324
|
+
display: flex;
|
|
325
|
+
align-items: center;
|
|
326
|
+
gap: 0.4rem;
|
|
327
|
+
width: 100%;
|
|
328
|
+
overflow: hidden;
|
|
329
|
+
text-align: left;
|
|
330
|
+
border: 0;
|
|
331
|
+
background: transparent;
|
|
332
|
+
border-radius: 0.5rem;
|
|
333
|
+
padding: 0.4rem 0.5rem;
|
|
334
|
+
font-size: 0.75rem;
|
|
335
|
+
color: #d4d4d8;
|
|
336
|
+
}
|
|
337
|
+
.hub-session:hover { background: #18181b; }
|
|
338
|
+
.hub-session-active {
|
|
339
|
+
background: #27272a;
|
|
340
|
+
color: #fafafa;
|
|
341
|
+
}
|
|
342
|
+
.hub-session-tag {
|
|
343
|
+
margin-left: auto;
|
|
344
|
+
flex-shrink: 0;
|
|
345
|
+
font-size: 0.62rem;
|
|
346
|
+
color: #6ee7b7;
|
|
347
|
+
}
|
|
348
|
+
.hub-session-tag-err { color: #fca5a5; }
|
|
349
|
+
.hub-quiet-btn {
|
|
350
|
+
display: inline-flex;
|
|
351
|
+
align-items: center;
|
|
352
|
+
gap: 0.3rem;
|
|
353
|
+
border: 0;
|
|
354
|
+
background: transparent;
|
|
355
|
+
padding: 0;
|
|
356
|
+
font-size: 0.75rem;
|
|
357
|
+
color: #a1a1aa;
|
|
358
|
+
}
|
|
359
|
+
.hub-quiet-btn:hover { color: #e4e4e7; }
|
|
360
|
+
.hub-footer-link {
|
|
361
|
+
display: flex;
|
|
362
|
+
align-items: center;
|
|
363
|
+
gap: 0.45rem;
|
|
364
|
+
flex-shrink: 0;
|
|
365
|
+
padding: 0.85rem 1rem;
|
|
366
|
+
font-size: 0.875rem;
|
|
367
|
+
color: #a1a1aa;
|
|
368
|
+
}
|
|
369
|
+
.hub-footer-link:hover { color: #f4f4f5; }
|
|
370
|
+
.hub-spend {
|
|
371
|
+
width: 100%;
|
|
372
|
+
max-width: 56rem;
|
|
373
|
+
margin: 0 auto;
|
|
374
|
+
display: flex;
|
|
375
|
+
flex-direction: column;
|
|
376
|
+
flex: 1;
|
|
377
|
+
gap: 1.5rem;
|
|
378
|
+
padding: 1.5rem;
|
|
379
|
+
}
|
|
380
|
+
.hub-spend h1 {
|
|
381
|
+
margin: 0;
|
|
382
|
+
font-size: 1.25rem;
|
|
383
|
+
font-weight: 600;
|
|
384
|
+
letter-spacing: -0.03em;
|
|
385
|
+
}
|
|
386
|
+
.hub-stat-grid {
|
|
387
|
+
display: grid;
|
|
388
|
+
gap: 0.75rem;
|
|
389
|
+
grid-template-columns: 1fr;
|
|
390
|
+
}
|
|
391
|
+
.hub-stat {
|
|
392
|
+
border: 1px solid #27272a;
|
|
393
|
+
border-radius: 0.85rem;
|
|
394
|
+
padding: 0.9rem 1rem;
|
|
395
|
+
background: #111113;
|
|
396
|
+
}
|
|
397
|
+
.hub-stat p {
|
|
398
|
+
margin: 0;
|
|
399
|
+
font-size: 0.75rem;
|
|
400
|
+
color: #a1a1aa;
|
|
401
|
+
}
|
|
402
|
+
.hub-stat strong {
|
|
403
|
+
display: block;
|
|
404
|
+
margin-top: 0.35rem;
|
|
405
|
+
font-size: 1.15rem;
|
|
406
|
+
font-weight: 600;
|
|
407
|
+
}
|
|
408
|
+
.hub-table-wrap { overflow-x: auto; }
|
|
409
|
+
@media (min-width: 640px) {
|
|
410
|
+
.hub-stat-grid { grid-template-columns: 1fr 1fr 1fr; }
|
|
411
|
+
}
|
|
412
|
+
@media (min-width: 700px) {
|
|
413
|
+
.hub-card-grid { grid-template-columns: 1fr 1fr; }
|
|
414
|
+
}
|
|
71
415
|
@media (min-width: 768px) {
|
|
72
416
|
.md\:block { display: block; }
|
|
73
417
|
.md\:hidden { display: none; }
|
|
@@ -81,7 +81,7 @@ export function AgentDialog({
|
|
|
81
81
|
<DialogHeader>
|
|
82
82
|
<DialogTitle>{agent ? "Editar agente" : "Novo agente"}</DialogTitle>
|
|
83
83
|
<DialogDescription>
|
|
84
|
-
Nome, etapa e harness definem como esta persona atua no workspace.
|
|
84
|
+
Nome, etapa e harness definem como esta persona atua no workspace. Simulador não chama CLI e não edita o disco.
|
|
85
85
|
</DialogDescription>
|
|
86
86
|
</DialogHeader>
|
|
87
87
|
<form
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useRef, type ReactNode } from "react";
|
|
4
|
+
import { ArrowRightLeft, GitBranch } from "lucide-react";
|
|
4
5
|
import { Composer } from "@/components/composer";
|
|
5
6
|
import { MarkdownLite } from "@/components/markdown-lite";
|
|
6
7
|
import { Badge } from "@/components/ui/badge";
|
|
7
8
|
import { Button } from "@/components/ui/button";
|
|
8
|
-
import { WorkspaceField } from "@/components/workspace-field";
|
|
9
9
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
STAGE_LABEL,
|
|
12
|
+
clisMissing,
|
|
13
|
+
isHarnessSimulated,
|
|
14
|
+
liveHarnessLabel,
|
|
15
|
+
simulatedBanner,
|
|
16
|
+
type HarnessAvailability,
|
|
17
|
+
} from "@/lib/labels";
|
|
11
18
|
import { cn } from "@/lib/utils";
|
|
12
19
|
import type { Agent, ChatMessage, Session } from "@/lib/types";
|
|
13
20
|
|
|
@@ -15,57 +22,90 @@ export type LiveSegment =
|
|
|
15
22
|
| { id: string; kind: "assistant"; text: string }
|
|
16
23
|
| { id: string; kind: "tool"; name: string; detail: string; status: "started" | "completed" };
|
|
17
24
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
function Bubble({
|
|
26
|
+
role,
|
|
27
|
+
children,
|
|
28
|
+
live,
|
|
29
|
+
simulated,
|
|
30
|
+
}: {
|
|
31
|
+
role: ChatMessage["role"];
|
|
32
|
+
children: ReactNode;
|
|
33
|
+
live?: boolean;
|
|
34
|
+
simulated?: boolean;
|
|
35
|
+
}) {
|
|
21
36
|
if (role === "tool") {
|
|
22
|
-
return
|
|
23
|
-
<div className="mr-auto max-w-[90%] rounded-md border border-zinc-800 bg-black px-2 py-1 font-mono text-[11px] leading-5 text-zinc-400">
|
|
24
|
-
{children}
|
|
25
|
-
</div>
|
|
26
|
-
);
|
|
37
|
+
return <div className="hub-msg hub-msg-tool">{children}</div>;
|
|
27
38
|
}
|
|
28
39
|
if (role === "handoff") {
|
|
29
|
-
return
|
|
30
|
-
<div className="mr-auto max-w-[85%] border-l-2 border-amber-400 bg-zinc-900 px-3 py-2 text-sm text-zinc-100">
|
|
31
|
-
{children}
|
|
32
|
-
</div>
|
|
33
|
-
);
|
|
40
|
+
return <div className="hub-msg hub-msg-handoff">{children}</div>;
|
|
34
41
|
}
|
|
35
42
|
if (role === "system") {
|
|
36
43
|
return <p className="text-center text-xs text-zinc-500">{children}</p>;
|
|
37
44
|
}
|
|
38
45
|
const user = role === "user";
|
|
39
46
|
return (
|
|
40
|
-
<div
|
|
41
|
-
className={cn(
|
|
42
|
-
"max-w-[85%] rounded-2xl px-3 py-2 text-sm leading-6",
|
|
43
|
-
user ? "ml-auto bg-zinc-800 text-zinc-50" : "mr-auto bg-zinc-900 text-zinc-100",
|
|
44
|
-
)}
|
|
45
|
-
>
|
|
47
|
+
<div className={cn("hub-msg", user ? "hub-msg-user" : "hub-msg-agent")}>
|
|
46
48
|
<span className="sr-only">{user ? "Você" : "Agente"}</span>
|
|
49
|
+
{!user && simulated && <p className="hub-kicker mb-1">Simulador</p>}
|
|
47
50
|
{children}
|
|
48
51
|
{live && <span className="ml-0.5 inline-block animate-pulse text-zinc-500">▍</span>}
|
|
49
52
|
</div>
|
|
50
53
|
);
|
|
51
54
|
}
|
|
52
55
|
|
|
56
|
+
function HomeBoard({
|
|
57
|
+
agents,
|
|
58
|
+
workspaceMissing,
|
|
59
|
+
onNewSession,
|
|
60
|
+
}: {
|
|
61
|
+
agents: Agent[];
|
|
62
|
+
workspaceMissing: boolean;
|
|
63
|
+
onNewSession: (agentId: string) => void;
|
|
64
|
+
}) {
|
|
65
|
+
return (
|
|
66
|
+
<div className="hub-home">
|
|
67
|
+
<p className="hub-kicker">SDLC</p>
|
|
68
|
+
<h1>Escolha um agente para começar</h1>
|
|
69
|
+
<p className="text-sm leading-6 text-zinc-400">
|
|
70
|
+
Cada persona opera no workspace local. Handoff passa o contexto para a próxima etapa; subagente abre uma conversa
|
|
71
|
+
filha sem sair desta.
|
|
72
|
+
</p>
|
|
73
|
+
{workspaceMissing && (
|
|
74
|
+
<p className="hub-status hub-status-mock">
|
|
75
|
+
O workspace ainda está vazio. Informe o caminho na barra lateral. Sem pasta, o Simulador responde e o disco não
|
|
76
|
+
é alterado.
|
|
77
|
+
</p>
|
|
78
|
+
)}
|
|
79
|
+
<div className="hub-card-grid">
|
|
80
|
+
{agents.map((agent) => (
|
|
81
|
+
<button key={agent.id} type="button" className="hub-card" onClick={() => onNewSession(agent.id)}>
|
|
82
|
+
<span className="hub-card-row">
|
|
83
|
+
<span className="hub-dot" style={{ backgroundColor: agent.color }} />
|
|
84
|
+
<span className="font-medium text-zinc-50">{agent.name}</span>
|
|
85
|
+
<span className="hub-stage">{STAGE_LABEL[agent.stage]}</span>
|
|
86
|
+
</span>
|
|
87
|
+
<span className="hub-card-copy">{agent.specs}</span>
|
|
88
|
+
</button>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
53
95
|
export function ChatPane({
|
|
54
96
|
phase,
|
|
55
97
|
agent,
|
|
56
98
|
session,
|
|
99
|
+
agents,
|
|
57
100
|
messages,
|
|
58
101
|
live,
|
|
59
102
|
streamError,
|
|
60
103
|
running,
|
|
61
|
-
|
|
62
|
-
workspace,
|
|
104
|
+
harness,
|
|
63
105
|
workspaceMissing,
|
|
64
|
-
ready,
|
|
65
|
-
onWorkspaceChange,
|
|
66
|
-
onWorkspaceCommit,
|
|
67
106
|
onCreateAgent,
|
|
68
107
|
onLoadStarters,
|
|
108
|
+
onNewSession,
|
|
69
109
|
onRetry,
|
|
70
110
|
onSend,
|
|
71
111
|
onHandoff,
|
|
@@ -73,21 +113,19 @@ export function ChatPane({
|
|
|
73
113
|
draft,
|
|
74
114
|
onDraftChange,
|
|
75
115
|
}: {
|
|
76
|
-
phase: "loading" | "error" | "
|
|
116
|
+
phase: "loading" | "error" | "agents" | "session" | "chat";
|
|
77
117
|
agent: Agent | null;
|
|
78
118
|
session: Session | null;
|
|
119
|
+
agents: Agent[];
|
|
79
120
|
messages: ChatMessage[];
|
|
80
121
|
live: LiveSegment[];
|
|
81
122
|
streamError: string | null;
|
|
82
123
|
running: boolean;
|
|
83
|
-
|
|
84
|
-
workspace: string;
|
|
124
|
+
harness: HarnessAvailability | null;
|
|
85
125
|
workspaceMissing: boolean;
|
|
86
|
-
ready: boolean;
|
|
87
|
-
onWorkspaceChange: (value: string) => void;
|
|
88
|
-
onWorkspaceCommit: (value?: string) => void;
|
|
89
126
|
onCreateAgent: () => void;
|
|
90
127
|
onLoadStarters: () => void;
|
|
128
|
+
onNewSession: (agentId: string) => void;
|
|
91
129
|
onRetry: () => void;
|
|
92
130
|
onSend: (text: string) => Promise<boolean>;
|
|
93
131
|
onHandoff: () => void;
|
|
@@ -100,23 +138,30 @@ export function ChatPane({
|
|
|
100
138
|
endRef.current?.scrollIntoView({ block: "end" });
|
|
101
139
|
}, [messages, live, streamError, phase]);
|
|
102
140
|
|
|
141
|
+
const simulated = agent ? isHarnessSimulated(agent.harness, harness) : clisMissing(harness);
|
|
142
|
+
const emptyChat = messages.length === 0 && live.length === 0 && !streamError;
|
|
143
|
+
|
|
103
144
|
return (
|
|
104
145
|
<section className="flex min-h-0 min-w-0 flex-1 flex-col bg-zinc-950">
|
|
105
|
-
{
|
|
106
|
-
<p className="
|
|
146
|
+
{simulated && (
|
|
147
|
+
<p className="hub-banner-mock">{agent ? simulatedBanner(agent.harness) : "Nenhum CLI encontrado. O Simulador está ativo — o projeto no disco não será editado."}</p>
|
|
107
148
|
)}
|
|
108
149
|
{phase === "chat" && agent && session ? (
|
|
109
150
|
<>
|
|
110
|
-
<header className="
|
|
151
|
+
<header className="hub-topbar">
|
|
111
152
|
<span className="size-2 shrink-0 rounded-full" style={{ backgroundColor: agent.color }} />
|
|
112
153
|
<span className="font-medium">{agent.name}</span>
|
|
113
154
|
<Badge variant="secondary">{STAGE_LABEL[agent.stage]}</Badge>
|
|
114
|
-
<
|
|
155
|
+
<span className={cn("hub-chip", simulated ? "hub-chip-mock" : "hub-chip-on")}>
|
|
156
|
+
{liveHarnessLabel(agent.harness, harness)}
|
|
157
|
+
</span>
|
|
115
158
|
<span className="min-w-0 flex-1 truncate text-sm text-zinc-400">{session.title}</span>
|
|
116
159
|
<Button type="button" size="sm" variant="outline" disabled={running} onClick={onHandoff}>
|
|
160
|
+
<ArrowRightLeft />
|
|
117
161
|
Handoff
|
|
118
162
|
</Button>
|
|
119
163
|
<Button type="button" size="sm" variant="outline" disabled={running} onClick={onSubagent}>
|
|
164
|
+
<GitBranch />
|
|
120
165
|
Subagente
|
|
121
166
|
</Button>
|
|
122
167
|
</header>
|
|
@@ -128,8 +173,20 @@ export function ChatPane({
|
|
|
128
173
|
<div className="relative min-h-0 flex-1">
|
|
129
174
|
<ScrollArea className="absolute inset-0">
|
|
130
175
|
<div className="flex flex-col gap-3 p-4">
|
|
176
|
+
{emptyChat && (
|
|
177
|
+
<div className="hub-empty-chat">
|
|
178
|
+
<p className="hub-kicker">{STAGE_LABEL[agent.stage]}</p>
|
|
179
|
+
<h2>{agent.name}</h2>
|
|
180
|
+
<p>{agent.specs}</p>
|
|
181
|
+
{simulated ? (
|
|
182
|
+
<p className="text-amber-200/90">Modo Simulador — nada será gravado no disco neste turno.</p>
|
|
183
|
+
) : (
|
|
184
|
+
<p>Escreva o pedido. O agente usa a CLI da sua assinatura no workspace.</p>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
131
188
|
{messages.map((message) => (
|
|
132
|
-
<Bubble key={message.id} role={message.role}>
|
|
189
|
+
<Bubble key={message.id} role={message.role} simulated={simulated}>
|
|
133
190
|
{message.role === "user" || message.role === "tool" || message.role === "system" ? (
|
|
134
191
|
<span className="whitespace-pre-wrap">
|
|
135
192
|
{message.role === "tool" && message.toolName ? `${message.toolName} · ` : ""}
|
|
@@ -146,18 +203,18 @@ export function ChatPane({
|
|
|
146
203
|
{segment.name} · {segment.status}: {segment.detail}
|
|
147
204
|
</Bubble>
|
|
148
205
|
) : (
|
|
149
|
-
<Bubble key={segment.id} role="assistant" live={running && index === live.length - 1}>
|
|
206
|
+
<Bubble key={segment.id} role="assistant" live={running && index === live.length - 1} simulated={simulated}>
|
|
150
207
|
<MarkdownLite text={segment.text} />
|
|
151
208
|
</Bubble>
|
|
152
209
|
),
|
|
153
210
|
)}
|
|
154
211
|
{running && live.length === 0 && (
|
|
155
212
|
<Bubble role="assistant" live>
|
|
156
|
-
<span className="text-zinc-500"
|
|
213
|
+
<span className="text-zinc-500">Aguardando o harness…</span>
|
|
157
214
|
</Bubble>
|
|
158
215
|
)}
|
|
159
216
|
{streamError && (
|
|
160
|
-
<div className="
|
|
217
|
+
<div className="hub-msg hub-msg-error">
|
|
161
218
|
<p>{streamError}</p>
|
|
162
219
|
<p className="mt-1 text-xs text-red-200/80">Envie a mensagem de novo para tentar outra vez.</p>
|
|
163
220
|
</div>
|
|
@@ -171,6 +228,7 @@ export function ChatPane({
|
|
|
171
228
|
sessionId={session.id}
|
|
172
229
|
draft={draft}
|
|
173
230
|
disabled={running}
|
|
231
|
+
simulated={simulated}
|
|
174
232
|
onDraftChange={onDraftChange}
|
|
175
233
|
onSend={onSend}
|
|
176
234
|
/>
|
|
@@ -186,23 +244,10 @@ export function ChatPane({
|
|
|
186
244
|
</Button>
|
|
187
245
|
</div>
|
|
188
246
|
)}
|
|
189
|
-
{phase === "workspace" && (
|
|
190
|
-
<div className="grid w-full max-w-md gap-3">
|
|
191
|
-
<h1 className="text-lg font-medium">Informe o caminho do workspace</h1>
|
|
192
|
-
<p className="text-sm leading-6 text-zinc-400">
|
|
193
|
-
No app do Windows, use Pasta e escolha o repositório. Ou cole um caminho absoluto como C:\src\meu-app.
|
|
194
|
-
</p>
|
|
195
|
-
<WorkspaceField
|
|
196
|
-
value={workspace}
|
|
197
|
-
disabled={!ready}
|
|
198
|
-
onChange={onWorkspaceChange}
|
|
199
|
-
onCommit={onWorkspaceCommit}
|
|
200
|
-
/>
|
|
201
|
-
</div>
|
|
202
|
-
)}
|
|
203
247
|
{phase === "agents" && (
|
|
204
|
-
<div className="
|
|
205
|
-
<
|
|
248
|
+
<div className="hub-home">
|
|
249
|
+
<p className="hub-kicker">Agentes</p>
|
|
250
|
+
<h1>Nenhum agente neste hub</h1>
|
|
206
251
|
<p className="text-sm leading-6 text-zinc-400">
|
|
207
252
|
Crie uma persona ou carregue os seis agentes do SDLC: Spec, Arquiteto, Implementador, Revisor, Testador e
|
|
208
253
|
Release.
|
|
@@ -218,10 +263,7 @@ export function ChatPane({
|
|
|
218
263
|
</div>
|
|
219
264
|
)}
|
|
220
265
|
{phase === "session" && (
|
|
221
|
-
<
|
|
222
|
-
<h1 className="text-lg font-medium">Escolha ou comece uma conversa</h1>
|
|
223
|
-
<p className="text-sm text-zinc-400">Selecione um agente na barra lateral e abra uma conversa nova.</p>
|
|
224
|
-
</div>
|
|
266
|
+
<HomeBoard agents={agents} workspaceMissing={workspaceMissing} onNewSession={onNewSession} />
|
|
225
267
|
)}
|
|
226
268
|
</div>
|
|
227
269
|
)}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { ArrowUp } from "lucide-react";
|
|
3
4
|
import { Button } from "@/components/ui/button";
|
|
4
5
|
import { Textarea } from "@/components/ui/textarea";
|
|
5
6
|
|
|
@@ -7,12 +8,14 @@ export function Composer({
|
|
|
7
8
|
sessionId,
|
|
8
9
|
draft,
|
|
9
10
|
disabled,
|
|
11
|
+
simulated,
|
|
10
12
|
onDraftChange,
|
|
11
13
|
onSend,
|
|
12
14
|
}: {
|
|
13
15
|
sessionId: string;
|
|
14
16
|
draft: string;
|
|
15
17
|
disabled: boolean;
|
|
18
|
+
simulated: boolean;
|
|
16
19
|
onDraftChange: (sessionId: string, text: string) => void;
|
|
17
20
|
onSend: (text: string) => Promise<boolean>;
|
|
18
21
|
}) {
|
|
@@ -28,7 +31,7 @@ export function Composer({
|
|
|
28
31
|
|
|
29
32
|
return (
|
|
30
33
|
<form
|
|
31
|
-
className="
|
|
34
|
+
className="hub-composer"
|
|
32
35
|
onSubmit={(event) => {
|
|
33
36
|
event.preventDefault();
|
|
34
37
|
void submit();
|
|
@@ -38,8 +41,14 @@ export function Composer({
|
|
|
38
41
|
value={draft}
|
|
39
42
|
disabled={disabled}
|
|
40
43
|
rows={2}
|
|
41
|
-
placeholder={
|
|
42
|
-
|
|
44
|
+
placeholder={
|
|
45
|
+
disabled
|
|
46
|
+
? "Aguardando a resposta do agente…"
|
|
47
|
+
: simulated
|
|
48
|
+
? "Escreva no Simulador — o disco não será alterado"
|
|
49
|
+
: "Escreva uma mensagem"
|
|
50
|
+
}
|
|
51
|
+
className="max-h-40 min-h-16 flex-1 resize-none border-0 bg-transparent shadow-none focus-visible:ring-0"
|
|
43
52
|
onChange={(event) => onDraftChange(sessionId, event.target.value)}
|
|
44
53
|
onKeyDown={(event) => {
|
|
45
54
|
if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return;
|
|
@@ -47,8 +56,8 @@ export function Composer({
|
|
|
47
56
|
void submit();
|
|
48
57
|
}}
|
|
49
58
|
/>
|
|
50
|
-
<Button type="submit" disabled={disabled || !trimmed}>
|
|
51
|
-
|
|
59
|
+
<Button type="submit" size="icon" disabled={disabled || !trimmed} aria-label="Enviar">
|
|
60
|
+
<ArrowUp />
|
|
52
61
|
</Button>
|
|
53
62
|
</form>
|
|
54
63
|
);
|
|
@@ -7,5 +7,12 @@ export function DesktopChrome() {
|
|
|
7
7
|
const [windows, setWindows] = useState(false);
|
|
8
8
|
useEffect(() => setWindows(isWindowsDesktop()), []);
|
|
9
9
|
if (!windows) return null;
|
|
10
|
-
return
|
|
10
|
+
return (
|
|
11
|
+
<div className="ah-drag flex h-9 shrink-0 items-center bg-black px-3" aria-hidden>
|
|
12
|
+
<span className="hub-brand text-xs text-zinc-400">
|
|
13
|
+
<span className="hub-mark" />
|
|
14
|
+
Agent Hub
|
|
15
|
+
</span>
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
11
18
|
}
|
|
@@ -113,19 +113,16 @@ export function HubShell() {
|
|
|
113
113
|
|
|
114
114
|
const selectedSession = sessions.find((session) => session.id === selectedSessionId) ?? null;
|
|
115
115
|
const selectedAgent = selectedSession ? (agents.find((agent) => agent.id === selectedSession.agentId) ?? null) : null;
|
|
116
|
-
const cliMissing = harness !== null && !harness.cursor && !harness.claude;
|
|
117
116
|
const workspaceMissing = !(settings?.workspace ?? "").trim();
|
|
118
117
|
const phase = !ready
|
|
119
118
|
? loadError
|
|
120
119
|
? "error"
|
|
121
120
|
: "loading"
|
|
122
|
-
:
|
|
123
|
-
? "
|
|
124
|
-
:
|
|
125
|
-
? "
|
|
126
|
-
:
|
|
127
|
-
? "chat"
|
|
128
|
-
: "session";
|
|
121
|
+
: agents.length === 0 && !selectedSession
|
|
122
|
+
? "agents"
|
|
123
|
+
: selectedSession && selectedAgent
|
|
124
|
+
? "chat"
|
|
125
|
+
: "session";
|
|
129
126
|
|
|
130
127
|
async function commitWorkspace(next?: string) {
|
|
131
128
|
const workspace = next ?? workspaceDraft;
|
|
@@ -347,7 +344,7 @@ export function HubShell() {
|
|
|
347
344
|
|
|
348
345
|
return (
|
|
349
346
|
<div className="flex h-dvh min-h-0 w-full overflow-hidden bg-black text-zinc-100">
|
|
350
|
-
<aside className="hidden h-full w-[
|
|
347
|
+
<aside className="hidden h-full w-[300px] shrink-0 border-r border-zinc-800 md:block">
|
|
351
348
|
<Sidebar idPrefix="desktop" {...sidebarProps} />
|
|
352
349
|
</aside>
|
|
353
350
|
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
|
@@ -355,24 +352,25 @@ export function HubShell() {
|
|
|
355
352
|
<Button type="button" variant="ghost" size="icon" aria-label="Abrir menu" onClick={() => setSheetOpen(true)}>
|
|
356
353
|
<Menu />
|
|
357
354
|
</Button>
|
|
358
|
-
<span className="text-sm
|
|
355
|
+
<span className="hub-brand text-sm">
|
|
356
|
+
<span className="hub-mark" aria-hidden />
|
|
357
|
+
Agent Hub
|
|
358
|
+
</span>
|
|
359
359
|
</div>
|
|
360
360
|
<ChatPane
|
|
361
361
|
phase={phase}
|
|
362
362
|
agent={selectedAgent}
|
|
363
363
|
session={selectedSession}
|
|
364
|
+
agents={agents}
|
|
364
365
|
messages={messages}
|
|
365
366
|
live={live}
|
|
366
367
|
streamError={streamError}
|
|
367
368
|
running={selectedSession?.status === "running"}
|
|
368
|
-
|
|
369
|
-
workspace={workspaceDraft}
|
|
369
|
+
harness={harness}
|
|
370
370
|
workspaceMissing={workspaceMissing}
|
|
371
|
-
ready={ready}
|
|
372
|
-
onWorkspaceChange={setWorkspaceDraft}
|
|
373
|
-
onWorkspaceCommit={(value) => void commitWorkspace(value)}
|
|
374
371
|
onCreateAgent={openCreate}
|
|
375
372
|
onLoadStarters={() => void loadStarters()}
|
|
373
|
+
onNewSession={(agentId) => void createSession(agentId)}
|
|
376
374
|
onRetry={() => {
|
|
377
375
|
setReady(false);
|
|
378
376
|
setLoadError(null);
|
|
@@ -394,7 +392,7 @@ export function HubShell() {
|
|
|
394
392
|
/>
|
|
395
393
|
</div>
|
|
396
394
|
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
|
397
|
-
<SheetContent side="left" className="w-[
|
|
395
|
+
<SheetContent side="left" className="w-[300px] max-w-[300px] gap-0 bg-black p-0 sm:max-w-[300px]">
|
|
398
396
|
<SheetTitle className="sr-only">Agent Hub</SheetTitle>
|
|
399
397
|
<Sidebar idPrefix="mobile" {...sidebarProps} />
|
|
400
398
|
</SheetContent>
|
|
@@ -1,31 +1,22 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import Link from "next/link";
|
|
4
|
+
import { MessageSquarePlus, Pencil, Plus, Wallet } from "lucide-react";
|
|
4
5
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
|
5
|
-
import { Badge } from "@/components/ui/badge";
|
|
6
6
|
import { Button } from "@/components/ui/button";
|
|
7
7
|
import { WorkspaceField } from "@/components/workspace-field";
|
|
8
8
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
9
9
|
import { Separator } from "@/components/ui/separator";
|
|
10
10
|
import { sessionsForAgent } from "@/lib/group-sessions";
|
|
11
|
-
import { STAGE_LABEL } from "@/lib/labels";
|
|
11
|
+
import { clisMissing, STAGE_LABEL, type HarnessAvailability } from "@/lib/labels";
|
|
12
12
|
import { cn } from "@/lib/utils";
|
|
13
13
|
import type { Agent, Session } from "@/lib/types";
|
|
14
14
|
|
|
15
|
-
export type HarnessAvailability
|
|
16
|
-
cursor: boolean;
|
|
17
|
-
claude: boolean;
|
|
18
|
-
};
|
|
15
|
+
export type { HarnessAvailability };
|
|
19
16
|
|
|
20
|
-
function Chip({ label,
|
|
17
|
+
function Chip({ label, on }: { label: string; on: boolean }) {
|
|
21
18
|
return (
|
|
22
|
-
<span
|
|
23
|
-
title={ok ? `${label} disponível` : `${label} ausente`}
|
|
24
|
-
className={cn(
|
|
25
|
-
"rounded-full border px-2 py-0.5 text-[11px]",
|
|
26
|
-
ok ? "border-emerald-500/40 bg-emerald-500/15 text-emerald-400" : "border-zinc-700 bg-zinc-800 text-zinc-500",
|
|
27
|
-
)}
|
|
28
|
-
>
|
|
19
|
+
<span className={cn("hub-chip", on ? "hub-chip-on" : "hub-chip-off")} title={on ? `${label} no PATH` : `${label} ausente`}>
|
|
29
20
|
{label}
|
|
30
21
|
</span>
|
|
31
22
|
);
|
|
@@ -66,11 +57,15 @@ export function Sidebar({
|
|
|
66
57
|
}) {
|
|
67
58
|
const selected = sessions.find((session) => session.id === selectedSessionId);
|
|
68
59
|
const fallbackAgentId = selected?.agentId ?? agents[0]?.id;
|
|
60
|
+
const mocked = clisMissing(harness);
|
|
69
61
|
|
|
70
62
|
return (
|
|
71
|
-
<div className="flex h-full w-full flex-col
|
|
63
|
+
<div className="hub-sidebar flex h-full w-full flex-col text-zinc-100">
|
|
72
64
|
<div className="flex shrink-0 flex-col gap-3 p-3">
|
|
73
|
-
<div className="
|
|
65
|
+
<div className="hub-brand">
|
|
66
|
+
<span className="hub-mark" aria-hidden />
|
|
67
|
+
Agent Hub
|
|
68
|
+
</div>
|
|
74
69
|
<label className="grid gap-1 text-xs text-zinc-400" htmlFor={`${idPrefix}-workspace`}>
|
|
75
70
|
Workspace
|
|
76
71
|
<WorkspaceField
|
|
@@ -82,14 +77,29 @@ export function Sidebar({
|
|
|
82
77
|
/>
|
|
83
78
|
</label>
|
|
84
79
|
{harness ? (
|
|
85
|
-
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
80
|
+
mocked ? (
|
|
81
|
+
<p className="hub-status hub-status-mock">
|
|
82
|
+
Simulador ativo. Cursor e Claude Code não estão no PATH — as respostas são fictícias e o disco não é
|
|
83
|
+
alterado.
|
|
84
|
+
</p>
|
|
85
|
+
) : (
|
|
86
|
+
<div className="grid gap-1.5">
|
|
87
|
+
<p className="hub-status hub-status-live">CLI da assinatura encontrada. O Hub não usa chave de API.</p>
|
|
88
|
+
<div className="flex flex-wrap gap-1.5">
|
|
89
|
+
<Chip label="Cursor" on={harness.cursor} />
|
|
90
|
+
<Chip label="Claude" on={harness.claude} />
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
)
|
|
90
94
|
) : (
|
|
91
95
|
<p className="text-xs text-zinc-500">Verificando CLIs…</p>
|
|
92
96
|
)}
|
|
97
|
+
{harness && mocked && (
|
|
98
|
+
<div className="flex flex-wrap gap-1.5">
|
|
99
|
+
<Chip label="Cursor" on={false} />
|
|
100
|
+
<Chip label="Claude" on={false} />
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
93
103
|
<div className="grid grid-cols-2 gap-2">
|
|
94
104
|
<Button
|
|
95
105
|
type="button"
|
|
@@ -99,9 +109,11 @@ export function Sidebar({
|
|
|
99
109
|
if (fallbackAgentId) onNewSession(fallbackAgentId);
|
|
100
110
|
}}
|
|
101
111
|
>
|
|
112
|
+
<MessageSquarePlus />
|
|
102
113
|
Nova conversa
|
|
103
114
|
</Button>
|
|
104
115
|
<Button type="button" size="sm" variant="outline" onClick={onNewAgent}>
|
|
116
|
+
<Plus />
|
|
105
117
|
Novo agente
|
|
106
118
|
</Button>
|
|
107
119
|
</div>
|
|
@@ -123,31 +135,22 @@ export function Sidebar({
|
|
|
123
135
|
{agents.map((agent) => {
|
|
124
136
|
const rows = sessionsForAgent(sessions, agent.id);
|
|
125
137
|
return (
|
|
126
|
-
<AccordionItem key={agent.id} value={agent.id} className="border-zinc-800">
|
|
138
|
+
<AccordionItem key={agent.id} value={agent.id} className="hub-agent border-zinc-800">
|
|
127
139
|
<AccordionTrigger className="px-2 hover:no-underline">
|
|
128
|
-
<span className="
|
|
129
|
-
<span className="
|
|
140
|
+
<span className="hub-agent-title">
|
|
141
|
+
<span className="hub-dot" style={{ backgroundColor: agent.color }} />
|
|
130
142
|
<span className="truncate">{agent.name}</span>
|
|
131
|
-
<
|
|
132
|
-
{STAGE_LABEL[agent.stage]}
|
|
133
|
-
</Badge>
|
|
143
|
+
<span className="hub-stage">{STAGE_LABEL[agent.stage]}</span>
|
|
134
144
|
</span>
|
|
135
145
|
</AccordionTrigger>
|
|
136
146
|
<AccordionContent className="pb-2">
|
|
137
147
|
<div className="grid gap-1">
|
|
138
148
|
<div className="flex items-center justify-between px-2">
|
|
139
|
-
<button
|
|
140
|
-
type="button"
|
|
141
|
-
className="text-xs text-zinc-400 hover:text-zinc-100"
|
|
142
|
-
onClick={() => onNewSession(agent.id)}
|
|
143
|
-
>
|
|
149
|
+
<button type="button" className="hub-quiet-btn" onClick={() => onNewSession(agent.id)}>
|
|
144
150
|
Nova conversa
|
|
145
151
|
</button>
|
|
146
|
-
<button
|
|
147
|
-
|
|
148
|
-
className="text-xs text-zinc-400 hover:text-zinc-100"
|
|
149
|
-
onClick={() => onEditAgent(agent)}
|
|
150
|
-
>
|
|
152
|
+
<button type="button" className="hub-quiet-btn" onClick={() => onEditAgent(agent)}>
|
|
153
|
+
<Pencil className="size-3" />
|
|
151
154
|
Editar
|
|
152
155
|
</button>
|
|
153
156
|
</div>
|
|
@@ -161,15 +164,15 @@ export function Sidebar({
|
|
|
161
164
|
title={session.title}
|
|
162
165
|
aria-current={session.id === selectedSessionId ? "true" : undefined}
|
|
163
166
|
className={cn(
|
|
164
|
-
"
|
|
165
|
-
session.id === selectedSessionId
|
|
167
|
+
"hub-session",
|
|
168
|
+
session.id === selectedSessionId && "hub-session-active",
|
|
166
169
|
)}
|
|
167
170
|
style={{ paddingLeft: 8 + depth * 12 }}
|
|
168
171
|
onClick={() => onSelectSession(session.id)}
|
|
169
172
|
>
|
|
170
|
-
{session.title}
|
|
171
|
-
{session.status === "running"
|
|
172
|
-
{session.status === "error"
|
|
173
|
+
<span className="truncate">{session.title}</span>
|
|
174
|
+
{session.status === "running" && <span className="hub-session-tag">em curso</span>}
|
|
175
|
+
{session.status === "error" && <span className="hub-session-tag hub-session-tag-err">erro</span>}
|
|
173
176
|
</button>
|
|
174
177
|
))
|
|
175
178
|
)}
|
|
@@ -184,7 +187,8 @@ export function Sidebar({
|
|
|
184
187
|
</ScrollArea>
|
|
185
188
|
</div>
|
|
186
189
|
<Separator className="shrink-0" />
|
|
187
|
-
<Link href="/gastos" className="
|
|
190
|
+
<Link href="/gastos" className="hub-footer-link">
|
|
191
|
+
<Wallet className="size-3.5" />
|
|
188
192
|
Gastos
|
|
189
193
|
</Link>
|
|
190
194
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Link from "next/link";
|
|
2
|
+
import { ArrowLeft } from "lucide-react";
|
|
2
3
|
import { HARNESS_LABEL } from "@/lib/labels";
|
|
3
4
|
import { usageDay, type SpendSummary } from "@/lib/spend";
|
|
4
5
|
import type { Agent, UsageEvent } from "@/lib/types";
|
|
@@ -59,7 +60,7 @@ function Table({
|
|
|
59
60
|
rows: string[][];
|
|
60
61
|
}) {
|
|
61
62
|
return (
|
|
62
|
-
<div className="
|
|
63
|
+
<div className="hub-table-wrap">
|
|
63
64
|
<table className="w-full border-collapse text-left text-sm">
|
|
64
65
|
<caption className="mb-2 text-left font-medium text-zinc-100">{caption}</caption>
|
|
65
66
|
<thead>
|
|
@@ -133,32 +134,34 @@ export function SpendView({
|
|
|
133
134
|
const columns = ["Grupo", "Execuções", "Duração", ...tokenHeaders, "USD"];
|
|
134
135
|
|
|
135
136
|
return (
|
|
136
|
-
<main className="
|
|
137
|
+
<main className="hub-spend">
|
|
137
138
|
<div className="flex flex-wrap items-end justify-between gap-3">
|
|
138
139
|
<div className="grid gap-1">
|
|
139
|
-
<
|
|
140
|
+
<p className="hub-kicker">Uso</p>
|
|
141
|
+
<h1>Gastos</h1>
|
|
140
142
|
<p className="max-w-2xl text-sm leading-6 text-zinc-400">{DISCLAIMER}</p>
|
|
141
143
|
</div>
|
|
142
|
-
<Link href="/" className="
|
|
144
|
+
<Link href="/" className="hub-quiet-btn">
|
|
145
|
+
<ArrowLeft className="size-3.5" />
|
|
143
146
|
Voltar ao hub
|
|
144
147
|
</Link>
|
|
145
148
|
</div>
|
|
146
|
-
<section className="
|
|
147
|
-
<div className="
|
|
148
|
-
<p
|
|
149
|
-
<
|
|
149
|
+
<section className="hub-stat-grid">
|
|
150
|
+
<div className="hub-stat">
|
|
151
|
+
<p>Total USD</p>
|
|
152
|
+
<strong>{summary.hasAnyCost ? formatUsd(summary.totalCostUsd) : "—"}</strong>
|
|
150
153
|
</div>
|
|
151
|
-
<div className="
|
|
152
|
-
<p
|
|
153
|
-
<
|
|
154
|
+
<div className="hub-stat">
|
|
155
|
+
<p>Execuções</p>
|
|
156
|
+
<strong>{events.length}</strong>
|
|
154
157
|
</div>
|
|
155
|
-
<div className="
|
|
156
|
-
<p
|
|
157
|
-
<
|
|
158
|
+
<div className="hub-stat">
|
|
159
|
+
<p>Duração</p>
|
|
160
|
+
<strong>{formatDuration(sumDuration(events))}</strong>
|
|
158
161
|
</div>
|
|
159
162
|
</section>
|
|
160
163
|
{events.length === 0 ? (
|
|
161
|
-
<p className="text-sm text-zinc-400">Nenhuma execução registrada.</p>
|
|
164
|
+
<p className="text-sm text-zinc-400">Nenhuma execução registrada. Os turnos do Simulador entram aqui com custo zero.</p>
|
|
162
165
|
) : (
|
|
163
166
|
<div className="grid gap-6">
|
|
164
167
|
<Table caption="Por agente" columns={["Agente", ...columns.slice(1)]} rows={agentRows} />
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { clisMissing, isHarnessSimulated, liveHarnessLabel, simulatedBanner } from "./labels";
|
|
3
|
+
|
|
4
|
+
describe("harness labels", () => {
|
|
5
|
+
it("treats missing CLIs as the simulator", () => {
|
|
6
|
+
expect(clisMissing(null)).toBe(false);
|
|
7
|
+
expect(clisMissing({ cursor: false, claude: false })).toBe(true);
|
|
8
|
+
expect(clisMissing({ cursor: true, claude: false })).toBe(false);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("simulates a cursor agent when the Cursor CLI is absent", () => {
|
|
12
|
+
expect(isHarnessSimulated("cursor", { cursor: false, claude: true })).toBe(true);
|
|
13
|
+
expect(isHarnessSimulated("cursor", { cursor: true, claude: false })).toBe(false);
|
|
14
|
+
expect(isHarnessSimulated("mock", { cursor: true, claude: true })).toBe(true);
|
|
15
|
+
expect(liveHarnessLabel("cursor", { cursor: false, claude: false })).toBe("Simulador · Cursor");
|
|
16
|
+
expect(liveHarnessLabel("cursor", { cursor: true, claude: false })).toBe("Cursor");
|
|
17
|
+
expect(liveHarnessLabel("mock", { cursor: true, claude: true })).toBe("Simulador");
|
|
18
|
+
expect(simulatedBanner("cursor")).toMatch(/Cursor/);
|
|
19
|
+
expect(simulatedBanner("mock")).toMatch(/Simulador/);
|
|
20
|
+
});
|
|
21
|
+
});
|
package/src/lib/labels.ts
CHANGED
|
@@ -18,3 +18,32 @@ export const HARNESS_LABEL: Record<HarnessId, string> = {
|
|
|
18
18
|
|
|
19
19
|
export const STAGES: SdlcStage[] = ["spec", "architect", "implement", "review", "test", "release", "custom"];
|
|
20
20
|
export const HARNESSES: HarnessId[] = ["cursor", "claude", "mock"];
|
|
21
|
+
|
|
22
|
+
export type HarnessAvailability = {
|
|
23
|
+
cursor: boolean;
|
|
24
|
+
claude: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function clisMissing(avail: HarnessAvailability | null): boolean {
|
|
28
|
+
return avail !== null && !avail.cursor && !avail.claude;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isHarnessSimulated(harness: HarnessId, avail: HarnessAvailability | null): boolean {
|
|
32
|
+
if (harness === "mock") return true;
|
|
33
|
+
if (!avail) return false;
|
|
34
|
+
if (harness === "cursor") return !avail.cursor;
|
|
35
|
+
return !avail.claude;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function liveHarnessLabel(harness: HarnessId, avail: HarnessAvailability | null): string {
|
|
39
|
+
if (!isHarnessSimulated(harness, avail)) return HARNESS_LABEL[harness];
|
|
40
|
+
if (harness === "mock") return "Simulador";
|
|
41
|
+
return `Simulador · ${HARNESS_LABEL[harness]}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function simulatedBanner(harness: HarnessId): string {
|
|
45
|
+
if (harness === "mock") {
|
|
46
|
+
return "Este agente está no Simulador. As respostas são fictícias e o projeto no disco não é editado.";
|
|
47
|
+
}
|
|
48
|
+
return `A CLI do ${HARNESS_LABEL[harness]} não está no PATH. Este turno usa o Simulador — o projeto no disco não será editado.`;
|
|
49
|
+
}
|