@robota-sdk/agent-ui-web 0.0.0-stage → 3.0.0-beta.93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +400 -0
  2. package/LICENSE +661 -0
  3. package/README.md +19 -2
  4. package/dist/browser/index.d.ts +715 -0
  5. package/dist/browser/index.d.ts.map +1 -0
  6. package/dist/browser/index.js +7923 -0
  7. package/dist/browser/index.js.map +1 -0
  8. package/dist/node/index.cjs +7973 -0
  9. package/dist/node/index.d.cts +715 -0
  10. package/dist/node/index.d.cts.map +1 -0
  11. package/dist/node/index.d.ts +715 -0
  12. package/dist/node/index.d.ts.map +1 -0
  13. package/dist/node/index.js +7923 -0
  14. package/dist/node/index.js.map +1 -0
  15. package/docs/README.md +62 -0
  16. package/docs/SPEC.md +124 -0
  17. package/package.json +99 -3
  18. package/src/client/runtime-host.ts +84 -0
  19. package/src/client/ws-session-client.ts +131 -0
  20. package/src/clipboard.ts +81 -0
  21. package/src/components/AgentActivityPanel.tsx +371 -0
  22. package/src/components/AgentSwitcherSheet.tsx +93 -0
  23. package/src/components/Brand.tsx +33 -0
  24. package/src/components/Composer.tsx +685 -0
  25. package/src/components/ConversationView.tsx +772 -0
  26. package/src/components/Dialog.tsx +230 -0
  27. package/src/components/DiffLines.tsx +81 -0
  28. package/src/components/ExecutionDetailSheet.tsx +142 -0
  29. package/src/components/HelpSheet.tsx +104 -0
  30. package/src/components/PermissionPrompt.tsx +661 -0
  31. package/src/components/PersonalUsageDashboard.tsx +39 -0
  32. package/src/components/PersonalUsageDashboardDetails.tsx +368 -0
  33. package/src/components/PersonalUsageDashboardSections.tsx +250 -0
  34. package/src/components/PopupMenu.tsx +166 -0
  35. package/src/components/ProjectPanel.tsx +268 -0
  36. package/src/components/SessionMonitor.tsx +180 -0
  37. package/src/components/SessionSidebar.tsx +429 -0
  38. package/src/components/SessionSurface.tsx +455 -0
  39. package/src/components/SessionSurfaceChrome.tsx +316 -0
  40. package/src/components/SettingsFields.tsx +159 -0
  41. package/src/components/SettingsGeneralSection.tsx +49 -0
  42. package/src/components/SettingsMcpSection.tsx +157 -0
  43. package/src/components/SettingsPermissionsSection.tsx +137 -0
  44. package/src/components/SettingsPluginsSection.tsx +165 -0
  45. package/src/components/SettingsProvidersSection.tsx +211 -0
  46. package/src/components/SettingsScreen.tsx +340 -0
  47. package/src/components/StatusControls.tsx +294 -0
  48. package/src/components/composer-attachments.ts +246 -0
  49. package/src/components/personal-usage-dashboard-types.ts +26 -0
  50. package/src/driver-labels.ts +88 -0
  51. package/src/execution-entry-status.ts +55 -0
  52. package/src/goal-view.ts +19 -0
  53. package/src/hooks/command-menu.ts +65 -0
  54. package/src/hooks/excluded-commands.ts +52 -0
  55. package/src/hooks/prompt-state.ts +83 -0
  56. package/src/hooks/server-message-handling.ts +92 -0
  57. package/src/hooks/session-client-types.ts +329 -0
  58. package/src/hooks/ui-intent-state.ts +74 -0
  59. package/src/hooks/use-agent-switcher-state.ts +125 -0
  60. package/src/hooks/use-execution-detail.ts +118 -0
  61. package/src/hooks/use-media-query.ts +27 -0
  62. package/src/hooks/use-model-list.ts +45 -0
  63. package/src/hooks/use-personal-usage.ts +138 -0
  64. package/src/hooks/use-project-panel.ts +117 -0
  65. package/src/hooks/use-schedules-state.ts +92 -0
  66. package/src/hooks/use-session-directory.ts +202 -0
  67. package/src/hooks/use-settings-state.ts +115 -0
  68. package/src/hooks/useSessionClient.ts +991 -0
  69. package/src/index.ts +66 -0
  70. package/src/product-identity.tsx +26 -0
  71. package/src/schedule-view.ts +92 -0
  72. package/src/styles/surface.css +231 -0
  73. package/src/styles/surface.css.d.mts +1 -0
  74. package/src/styles/theme.css +38 -0
  75. 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
+ }