astryx-dracula 0.1.5 → 0.2.1

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