@robota-sdk/agent-ui-web 0.0.0-stage → 3.0.0-beta.94
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 +409 -0
- package/LICENSE +661 -0
- package/README.md +19 -2
- package/dist/browser/index.d.ts +715 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +7923 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/node/index.cjs +7973 -0
- package/dist/node/index.d.cts +715 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +715 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +7923 -0
- package/dist/node/index.js.map +1 -0
- package/docs/README.md +62 -0
- package/docs/SPEC.md +124 -0
- package/package.json +99 -3
- package/src/client/runtime-host.ts +84 -0
- package/src/client/ws-session-client.ts +131 -0
- package/src/clipboard.ts +81 -0
- package/src/components/AgentActivityPanel.tsx +371 -0
- package/src/components/AgentSwitcherSheet.tsx +93 -0
- package/src/components/Brand.tsx +33 -0
- package/src/components/Composer.tsx +685 -0
- package/src/components/ConversationView.tsx +772 -0
- package/src/components/Dialog.tsx +230 -0
- package/src/components/DiffLines.tsx +81 -0
- package/src/components/ExecutionDetailSheet.tsx +142 -0
- package/src/components/HelpSheet.tsx +104 -0
- package/src/components/PermissionPrompt.tsx +661 -0
- package/src/components/PersonalUsageDashboard.tsx +39 -0
- package/src/components/PersonalUsageDashboardDetails.tsx +368 -0
- package/src/components/PersonalUsageDashboardSections.tsx +250 -0
- package/src/components/PopupMenu.tsx +166 -0
- package/src/components/ProjectPanel.tsx +268 -0
- package/src/components/SessionMonitor.tsx +180 -0
- package/src/components/SessionSidebar.tsx +429 -0
- package/src/components/SessionSurface.tsx +455 -0
- package/src/components/SessionSurfaceChrome.tsx +316 -0
- package/src/components/SettingsFields.tsx +159 -0
- package/src/components/SettingsGeneralSection.tsx +49 -0
- package/src/components/SettingsMcpSection.tsx +157 -0
- package/src/components/SettingsPermissionsSection.tsx +137 -0
- package/src/components/SettingsPluginsSection.tsx +165 -0
- package/src/components/SettingsProvidersSection.tsx +211 -0
- package/src/components/SettingsScreen.tsx +340 -0
- package/src/components/StatusControls.tsx +294 -0
- package/src/components/composer-attachments.ts +246 -0
- package/src/components/personal-usage-dashboard-types.ts +26 -0
- package/src/driver-labels.ts +88 -0
- package/src/execution-entry-status.ts +55 -0
- package/src/goal-view.ts +19 -0
- package/src/hooks/command-menu.ts +65 -0
- package/src/hooks/excluded-commands.ts +52 -0
- package/src/hooks/prompt-state.ts +83 -0
- package/src/hooks/server-message-handling.ts +92 -0
- package/src/hooks/session-client-types.ts +329 -0
- package/src/hooks/ui-intent-state.ts +74 -0
- package/src/hooks/use-agent-switcher-state.ts +125 -0
- package/src/hooks/use-execution-detail.ts +118 -0
- package/src/hooks/use-media-query.ts +27 -0
- package/src/hooks/use-model-list.ts +45 -0
- package/src/hooks/use-personal-usage.ts +138 -0
- package/src/hooks/use-project-panel.ts +117 -0
- package/src/hooks/use-schedules-state.ts +92 -0
- package/src/hooks/use-session-directory.ts +202 -0
- package/src/hooks/use-settings-state.ts +115 -0
- package/src/hooks/useSessionClient.ts +991 -0
- package/src/index.ts +66 -0
- package/src/product-identity.tsx +26 -0
- package/src/schedule-view.ts +92 -0
- package/src/styles/surface.css +231 -0
- package/src/styles/surface.css.d.mts +1 -0
- package/src/styles/theme.css +38 -0
- package/src/styles/theme.css.d.mts +1 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CircleCheck,
|
|
3
|
+
CircleDashed,
|
|
4
|
+
CircleX,
|
|
5
|
+
LoaderCircle,
|
|
6
|
+
Pause,
|
|
7
|
+
Play,
|
|
8
|
+
ShieldAlert,
|
|
9
|
+
Square,
|
|
10
|
+
Target,
|
|
11
|
+
} from 'lucide-react';
|
|
12
|
+
import React, { useState } from 'react';
|
|
13
|
+
|
|
14
|
+
import { ConfirmDialog } from './Dialog.js';
|
|
15
|
+
import { describeGoalStatus } from '../goal-view.js';
|
|
16
|
+
import {
|
|
17
|
+
describeScheduleInstruction,
|
|
18
|
+
describeScheduleStatus,
|
|
19
|
+
describeScheduleTiming,
|
|
20
|
+
} from '../schedule-view.js';
|
|
21
|
+
|
|
22
|
+
import { describeExecutionStatus, hadDeniedToolCalls } from '../execution-entry-status.js';
|
|
23
|
+
|
|
24
|
+
import type {
|
|
25
|
+
IBackgroundTaskState,
|
|
26
|
+
IExecutionWorkspaceEntry,
|
|
27
|
+
TExecutionAttention,
|
|
28
|
+
TExecutionWorkspaceStatus,
|
|
29
|
+
} from '@robota-sdk/agent-interface-execution';
|
|
30
|
+
import type { IGoalState } from '@robota-sdk/agent-interface-session';
|
|
31
|
+
|
|
32
|
+
interface IAgentActivityPanelProps {
|
|
33
|
+
tasks: readonly IExecutionWorkspaceEntry[];
|
|
34
|
+
/** #3282 §4 part b-3: the "Scheduled" group — every `kind: 'scheduled'` background task. */
|
|
35
|
+
schedules?: readonly IBackgroundTaskState<'scheduled'>[];
|
|
36
|
+
/** #3282 §4 part b-3: the current goal (`/goal`), or null/absent when none is set. */
|
|
37
|
+
goal?: IGoalState | null;
|
|
38
|
+
onPauseSchedule?: (taskId: string) => void;
|
|
39
|
+
onResumeSchedule?: (taskId: string) => void;
|
|
40
|
+
onDeleteSchedule?: (taskId: string) => void;
|
|
41
|
+
onCancelGoal?: () => void;
|
|
42
|
+
className?: string;
|
|
43
|
+
/** #3288 §1: open a non-main-thread entry's detail sheet. */
|
|
44
|
+
onSelect?: (entry: IExecutionWorkspaceEntry) => void;
|
|
45
|
+
/** #3288 §1: the main thread entry was clicked — the conversation beside this panel is it. */
|
|
46
|
+
onReturnToConversation?: () => void;
|
|
47
|
+
/** #3288 §1: Stop was clicked on a running/queued entry. */
|
|
48
|
+
onStop?: (entry: IExecutionWorkspaceEntry) => void;
|
|
49
|
+
/** #3288 §1: the entry whose detail sheet is currently open, for a highlighted row. */
|
|
50
|
+
selectedEntryId?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function AgentActivityPanel({
|
|
54
|
+
tasks,
|
|
55
|
+
schedules = [],
|
|
56
|
+
goal = null,
|
|
57
|
+
onPauseSchedule,
|
|
58
|
+
onResumeSchedule,
|
|
59
|
+
onDeleteSchedule,
|
|
60
|
+
onCancelGoal,
|
|
61
|
+
className,
|
|
62
|
+
onSelect,
|
|
63
|
+
onReturnToConversation,
|
|
64
|
+
onStop,
|
|
65
|
+
selectedEntryId,
|
|
66
|
+
}: IAgentActivityPanelProps): React.ReactElement {
|
|
67
|
+
const runningCount = tasks.filter((t) => t.status === 'running').length;
|
|
68
|
+
// A deleted schedule is `cancelled` forever (the manager never removes the record) — the group
|
|
69
|
+
// shows only what is still relevant, exactly like Delete disappearing the row it was pressed on.
|
|
70
|
+
const visibleSchedules = schedules.filter((task) => task.status !== 'cancelled');
|
|
71
|
+
const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null);
|
|
72
|
+
const pendingDelete = visibleSchedules.find((task) => task.id === pendingDeleteId) ?? null;
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div className={`agent-ui flex flex-col overflow-hidden ${className ?? ''}`}>
|
|
76
|
+
<div className="flex h-12 flex-shrink-0 items-center gap-2 px-4">
|
|
77
|
+
<span className="text-[14px] font-medium text-foreground">Agents</span>
|
|
78
|
+
{runningCount > 0 && (
|
|
79
|
+
<span className="ml-auto text-[13px] tabular-nums text-subtle">
|
|
80
|
+
{runningCount} running
|
|
81
|
+
</span>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div className="flex-1 space-y-4 overflow-y-auto px-3 pb-3">
|
|
86
|
+
{tasks.length > 0 && (
|
|
87
|
+
<div className="space-y-2">
|
|
88
|
+
{tasks.map((entry) => (
|
|
89
|
+
<AgentCard
|
|
90
|
+
key={entry.id}
|
|
91
|
+
entry={entry}
|
|
92
|
+
selected={entry.id === selectedEntryId}
|
|
93
|
+
onOpen={
|
|
94
|
+
entry.kind === 'main_thread'
|
|
95
|
+
? onReturnToConversation
|
|
96
|
+
: onSelect && (() => onSelect(entry))
|
|
97
|
+
}
|
|
98
|
+
onStop={
|
|
99
|
+
onStop && entry.controls?.includes('cancel') ? () => onStop(entry) : undefined
|
|
100
|
+
}
|
|
101
|
+
/>
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
{visibleSchedules.length > 0 && (
|
|
107
|
+
<div>
|
|
108
|
+
<p className="px-0.5 pb-1.5 text-[12px] font-medium uppercase tracking-wide text-subtle">
|
|
109
|
+
Scheduled
|
|
110
|
+
</p>
|
|
111
|
+
<div className="space-y-2">
|
|
112
|
+
{visibleSchedules.map((task) => (
|
|
113
|
+
<ScheduleCard
|
|
114
|
+
key={task.id}
|
|
115
|
+
task={task}
|
|
116
|
+
onPause={onPauseSchedule}
|
|
117
|
+
onResume={onResumeSchedule}
|
|
118
|
+
onRequestDelete={() => setPendingDeleteId(task.id)}
|
|
119
|
+
/>
|
|
120
|
+
))}
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
{goal && (
|
|
126
|
+
<div>
|
|
127
|
+
<p className="px-0.5 pb-1.5 text-[12px] font-medium uppercase tracking-wide text-subtle">
|
|
128
|
+
Goal
|
|
129
|
+
</p>
|
|
130
|
+
<GoalCard goal={goal} onCancel={onCancelGoal} />
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<ConfirmDialog
|
|
136
|
+
open={pendingDelete !== null}
|
|
137
|
+
title="Delete this schedule?"
|
|
138
|
+
body={
|
|
139
|
+
pendingDelete
|
|
140
|
+
? `"${describeScheduleInstruction(pendingDelete)}" will stop running. This cannot be undone.`
|
|
141
|
+
: ''
|
|
142
|
+
}
|
|
143
|
+
confirmLabel="Delete"
|
|
144
|
+
destructive
|
|
145
|
+
onConfirm={() => {
|
|
146
|
+
if (pendingDelete) onDeleteSchedule?.(pendingDelete.id);
|
|
147
|
+
setPendingDeleteId(null);
|
|
148
|
+
}}
|
|
149
|
+
onCancel={() => setPendingDeleteId(null)}
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function AgentCard({
|
|
156
|
+
entry,
|
|
157
|
+
selected,
|
|
158
|
+
onOpen,
|
|
159
|
+
onStop,
|
|
160
|
+
}: {
|
|
161
|
+
entry: IExecutionWorkspaceEntry;
|
|
162
|
+
selected: boolean;
|
|
163
|
+
onOpen?: () => void;
|
|
164
|
+
onStop?: () => void;
|
|
165
|
+
}): React.ReactElement {
|
|
166
|
+
const needsYou = entry.attention === 'permission' || entry.status === 'waiting_permission';
|
|
167
|
+
const failed = entry.attention === 'failed' || entry.status === 'failed';
|
|
168
|
+
return (
|
|
169
|
+
<div
|
|
170
|
+
role={onOpen ? 'button' : undefined}
|
|
171
|
+
tabIndex={onOpen ? 0 : undefined}
|
|
172
|
+
onClick={onOpen}
|
|
173
|
+
onKeyDown={
|
|
174
|
+
onOpen
|
|
175
|
+
? (event) => {
|
|
176
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
177
|
+
event.preventDefault();
|
|
178
|
+
onOpen();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
: undefined
|
|
182
|
+
}
|
|
183
|
+
aria-label={onOpen ? entry.title : undefined}
|
|
184
|
+
className={`rounded-xl px-3.5 py-3 transition-opacity duration-500 ${
|
|
185
|
+
needsYou ? 'bg-warning/10' : failed ? 'bg-destructive/10' : 'bg-card'
|
|
186
|
+
} ${entry.status === 'completed' ? 'opacity-60' : ''} ${
|
|
187
|
+
selected ? 'ring-1 ring-inset ring-accent' : ''
|
|
188
|
+
} ${onOpen ? 'cursor-pointer' : ''}`}
|
|
189
|
+
>
|
|
190
|
+
<div className="flex items-center gap-2.5">
|
|
191
|
+
<StatusIcon status={entry.status} attention={entry.attention} deniedToolCalls={entry.deniedToolCalls} />
|
|
192
|
+
<span className="min-w-0 flex-1 truncate text-[14px] font-medium text-foreground">
|
|
193
|
+
{entry.title}
|
|
194
|
+
</span>
|
|
195
|
+
<AttentionTag status={entry.status} attention={entry.attention} deniedToolCalls={entry.deniedToolCalls} />
|
|
196
|
+
{onStop && (
|
|
197
|
+
<button
|
|
198
|
+
type="button"
|
|
199
|
+
aria-label={`Stop ${entry.title}`}
|
|
200
|
+
onClick={(event) => {
|
|
201
|
+
event.stopPropagation();
|
|
202
|
+
onStop();
|
|
203
|
+
}}
|
|
204
|
+
className="flex-shrink-0 rounded-md px-1.5 py-px text-[12px] text-subtle hover:bg-destructive/10 hover:text-destructive"
|
|
205
|
+
>
|
|
206
|
+
Stop
|
|
207
|
+
</button>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
{entry.currentAction && (
|
|
211
|
+
<p className="mt-1.5 truncate pl-[26px] text-[13px] leading-snug text-muted-foreground">
|
|
212
|
+
{entry.currentAction}
|
|
213
|
+
</p>
|
|
214
|
+
)}
|
|
215
|
+
{entry.preview && (
|
|
216
|
+
<p className="mt-1 truncate pl-[26px] font-mono text-[12px] leading-snug text-subtle">
|
|
217
|
+
{entry.preview}
|
|
218
|
+
</p>
|
|
219
|
+
)}
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** #3282 §4 part b-3: one row of the "Scheduled" group — what it runs, when next, status, controls. */
|
|
225
|
+
function ScheduleCard({
|
|
226
|
+
task,
|
|
227
|
+
onPause,
|
|
228
|
+
onResume,
|
|
229
|
+
onRequestDelete,
|
|
230
|
+
}: {
|
|
231
|
+
task: IBackgroundTaskState<'scheduled'>;
|
|
232
|
+
onPause?: (taskId: string) => void;
|
|
233
|
+
onResume?: (taskId: string) => void;
|
|
234
|
+
onRequestDelete: () => void;
|
|
235
|
+
}): React.ReactElement {
|
|
236
|
+
const isPaused = task.status === 'paused';
|
|
237
|
+
return (
|
|
238
|
+
<div className="rounded-xl bg-card px-3.5 py-3">
|
|
239
|
+
<p className="truncate text-[14px] font-medium text-foreground">
|
|
240
|
+
{describeScheduleInstruction(task)}
|
|
241
|
+
</p>
|
|
242
|
+
<p className="mt-1 text-[13px] text-muted-foreground">
|
|
243
|
+
{describeScheduleTiming(task)} · {describeScheduleStatus(task)}
|
|
244
|
+
</p>
|
|
245
|
+
<div className="mt-2 flex items-center gap-3">
|
|
246
|
+
{isPaused ? (
|
|
247
|
+
<button
|
|
248
|
+
type="button"
|
|
249
|
+
onClick={() => onResume?.(task.id)}
|
|
250
|
+
className="flex items-center gap-1.5 rounded-lg px-2 py-1 text-[13px] text-muted-foreground hover:bg-hover hover:text-foreground"
|
|
251
|
+
>
|
|
252
|
+
<Play size={11} fill="currentColor" aria-hidden="true" />
|
|
253
|
+
Resume
|
|
254
|
+
</button>
|
|
255
|
+
) : (
|
|
256
|
+
<button
|
|
257
|
+
type="button"
|
|
258
|
+
onClick={() => onPause?.(task.id)}
|
|
259
|
+
className="flex items-center gap-1.5 rounded-lg px-2 py-1 text-[13px] text-muted-foreground hover:bg-hover hover:text-foreground"
|
|
260
|
+
>
|
|
261
|
+
<Pause size={11} fill="currentColor" aria-hidden="true" />
|
|
262
|
+
Pause
|
|
263
|
+
</button>
|
|
264
|
+
)}
|
|
265
|
+
<button
|
|
266
|
+
type="button"
|
|
267
|
+
onClick={onRequestDelete}
|
|
268
|
+
className="flex items-center gap-1.5 rounded-lg px-2 py-1 text-[13px] text-destructive hover:bg-destructive/10"
|
|
269
|
+
>
|
|
270
|
+
Delete…
|
|
271
|
+
</button>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** #3282 §4 part b-3: the current goal — objective, plain-word status, and Cancel. */
|
|
278
|
+
function GoalCard({
|
|
279
|
+
goal,
|
|
280
|
+
onCancel,
|
|
281
|
+
}: {
|
|
282
|
+
goal: IGoalState;
|
|
283
|
+
onCancel?: () => void;
|
|
284
|
+
}): React.ReactElement {
|
|
285
|
+
return (
|
|
286
|
+
<div className="rounded-xl bg-card px-3.5 py-3">
|
|
287
|
+
<div className="flex items-center gap-2.5">
|
|
288
|
+
<Target size={16} strokeWidth={1.9} className="flex-shrink-0 text-accent" />
|
|
289
|
+
<span className="min-w-0 flex-1 truncate text-[14px] font-medium text-foreground">
|
|
290
|
+
{goal.objective}
|
|
291
|
+
</span>
|
|
292
|
+
</div>
|
|
293
|
+
<p className="mt-1.5 pl-[26px] text-[13px] text-muted-foreground">
|
|
294
|
+
{describeGoalStatus(goal)}
|
|
295
|
+
</p>
|
|
296
|
+
{goal.status === 'active' && (
|
|
297
|
+
<div className="mt-2 pl-[26px]">
|
|
298
|
+
<button
|
|
299
|
+
type="button"
|
|
300
|
+
onClick={onCancel}
|
|
301
|
+
className="flex items-center gap-1.5 rounded-lg px-2 py-1 text-[13px] text-muted-foreground hover:bg-hover hover:text-foreground"
|
|
302
|
+
>
|
|
303
|
+
<Square size={11} fill="currentColor" aria-hidden="true" />
|
|
304
|
+
Cancel goal
|
|
305
|
+
</button>
|
|
306
|
+
</div>
|
|
307
|
+
)}
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function StatusIcon({
|
|
313
|
+
status,
|
|
314
|
+
attention,
|
|
315
|
+
deniedToolCalls,
|
|
316
|
+
}: {
|
|
317
|
+
status: TExecutionWorkspaceStatus;
|
|
318
|
+
attention: TExecutionAttention;
|
|
319
|
+
deniedToolCalls?: number;
|
|
320
|
+
}): React.ReactElement {
|
|
321
|
+
const props = { size: 16, strokeWidth: 1.9, className: 'flex-shrink-0' } as const;
|
|
322
|
+
if (attention === 'permission' || status === 'waiting_permission') {
|
|
323
|
+
return <ShieldAlert {...props} className={`${props.className} text-warning`} />;
|
|
324
|
+
}
|
|
325
|
+
if (attention === 'failed' || status === 'failed') {
|
|
326
|
+
return <CircleX {...props} className={`${props.className} text-destructive`} />;
|
|
327
|
+
}
|
|
328
|
+
if (hadDeniedToolCalls(status, deniedToolCalls)) {
|
|
329
|
+
return <ShieldAlert {...props} className={`${props.className} text-warning`} />;
|
|
330
|
+
}
|
|
331
|
+
if (status === 'running') {
|
|
332
|
+
return (
|
|
333
|
+
<LoaderCircle
|
|
334
|
+
{...props}
|
|
335
|
+
className={`${props.className} animate-spin text-muted-foreground`}
|
|
336
|
+
/>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
if (status === 'completed') {
|
|
340
|
+
return <CircleCheck {...props} className={`${props.className} text-success`} />;
|
|
341
|
+
}
|
|
342
|
+
if (status === 'cancelled') {
|
|
343
|
+
return <CircleX {...props} className={`${props.className} text-subtle`} />;
|
|
344
|
+
}
|
|
345
|
+
return <CircleDashed {...props} className={`${props.className} text-subtle`} />;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const STATUS_TAG_TONE: Record<string, string> = {
|
|
349
|
+
warning: 'bg-warning/15 text-warning',
|
|
350
|
+
destructive: 'bg-destructive/12 text-destructive',
|
|
351
|
+
muted: 'text-subtle',
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
function AttentionTag({
|
|
355
|
+
attention,
|
|
356
|
+
status,
|
|
357
|
+
deniedToolCalls,
|
|
358
|
+
}: {
|
|
359
|
+
attention: TExecutionAttention;
|
|
360
|
+
status: TExecutionWorkspaceStatus;
|
|
361
|
+
deniedToolCalls?: number;
|
|
362
|
+
}): React.ReactElement | null {
|
|
363
|
+
const described = describeExecutionStatus(status, attention, deniedToolCalls);
|
|
364
|
+
// A running row already has its spinner; no text tag repeats that.
|
|
365
|
+
if (!described || described.label === 'Running') return null;
|
|
366
|
+
return (
|
|
367
|
+
<span className={`flex-shrink-0 rounded-md px-1.5 py-px text-[12px] ${STATUS_TAG_TONE[described.tone]}`}>
|
|
368
|
+
{described.label}
|
|
369
|
+
</span>
|
|
370
|
+
);
|
|
371
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Check, X } from 'lucide-react';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
import { Dialog } from './Dialog.js';
|
|
7
|
+
|
|
8
|
+
import type { IWsSessionState } from '../hooks/useSessionClient.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* #3282 §4 part b-3 — the agent switcher: `show-agent-switcher` (`/agent` with no args) opens this
|
|
12
|
+
* sheet instead of the surface's old "not available" line. It lists every agent this session can
|
|
13
|
+
* run — name, one-line description, and where each is defined in plain words — with the current one
|
|
14
|
+
* checked. Choosing a row runs the SAME path as typing `/agent <name>` (a plain `command`, wired in
|
|
15
|
+
* `useAgentSwitcherState`); the reply is this sheet's own confirmation line, never a conversation
|
|
16
|
+
* card (issue #3282 §4's decided design).
|
|
17
|
+
*/
|
|
18
|
+
export function AgentSwitcherSheet({ state }: { state: IWsSessionState }): React.ReactElement | null {
|
|
19
|
+
if (!state.agentSwitcherOpen) return null;
|
|
20
|
+
|
|
21
|
+
const loading = state.agentSwitcherStatus === 'loading' && state.agentDefinitions.length === 0;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Dialog
|
|
25
|
+
open
|
|
26
|
+
onClose={() => state.closeAgentSwitcher()}
|
|
27
|
+
title="Switch agent"
|
|
28
|
+
panelClassName="flex max-h-[min(560px,85vh)] w-full max-w-md flex-col overflow-hidden rounded-2xl bg-card text-card-foreground shadow-[0_8px_30px_-12px_rgb(0_0_0/0.45)] focus:outline-none"
|
|
29
|
+
>
|
|
30
|
+
<div className="flex h-12 flex-shrink-0 items-center gap-1 border-b border-border px-3">
|
|
31
|
+
<h2 className="px-1.5 text-[15px] font-semibold text-foreground">Switch agent</h2>
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
aria-label="Close"
|
|
35
|
+
onClick={() => state.closeAgentSwitcher()}
|
|
36
|
+
className="ml-auto rounded-md p-1.5 text-muted-foreground hover:bg-hover hover:text-foreground"
|
|
37
|
+
>
|
|
38
|
+
<X size={17} strokeWidth={1.75} />
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
{state.agentSwitchMessage ? (
|
|
43
|
+
<p
|
|
44
|
+
role="status"
|
|
45
|
+
className="mx-4 mt-3 rounded-lg bg-raised px-3 py-2 text-[13px] text-foreground"
|
|
46
|
+
>
|
|
47
|
+
{state.agentSwitchMessage}
|
|
48
|
+
</p>
|
|
49
|
+
) : null}
|
|
50
|
+
|
|
51
|
+
<ul className="min-h-0 flex-1 overflow-y-auto p-2">
|
|
52
|
+
{loading ? (
|
|
53
|
+
<li className="px-3 py-4 text-[13.5px] text-muted-foreground">Loading agents…</li>
|
|
54
|
+
) : state.agentDefinitions.length === 0 ? (
|
|
55
|
+
<li className="px-3 py-4 text-[13.5px] text-muted-foreground">
|
|
56
|
+
No agents are configured on this host.
|
|
57
|
+
</li>
|
|
58
|
+
) : (
|
|
59
|
+
state.agentDefinitions.map((agent) => {
|
|
60
|
+
const isCurrent = agent.name === state.currentAgentType;
|
|
61
|
+
return (
|
|
62
|
+
<li key={agent.name}>
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
onClick={() => state.selectAgent(agent.name)}
|
|
66
|
+
aria-current={isCurrent ? 'true' : undefined}
|
|
67
|
+
className="flex w-full items-start gap-3 rounded-xl px-3 py-2.5 text-left hover:bg-hover"
|
|
68
|
+
>
|
|
69
|
+
<span className="mt-0.5 flex h-4 w-4 flex-shrink-0 items-center justify-center">
|
|
70
|
+
{isCurrent ? (
|
|
71
|
+
<Check size={16} strokeWidth={2} className="text-accent" />
|
|
72
|
+
) : null}
|
|
73
|
+
</span>
|
|
74
|
+
<span className="min-w-0 flex-1">
|
|
75
|
+
<span className="block truncate text-[14px] font-medium text-foreground">
|
|
76
|
+
{agent.name}
|
|
77
|
+
</span>
|
|
78
|
+
<span className="block truncate text-[13px] text-muted-foreground">
|
|
79
|
+
{agent.description}
|
|
80
|
+
</span>
|
|
81
|
+
<span className="block truncate text-[12px] text-subtle">
|
|
82
|
+
{agent.definedIn}
|
|
83
|
+
</span>
|
|
84
|
+
</span>
|
|
85
|
+
</button>
|
|
86
|
+
</li>
|
|
87
|
+
);
|
|
88
|
+
})
|
|
89
|
+
)}
|
|
90
|
+
</ul>
|
|
91
|
+
</Dialog>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useProductIdentity } from '../product-identity.js';
|
|
2
|
+
/** Shared mark: a rounded face with two eyes, in the accent colour. */
|
|
3
|
+
export function ProductMark({ size = 20 }: { size?: number }): React.ReactElement {
|
|
4
|
+
return (
|
|
5
|
+
<svg
|
|
6
|
+
width={size}
|
|
7
|
+
height={size}
|
|
8
|
+
viewBox="0 0 20 20"
|
|
9
|
+
aria-hidden="true"
|
|
10
|
+
className="flex-shrink-0"
|
|
11
|
+
>
|
|
12
|
+
<rect x="1" y="2.5" width="18" height="15" rx="5.5" className="fill-accent" />
|
|
13
|
+
<circle cx="7.2" cy="10" r="1.7" className="fill-accent-foreground" />
|
|
14
|
+
<circle cx="12.8" cy="10" r="1.7" className="fill-accent-foreground" />
|
|
15
|
+
</svg>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The mark and the wordmark, as the app's name in its chrome. */
|
|
20
|
+
export function ProductWordmark({ surface }: { surface?: string }): React.ReactElement {
|
|
21
|
+
const { identity } = useProductIdentity();
|
|
22
|
+
return (
|
|
23
|
+
<span className="flex items-center gap-2">
|
|
24
|
+
<ProductMark />
|
|
25
|
+
<span className="text-[15px] font-semibold tracking-[-0.01em] text-foreground">{identity.displayName}</span>
|
|
26
|
+
{surface ? (
|
|
27
|
+
<span className="rounded-md bg-raised px-1.5 py-px text-[11px] font-medium text-muted-foreground">
|
|
28
|
+
{surface}
|
|
29
|
+
</span>
|
|
30
|
+
) : null}
|
|
31
|
+
</span>
|
|
32
|
+
);
|
|
33
|
+
}
|