astryx-dracula 0.1.3 → 0.2.0

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 (94) hide show
  1. package/BRAND.md +1 -1
  2. package/README.md +2 -0
  3. package/USAGE.md +13 -0
  4. package/astryx-dracula.js +4 -4
  5. package/astryx.integration.mjs +3 -0
  6. package/package.json +4 -2
  7. package/templates/ai-chat-landing.template.mjs +6 -0
  8. package/templates/ai-chat-landing.tsx +476 -0
  9. package/templates/ai-chat.template.mjs +6 -0
  10. package/templates/ai-chat.tsx +762 -0
  11. package/templates/blank.template.mjs +6 -0
  12. package/templates/blank.tsx +16 -0
  13. package/templates/centered-hero.template.mjs +6 -0
  14. package/templates/centered-hero.tsx +188 -0
  15. package/templates/classic-gallery.template.mjs +6 -0
  16. package/templates/classic-gallery.tsx +181 -0
  17. package/templates/contact-form.template.mjs +6 -0
  18. package/templates/contact-form.tsx +292 -0
  19. package/templates/dashboard-portfolio.template.mjs +6 -0
  20. package/templates/dashboard-portfolio.tsx +800 -0
  21. package/templates/dashboard.template.mjs +6 -0
  22. package/templates/dashboard.tsx +774 -0
  23. package/templates/detail-page.template.mjs +6 -0
  24. package/templates/detail-page.tsx +695 -0
  25. package/templates/documentation-design.template.mjs +6 -0
  26. package/templates/documentation-design.tsx +751 -0
  27. package/templates/documentation-technical.template.mjs +6 -0
  28. package/templates/documentation-technical.tsx +292 -0
  29. package/templates/documentation.template.mjs +6 -0
  30. package/templates/documentation.tsx +347 -0
  31. package/templates/editor.template.mjs +6 -0
  32. package/templates/editor.tsx +959 -0
  33. package/templates/file-explorer.template.mjs +6 -0
  34. package/templates/file-explorer.tsx +577 -0
  35. package/templates/form-two-column.template.mjs +6 -0
  36. package/templates/form-two-column.tsx +314 -0
  37. package/templates/gallery-hero.template.mjs +6 -0
  38. package/templates/gallery-hero.tsx +287 -0
  39. package/templates/ide.template.mjs +6 -0
  40. package/templates/ide.tsx +533 -0
  41. package/templates/incident-console.template.mjs +6 -0
  42. package/templates/incident-console.tsx +584 -0
  43. package/templates/kanban-board.template.mjs +6 -0
  44. package/templates/kanban-board.tsx +746 -0
  45. package/templates/library.template.mjs +6 -0
  46. package/templates/library.tsx +489 -0
  47. package/templates/login-card.template.mjs +6 -0
  48. package/templates/login-card.tsx +200 -0
  49. package/templates/login-split.template.mjs +6 -0
  50. package/templates/login-split.tsx +318 -0
  51. package/templates/login-sso.template.mjs +6 -0
  52. package/templates/login-sso.tsx +277 -0
  53. package/templates/login.template.mjs +6 -0
  54. package/templates/login.tsx +98 -0
  55. package/templates/messaging-shell.template.mjs +6 -0
  56. package/templates/messaging-shell.tsx +675 -0
  57. package/templates/mixed-gallery.template.mjs +6 -0
  58. package/templates/mixed-gallery.tsx +190 -0
  59. package/templates/payment-form.template.mjs +6 -0
  60. package/templates/payment-form.tsx +990 -0
  61. package/templates/product-detail.template.mjs +6 -0
  62. package/templates/product-detail.tsx +366 -0
  63. package/templates/product-gallery.template.mjs +6 -0
  64. package/templates/product-gallery.tsx +173 -0
  65. package/templates/settings-dialog.template.mjs +6 -0
  66. package/templates/settings-dialog.tsx +920 -0
  67. package/templates/settings-sidebar.template.mjs +6 -0
  68. package/templates/settings-sidebar.tsx +952 -0
  69. package/templates/settings.template.mjs +6 -0
  70. package/templates/settings.tsx +235 -0
  71. package/templates/shell-nav.template.mjs +6 -0
  72. package/templates/shell-nav.tsx +306 -0
  73. package/templates/shell-side-nav.template.mjs +6 -0
  74. package/templates/shell-side-nav.tsx +238 -0
  75. package/templates/shell-top-nav.template.mjs +6 -0
  76. package/templates/shell-top-nav.tsx +243 -0
  77. package/templates/side-gallery.template.mjs +6 -0
  78. package/templates/side-gallery.tsx +179 -0
  79. package/templates/table-grouped.template.mjs +6 -0
  80. package/templates/table-grouped.tsx +1174 -0
  81. package/templates/table-page-chart.template.mjs +6 -0
  82. package/templates/table-page-chart.tsx +693 -0
  83. package/templates/table-page-heatmap-status.template.mjs +6 -0
  84. package/templates/table-page-heatmap-status.tsx +547 -0
  85. package/templates/table-page-shoe-store-heatmap.template.mjs +6 -0
  86. package/templates/table-page-shoe-store-heatmap.tsx +1038 -0
  87. package/templates/table-page.template.mjs +6 -0
  88. package/templates/table-page.tsx +445 -0
  89. package/templates/table.template.mjs +6 -0
  90. package/templates/table.tsx +84 -0
  91. package/templates/theme-showcase.template.mjs +6 -0
  92. package/templates/theme-showcase.tsx +1282 -0
  93. package/theme.css +4 -4
  94. package/tokens.css +4 -4
@@ -0,0 +1,762 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {useRef, useState, type CSSProperties} from 'react';
4
+
5
+ import {
6
+ HStack,
7
+ VStack,
8
+ StackItem,
9
+ Layout,
10
+ LayoutContent,
11
+ } from '@astryxdesign/core/Layout';
12
+ import {Text, Heading} from '@astryxdesign/core/Text';
13
+ import {
14
+ ChatComposer,
15
+ ChatComposerInput,
16
+ ChatLayout,
17
+ ChatMessage,
18
+ ChatMessageBubble,
19
+ ChatMessageList,
20
+ ChatMessageMetadata,
21
+ ChatSystemMessage,
22
+ ChatTokenizedText,
23
+ ChatToolCalls,
24
+ } from '@astryxdesign/core/Chat';
25
+ import {Avatar} from '@astryxdesign/core/Avatar';
26
+ import {Card} from '@astryxdesign/core/Card';
27
+ import {ClickableCard} from '@astryxdesign/core/ClickableCard';
28
+ import {Section} from '@astryxdesign/core/Section';
29
+ import {Markdown} from '@astryxdesign/core/Markdown';
30
+ import {CodeBlock} from '@astryxdesign/core/CodeBlock';
31
+ import {Timestamp} from '@astryxdesign/core/Timestamp';
32
+ import {Token} from '@astryxdesign/core/Token';
33
+ import {Button} from '@astryxdesign/core/Button';
34
+ import {Icon} from '@astryxdesign/core/Icon';
35
+ import {Dialog, DialogHeader} from '@astryxdesign/core/Dialog';
36
+ import {DropdownMenu} from '@astryxdesign/core/DropdownMenu';
37
+ import {MoreMenu} from '@astryxdesign/core/MoreMenu';
38
+ import {Toolbar} from '@astryxdesign/core/Toolbar';
39
+ import {useResizable, ResizeHandle} from '@astryxdesign/core/Resizable';
40
+
41
+ import {
42
+ FileText,
43
+ Copy,
44
+ Share2,
45
+ AtSign,
46
+ Paperclip,
47
+ X,
48
+ ChevronRight,
49
+ } from 'lucide-react';
50
+
51
+ // Below this container width the split-pane collapses to a single chat column
52
+ // and the artifact opens as a full-screen dialog instead. Shared by the CSS
53
+ // container query and the JS check in openArtifact so they can't drift. 1023
54
+ // keeps tablet widths from squeezing the chat into a ~100px rail beside the
55
+ // 640px default artifact panel.
56
+ const MOBILE_MAX_WIDTH = 1023;
57
+
58
+ const root: CSSProperties = {
59
+ // The showcase renders a 44px breadcrumb row above the template, so a full
60
+ // 100dvh root always spills exactly that far into page-level scroll. Subtract
61
+ // it so scrolling stays inside the message list and artifact body.
62
+ height: 'calc(100dvh - 44px)',
63
+ width: '100%',
64
+ containerType: 'inline-size',
65
+ containerName: 'artifact',
66
+ };
67
+ const chatColumn: CSSProperties = {
68
+ flex: 1,
69
+ width: '100%',
70
+ minWidth: 0,
71
+ height: '100%',
72
+ };
73
+ const chatLayout: CSSProperties = {
74
+ flex: 1,
75
+ minHeight: 0,
76
+ };
77
+ const artifactCard: CSSProperties = {
78
+ marginBlockStart: 'var(--space-gap)',
79
+ };
80
+ const artifactScroll: CSSProperties = {
81
+ flex: 1,
82
+ overflowY: 'auto',
83
+ };
84
+ const articleBody: CSSProperties = {
85
+ maxWidth: 720,
86
+ marginInline: 'auto',
87
+ };
88
+
89
+ // Runtime width for the artifact panel, passed in via the --artifact-panel-width
90
+ // custom property so the MOBILE container query can still override it to 100%
91
+ // (an inline `width` would beat the class rule). The container query lives in a
92
+ // plain <style> tag below so it needs NO CSS compiler.
93
+ const artifactPanelWidthVar = (size: number | string): CSSProperties =>
94
+ ({
95
+ '--artifact-panel-width': typeof size === 'number' ? `${size}px` : size,
96
+ }) as CSSProperties;
97
+
98
+ const AI_CHAT_CSS = `
99
+ .ai-chat-resize-handle {
100
+ display: flex;
101
+ }
102
+ .ai-chat-artifact-panel {
103
+ overflow: hidden;
104
+ display: flex;
105
+ flex-direction: column;
106
+ width: var(--artifact-panel-width);
107
+ flex-shrink: 0;
108
+ }
109
+ @container artifact (max-width: ${MOBILE_MAX_WIDTH}px) {
110
+ .ai-chat-resize-handle {
111
+ display: none;
112
+ }
113
+ .ai-chat-artifact-panel {
114
+ display: none;
115
+ width: 100%;
116
+ flex-shrink: 1;
117
+ }
118
+ }
119
+ `;
120
+
121
+ // Artifact content
122
+
123
+ const MENTION_TOKENS = [
124
+ {value: '@agent', label: '@Agent', variant: 'blue' as const},
125
+ ];
126
+
127
+ const ARTIFACT_TITLE = 'JWT Token Refresh: Design & Rollout';
128
+
129
+ const ARTIFACT_SUBTITLE = 'Grimoire · Sealed just now';
130
+
131
+ const ARTIFACT_CONTENT = `## Overview
132
+
133
+ Our API gateway authenticates every request with a short-lived JWT access token. Until now, an expired token meant an immediate \`401\` — even when the user still held a valid refresh token. This document describes the silent-refresh flow we just shipped and how we're rolling it out.
134
+
135
+ ## The Problem
136
+
137
+ Token validation ran **before** any refresh logic, so the middleware rejected expired tokens outright:
138
+
139
+ 1. A request arrives with an expired access token
140
+ 2. \`validateToken()\` throws \`TokenExpiredError\`
141
+ 3. The catch block returns \`401\` — \`refreshToken()\` is never reached
142
+
143
+ The result was users getting logged out whenever an access token lapsed mid-session.
144
+
145
+ ## The Fix
146
+
147
+ The middleware now catches \`TokenExpiredError\` specifically and attempts a silent refresh before rejecting. On success it reissues an access token and continues the request; on failure it falls back to \`401\`.
148
+
149
+ - **Transparent** — valid sessions never see an interruption
150
+ - **Safe** — a missing or invalid refresh token still returns \`401\`
151
+ - **Cheap** — refresh only runs on the expiry path, not on every request
152
+
153
+ ## Testing
154
+
155
+ The refresh path is covered end to end:
156
+
157
+ | Scenario | Expected |
158
+ |----------|----------|
159
+ | Valid token passes through | \`200\` |
160
+ | Expired token, valid refresh | \`200\` + new access token |
161
+ | Expired token, invalid refresh | \`401\` |
162
+ | Malformed token | \`401\` |
163
+
164
+ ## Rollout & Monitoring
165
+
166
+ 1. Ship behind the \`silent_refresh\` flag at 5% of traffic
167
+ 2. Watch the \`auth.refresh.success\` and \`auth.refresh.failure\` counters
168
+ 3. Alert if the failure rate exceeds **2%** over any 5-minute window
169
+ 4. Ramp to 100% once metrics hold steady for 24 hours`;
170
+
171
+ // Artifact subviews
172
+
173
+ // Header actions: version menu, copy, share. Pass `onClose` for the desktop
174
+ // close button. A fragment so each control is a direct child of the toolbar.
175
+ function ArtifactActions({onClose}: {onClose?: () => void}) {
176
+ return (
177
+ <>
178
+ <DropdownMenu
179
+ button={{
180
+ label: 'v2',
181
+ variant: 'ghost',
182
+ size: 'sm',
183
+ }}
184
+ items={[{label: 'v2 (current)'}, {label: 'v1'}]}
185
+ />
186
+ <Button
187
+ label="Copy"
188
+ variant="ghost"
189
+ size="sm"
190
+ icon={<Icon icon={Copy} size="sm" />}
191
+ isIconOnly
192
+ />
193
+ <Button
194
+ label="Share"
195
+ variant="ghost"
196
+ size="sm"
197
+ icon={<Icon icon={Share2} size="sm" />}
198
+ isIconOnly
199
+ />
200
+ {onClose != null && (
201
+ <Button
202
+ label="Close document"
203
+ variant="ghost"
204
+ size="sm"
205
+ icon={<Icon icon={X} size="sm" />}
206
+ isIconOnly
207
+ onClick={onClose}
208
+ />
209
+ )}
210
+ </>
211
+ );
212
+ }
213
+
214
+ // Mobile variant: the actions collapse into an overflow menu.
215
+ function MobileArtifactActions() {
216
+ return (
217
+ <MoreMenu
218
+ label="Document actions"
219
+ size="sm"
220
+ items={[
221
+ {
222
+ type: 'section',
223
+ title: 'Version',
224
+ items: [
225
+ {label: 'v2 (current)', onClick: () => {}},
226
+ {label: 'v1', onClick: () => {}},
227
+ ],
228
+ },
229
+ {type: 'divider'},
230
+ {label: 'Copy', icon: Copy},
231
+ {label: 'Share', icon: Share2},
232
+ ]}
233
+ />
234
+ );
235
+ }
236
+
237
+ // Scrollable artifact body — the formatted document.
238
+ function ArtifactBody() {
239
+ return (
240
+ <Section variant="transparent" style={artifactScroll}>
241
+ <VStack gap={2} style={articleBody}>
242
+ <Heading level={1}>{ARTIFACT_TITLE}</Heading>
243
+ <Markdown>{ARTIFACT_CONTENT}</Markdown>
244
+ </VStack>
245
+ </Section>
246
+ );
247
+ }
248
+
249
+ // In-message card that opens the artifact panel/dialog.
250
+ function ArtifactCard({onOpen}: {onOpen: () => void}) {
251
+ return (
252
+ <ClickableCard
253
+ label={`Open ${ARTIFACT_TITLE}`}
254
+ onClick={onOpen}
255
+ variant="muted"
256
+ padding={3}
257
+ maxWidth={360}
258
+ style={artifactCard}>
259
+ <HStack gap={3} vAlign="center" width="100%">
260
+ <Icon icon={FileText} size="md" color="secondary" />
261
+ <StackItem size="fill">
262
+ <VStack gap={0}>
263
+ <Text type="label" weight="semibold">
264
+ {ARTIFACT_TITLE}
265
+ </Text>
266
+ <Text type="supporting" color="secondary">
267
+ Grimoire
268
+ </Text>
269
+ </VStack>
270
+ </StackItem>
271
+ <Icon icon={ChevronRight} size="sm" color="secondary" />
272
+ </HStack>
273
+ </ClickableCard>
274
+ );
275
+ }
276
+
277
+ // Main component
278
+
279
+ export default function AiChat() {
280
+ const [composerMode, setComposerMode] = useState('ask');
281
+ // Mobile shows the artifact as a full-screen dialog; desktop as a side panel.
282
+ const [isArtifactDialogOpen, setIsArtifactDialogOpen] = useState(false);
283
+ const [isArtifactOpen, setIsArtifactOpen] = useState(true);
284
+ const rootRef = useRef<HTMLElement>(null);
285
+ const artifactResize = useResizable({
286
+ defaultSize: 640,
287
+ minSizePx: 480,
288
+ maxSizePx: 960,
289
+ autoSaveId: 'ai-chat-artifact-panel',
290
+ });
291
+
292
+ // Match the container-query breakpoint by measuring the root, not the viewport.
293
+ const openArtifact = () => {
294
+ const width = rootRef.current?.offsetWidth ?? Infinity;
295
+ if (width <= MOBILE_MAX_WIDTH) {
296
+ setIsArtifactDialogOpen(true);
297
+ } else {
298
+ setIsArtifactOpen(true);
299
+ }
300
+ };
301
+
302
+ return (
303
+ <VStack ref={rootRef} style={root}>
304
+ <style>{AI_CHAT_CSS}</style>
305
+ <Layout
306
+ height="fill"
307
+ content={
308
+ <LayoutContent padding={0}>
309
+ <HStack height="100%">
310
+ {/* Chat column — flexes to fill the space the artifact leaves */}
311
+ <VStack style={chatColumn}>
312
+ <ChatLayout
313
+ density="spacious"
314
+ style={chatLayout}
315
+ composer={
316
+ <ChatComposer
317
+ onSubmit={() => {}}
318
+ placeholder={
319
+ composerMode === 'ask'
320
+ ? 'Ask the night anything...'
321
+ : 'Describe the rite...'
322
+ }
323
+ input={<ChatComposerInput />}
324
+ headerActions={
325
+ <>
326
+ <Button
327
+ label="Mention"
328
+ variant="ghost"
329
+ size="sm"
330
+ icon={<Icon icon={AtSign} size="sm" />}
331
+ isIconOnly
332
+ />
333
+ <Button
334
+ label="Attach"
335
+ variant="ghost"
336
+ size="sm"
337
+ icon={<Icon icon={Paperclip} size="sm" />}
338
+ isIconOnly
339
+ />
340
+ </>
341
+ }
342
+ footerActions={
343
+ <DropdownMenu
344
+ button={{
345
+ label: composerMode === 'ask' ? 'Ask' : 'Edit',
346
+ variant: 'ghost',
347
+ size: 'sm',
348
+ }}
349
+ items={[
350
+ {
351
+ label: 'Ask',
352
+ onClick: () => setComposerMode('ask'),
353
+ },
354
+ {
355
+ label: 'Edit',
356
+ onClick: () => setComposerMode('edit'),
357
+ },
358
+ ]}
359
+ />
360
+ }
361
+ />
362
+ }>
363
+ <ChatMessageList>
364
+ {/* Date divider */}
365
+ <ChatSystemMessage variant="divider">
366
+ Today
367
+ </ChatSystemMessage>
368
+
369
+ {/* User message: mention + file attachments */}
370
+ <ChatMessage sender="user">
371
+ <HStack gap={1} wrap="wrap">
372
+ <Token label="auth-service.ts" />
373
+ <Token label="middleware.ts" />
374
+ </HStack>
375
+ <ChatMessageBubble
376
+ metadata={
377
+ <ChatMessageMetadata
378
+ timestamp={
379
+ <Timestamp
380
+ value="2026-04-29T10:15:00"
381
+ format="time"
382
+ />
383
+ }
384
+ />
385
+ }>
386
+ <ChatTokenizedText tokens={MENTION_TOKENS}>
387
+ @agent Can you review these auth files? The JWT
388
+ refresh logic seems broken — tokens expire but the
389
+ middleware doesn't catch it.
390
+ </ChatTokenizedText>
391
+ </ChatMessageBubble>
392
+ </ChatMessage>
393
+
394
+ {/* Assistant message: tool calls, markdown, code block */}
395
+ <ChatMessage
396
+ sender="assistant"
397
+ avatar={<Avatar name="Agent" size="md" />}>
398
+ <ChatMessageBubble variant="ghost">
399
+ Looking into the auth files now. Let me read through the
400
+ code and trace the token refresh flow.
401
+ </ChatMessageBubble>
402
+ <ChatToolCalls
403
+ defaultIsExpanded
404
+ calls={[
405
+ {
406
+ name: 'read',
407
+ target: 'auth-service.ts',
408
+ status: 'complete',
409
+ duration: '45ms',
410
+ },
411
+ {
412
+ name: 'read',
413
+ target: 'middleware.ts',
414
+ status: 'complete',
415
+ duration: '38ms',
416
+ },
417
+ {
418
+ name: 'bash',
419
+ target: 'grep -rn "refreshToken" src/',
420
+ status: 'complete',
421
+ duration: '120ms',
422
+ node: 'cli:remote-server',
423
+ },
424
+ ]}
425
+ />
426
+
427
+ <ChatMessageBubble variant="ghost">
428
+ <Markdown density="compact">{`Found the issue. In \`middleware.ts\`, the token validation runs **before** the refresh check. When a token expires, the middleware rejects the request immediately instead of attempting a refresh.
429
+
430
+ Here's the problematic sequence:
431
+
432
+ 1. Request arrives with an expired access token
433
+ 2. \`validateToken()\` throws \`TokenExpiredError\`
434
+ 3. The catch block returns \`401\` — never reaching \`refreshToken()\`
435
+
436
+ The fix is to catch \`TokenExpiredError\` specifically and attempt a refresh before rejecting:`}</Markdown>
437
+ </ChatMessageBubble>
438
+
439
+ <ChatMessageBubble variant="ghost">
440
+ <CodeBlock
441
+ title="middleware.ts"
442
+ language="typescript"
443
+ code={`async function authMiddleware(req: Request) {
444
+ try {
445
+ const decoded = validateToken(req.headers.authorization);
446
+ req.user = decoded;
447
+ } catch (err) {
448
+ if (err instanceof TokenExpiredError) {
449
+ // Attempt silent refresh before rejecting
450
+ const refreshed = await refreshToken(req.cookies.refreshToken);
451
+ if (refreshed) {
452
+ req.user = refreshed.user;
453
+ req.newAccessToken = refreshed.accessToken;
454
+ return next(req);
455
+ }
456
+ }
457
+ return new Response('Unauthorized', { status: 401 });
458
+ }
459
+ return next(req);
460
+ }`}
461
+ />
462
+ </ChatMessageBubble>
463
+
464
+ <ChatToolCalls
465
+ calls={[
466
+ {
467
+ name: 'edit',
468
+ target: 'middleware.ts',
469
+ status: 'complete',
470
+ duration: '85ms',
471
+ additions: 8,
472
+ deletions: 2,
473
+ },
474
+ ]}
475
+ />
476
+
477
+ <ChatMessageMetadata
478
+ timestamp={
479
+ <Timestamp
480
+ value="2026-04-29T10:15:30"
481
+ format="time"
482
+ />
483
+ }
484
+ footer={
485
+ <Text type="supporting" color="secondary">
486
+ Agent
487
+ </Text>
488
+ }
489
+ />
490
+ </ChatMessage>
491
+
492
+ {/* User message: multi-bubble grouping */}
493
+ <ChatMessage sender="user">
494
+ <ChatMessageBubble group="first">
495
+ Nice catch, that makes sense
496
+ </ChatMessageBubble>
497
+ <ChatMessageBubble
498
+ group="last"
499
+ metadata={
500
+ <ChatMessageMetadata
501
+ timestamp={
502
+ <Timestamp
503
+ value="2026-04-29T10:16:00"
504
+ format="time"
505
+ />
506
+ }
507
+ status="delivered"
508
+ />
509
+ }>
510
+ Can you also add a test for the refresh path?
511
+ </ChatMessageBubble>
512
+ </ChatMessage>
513
+
514
+ {/* Assistant message: test results table + code block */}
515
+ <ChatMessage
516
+ sender="assistant"
517
+ avatar={<Avatar name="Agent" size="md" />}>
518
+ <ChatToolCalls
519
+ defaultIsExpanded
520
+ calls={[
521
+ {
522
+ name: 'read',
523
+ target: 'middleware.test.ts',
524
+ status: 'complete',
525
+ duration: '32ms',
526
+ },
527
+ {
528
+ name: 'edit',
529
+ target: 'middleware.test.ts',
530
+ status: 'complete',
531
+ duration: '110ms',
532
+ additions: 24,
533
+ deletions: 0,
534
+ },
535
+ {
536
+ name: 'bash',
537
+ target: 'yarn test middleware',
538
+ status: 'complete',
539
+ duration: '3.2s',
540
+ node: 'cli:remote-server',
541
+ },
542
+ ]}
543
+ />
544
+ <ChatMessageBubble variant="ghost">
545
+ <Markdown density="compact">{`Added a test for the refresh flow. All **4 tests** pass:
546
+
547
+ | Test | Status |
548
+ |------|--------|
549
+ | Valid token passes through | ✅ |
550
+ | Expired token triggers refresh | ✅ |
551
+ | Expired token with invalid refresh returns 401 | ✅ |
552
+ | Malformed token returns 401 immediately | ✅ |`}</Markdown>
553
+ </ChatMessageBubble>
554
+
555
+ <ChatMessageBubble variant="ghost">
556
+ <CodeBlock
557
+ title="middleware.test.ts"
558
+ language="typescript"
559
+ code={`describe('authMiddleware', () => {
560
+ it('refreshes an expired token silently', async () => {
561
+ const expiredToken = createExpiredJWT(mockUser);
562
+ const validRefresh = createRefreshToken(mockUser);
563
+
564
+ const req = mockRequest({
565
+ authorization: \`Bearer \${expiredToken}\`,
566
+ cookies: { refreshToken: validRefresh },
567
+ });
568
+
569
+ const res = await authMiddleware(req);
570
+
571
+ expect(res.status).toBe(200);
572
+ expect(req.user.id).toBe(mockUser.id);
573
+ expect(req.newAccessToken).toBeDefined();
574
+ });
575
+ });`}
576
+ />
577
+ </ChatMessageBubble>
578
+ <ChatMessageMetadata
579
+ timestamp={
580
+ <Timestamp
581
+ value="2026-04-29T10:16:45"
582
+ format="time"
583
+ />
584
+ }
585
+ />
586
+ </ChatMessage>
587
+
588
+ {/* Status message */}
589
+ <ChatSystemMessage>
590
+ Changes sealed in the crypt
591
+ </ChatSystemMessage>
592
+
593
+ {/* User message: requests a document artifact */}
594
+ <ChatMessage sender="user">
595
+ <ChatMessageBubble
596
+ metadata={
597
+ <ChatMessageMetadata
598
+ timestamp={
599
+ <Timestamp
600
+ value="2026-04-29T10:18:00"
601
+ format="time"
602
+ />
603
+ }
604
+ />
605
+ }>
606
+ Looks solid. Before I open the PR, can you write up a
607
+ short design doc explaining the token-refresh flow for
608
+ the team?
609
+ </ChatMessageBubble>
610
+ </ChatMessage>
611
+
612
+ {/* Assistant message: artifact card */}
613
+ <ChatMessage
614
+ sender="assistant"
615
+ avatar={<Avatar name="Agent" size="md" />}>
616
+ <ChatMessageBubble variant="ghost">
617
+ <Markdown density="compact">
618
+ {`I've drafted a design doc covering the problem, the fix, and the test matrix — pulling straight from the changes we just made.\n\nOpen the document below to review it. Want me to expand any section?`}
619
+ </Markdown>
620
+ </ChatMessageBubble>
621
+ <ArtifactCard onOpen={openArtifact} />
622
+ <ChatMessageMetadata
623
+ timestamp={
624
+ <Timestamp
625
+ value="2026-04-29T10:18:40"
626
+ format="time"
627
+ />
628
+ }
629
+ />
630
+ </ChatMessage>
631
+
632
+ {/* User follow-up */}
633
+ <ChatMessage sender="user">
634
+ <ChatMessageBubble
635
+ metadata={
636
+ <ChatMessageMetadata
637
+ timestamp={
638
+ <Timestamp
639
+ value="2026-04-29T10:20:00"
640
+ format="time"
641
+ />
642
+ }
643
+ status="delivered"
644
+ />
645
+ }>
646
+ This is great. Can you add a section on rollout and
647
+ monitoring at the end?
648
+ </ChatMessageBubble>
649
+ </ChatMessage>
650
+
651
+ {/* Assistant message: in-progress tool call */}
652
+ <ChatMessage
653
+ sender="assistant"
654
+ avatar={<Avatar name="Agent" size="md" />}>
655
+ <ChatMessageBubble variant="ghost">
656
+ <Markdown density="compact">
657
+ {`On it — adding a **Rollout & Monitoring** section with a staged flag ramp and the alert thresholds. Updating the document now.`}
658
+ </Markdown>
659
+ </ChatMessageBubble>
660
+ <ChatToolCalls
661
+ calls={[
662
+ {
663
+ name: 'edit',
664
+ target: 'docs/token-refresh.md',
665
+ status: 'running',
666
+ node: 'cli:remote-server',
667
+ },
668
+ ]}
669
+ />
670
+ <ChatMessageMetadata
671
+ timestamp={
672
+ <Timestamp
673
+ value="2026-04-29T10:20:30"
674
+ format="time"
675
+ />
676
+ }
677
+ />
678
+ </ChatMessage>
679
+ </ChatMessageList>
680
+ </ChatLayout>
681
+ </VStack>
682
+
683
+ {/* Desktop split-pane: resize handle + artifact panel */}
684
+ {isArtifactOpen && (
685
+ <>
686
+ <ResizeHandle
687
+ direction="horizontal"
688
+ resizable={artifactResize.props}
689
+ isReversed
690
+ pillPlacement="start"
691
+ hasDivider
692
+ label="Resize artifact panel"
693
+ className="ai-chat-resize-handle"
694
+ />
695
+
696
+ {/* Toolbar as the card header, body below */}
697
+ <Card
698
+ variant="transparent"
699
+ height="100%"
700
+ className="ai-chat-artifact-panel"
701
+ style={artifactPanelWidthVar(artifactResize.size)}>
702
+ <Toolbar
703
+ label="Artifact actions"
704
+ dividers={['bottom']}
705
+ startContent={
706
+ <HStack gap={3} vAlign="center">
707
+ <Icon
708
+ icon={FileText}
709
+ size="sm"
710
+ color="secondary"
711
+ />
712
+ <VStack gap={0}>
713
+ <Text type="label" weight="semibold">
714
+ {ARTIFACT_TITLE}
715
+ </Text>
716
+ <Text type="supporting" color="secondary">
717
+ {ARTIFACT_SUBTITLE}
718
+ </Text>
719
+ </VStack>
720
+ </HStack>
721
+ }
722
+ endContent={
723
+ <ArtifactActions
724
+ onClose={() => setIsArtifactOpen(false)}
725
+ />
726
+ }
727
+ />
728
+
729
+ <ArtifactBody />
730
+ </Card>
731
+ </>
732
+ )}
733
+ </HStack>
734
+ </LayoutContent>
735
+ }
736
+ />
737
+ {/* Mobile artifact view — full-screen Dialog */}
738
+ <Dialog
739
+ isOpen={isArtifactDialogOpen}
740
+ onOpenChange={setIsArtifactDialogOpen}
741
+ purpose="info"
742
+ variant="fullscreen">
743
+ <Layout
744
+ header={
745
+ <DialogHeader
746
+ title={ARTIFACT_TITLE}
747
+ subtitle={ARTIFACT_SUBTITLE}
748
+ hasDivider
749
+ onOpenChange={setIsArtifactDialogOpen}
750
+ endContent={<MobileArtifactActions />}
751
+ />
752
+ }
753
+ content={
754
+ <LayoutContent padding={0}>
755
+ <ArtifactBody />
756
+ </LayoutContent>
757
+ }
758
+ />
759
+ </Dialog>
760
+ </VStack>
761
+ );
762
+ }