@grackle-ai/web-components 0.111.0 → 0.112.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.
- package/.rush/temp/{406cafe37bd01cafa4c4b9e60e3abee9180ce991.tar.log → c8b4fba3a6cfb704f3582f1197cac0fc11abbc50.tar.log} +18 -18
- package/.rush/temp/{406cafe37bd01cafa4c4b9e60e3abee9180ce991.untar.log → c8b4fba3a6cfb704f3582f1197cac0fc11abbc50.untar.log} +2 -2
- package/.rush/temp/chunked-rush-logs/web-components._phase_build.chunks.jsonl +5 -5
- package/.rush/temp/chunked-rush-logs/web-components._phase_test.chunks.jsonl +21 -20
- package/.rush/temp/{9432bf197de04657ccc35f53841e118300f161e9.tar.log → e2794f6b3dd02e58a0ca34e8c438c22d25296c3e.tar.log} +2 -2
- package/.rush/temp/{9432bf197de04657ccc35f53841e118300f161e9.untar.log → e2794f6b3dd02e58a0ca34e8c438c22d25296c3e.untar.log} +2 -2
- package/.rush/temp/operation/_phase_build/all.log +5 -5
- package/.rush/temp/operation/_phase_build/log-chunks.jsonl +5 -5
- package/.rush/temp/operation/_phase_build/state.json +1 -1
- package/.rush/temp/operation/_phase_test/all.log +21 -20
- package/.rush/temp/operation/_phase_test/log-chunks.jsonl +21 -20
- package/.rush/temp/operation/_phase_test/state.json +1 -1
- package/dist/index.css +1 -1
- package/dist/index.js +6629 -6581
- package/package.json +2 -2
- package/rush-logs/web-components._phase_build.cache.log +1 -1
- package/rush-logs/web-components._phase_build.log +5 -5
- package/rush-logs/web-components._phase_test.cache.log +1 -1
- package/rush-logs/web-components._phase_test.log +21 -20
- package/src/components/chat/ChatInput.module.scss +10 -2
- package/src/components/chat/ChatInput.stories.tsx +35 -0
- package/src/components/chat/ChatInput.tsx +88 -6
- package/src/components/display/EventRenderer.module.scss +18 -85
- package/src/components/display/EventRenderer.stories.tsx +16 -0
- package/src/components/display/EventRenderer.tsx +19 -5
- package/src/components/display/EventStream.stories.tsx +3 -1
- package/src/mocks/mockData.ts +12 -0
- package/src/styles/mixins.scss +93 -0
- package/temp/build/lint/_eslint-5eVG3S6w.json +6 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grackle-ai/web-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.112.0",
|
|
4
4
|
"description": "Presentational React component library for the Grackle web UI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"sideEffects": [
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"remark-gfm": "^4.0.0",
|
|
47
47
|
"lucide-react": "~0.474.0",
|
|
48
48
|
"react-router": "^7.0.0",
|
|
49
|
-
"@grackle-ai/common": "0.
|
|
49
|
+
"@grackle-ai/common": "0.112.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@rushstack/heft": "1.2.7",
|
|
@@ -10,9 +10,9 @@ transforming...
|
|
|
10
10
|
✓ 2715 modules transformed.
|
|
11
11
|
rendering chunks...
|
|
12
12
|
computing gzip size...
|
|
13
|
-
dist/index.css
|
|
14
|
-
dist/index.js 1,
|
|
15
|
-
✓ built in
|
|
13
|
+
dist/index.css 158.47 kB │ gzip: 20.60 kB
|
|
14
|
+
dist/index.js 1,590.94 kB │ gzip: 401.57 kB
|
|
15
|
+
✓ built in 5.74s
|
|
16
16
|
[build:vite-build] Vite build completed.
|
|
17
|
-
---- build finished (
|
|
18
|
-
-------------------- Finished (
|
|
17
|
+
---- build finished (74.958s) ----
|
|
18
|
+
-------------------- Finished (74.961s) --------------------
|
|
@@ -5,29 +5,30 @@ The provided list of phases does not contain all phase dependencies. You may nee
|
|
|
5
5
|
|
|
6
6
|
RUN v3.2.4 /home/runner/work/grackle/grackle/packages/web-components
|
|
7
7
|
|
|
8
|
-
✓ src/utils/sessionEvents.test.ts (14 tests)
|
|
9
|
-
✓ src/utils/eventContent.test.ts (38 tests)
|
|
10
|
-
✓ src/utils/dashboard.test.ts (4 tests)
|
|
11
|
-
✓ src/utils/route-config.test.ts (23 tests)
|
|
12
|
-
✓ src/utils/scrollUtils.test.ts (11 tests)
|
|
13
|
-
✓ src/utils/breadcrumbs.test.ts (18 tests)
|
|
14
|
-
✓ src/components/tools/
|
|
15
|
-
✓ src/components/tools/
|
|
16
|
-
✓ src/components/display/extractText.test.tsx (8 tests)
|
|
17
|
-
✓ src/
|
|
18
|
-
✓ src/
|
|
19
|
-
✓ src/components/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
✓ src/
|
|
8
|
+
✓ src/utils/sessionEvents.test.ts (14 tests) 54ms
|
|
9
|
+
✓ src/utils/eventContent.test.ts (38 tests) 139ms
|
|
10
|
+
✓ src/utils/dashboard.test.ts (4 tests) 7ms
|
|
11
|
+
✓ src/utils/route-config.test.ts (23 tests) 30ms
|
|
12
|
+
✓ src/utils/scrollUtils.test.ts (11 tests) 24ms
|
|
13
|
+
✓ src/utils/breadcrumbs.test.ts (18 tests) 55ms
|
|
14
|
+
✓ src/components/tools/toolCardHelpers.test.ts (10 tests) 27ms
|
|
15
|
+
✓ src/components/tools/classifyTool.test.ts (6 tests) 28ms
|
|
16
|
+
✓ src/components/display/extractText.test.tsx (8 tests) 32ms
|
|
17
|
+
✓ src/hooks/useEventSelection.test.ts (13 tests) 131ms
|
|
18
|
+
✓ src/components/editable/useEditableField.test.tsx (17 tests) 200ms
|
|
19
|
+
✓ src/components/notifications/UpdateBanner.test.tsx (4 tests) 264ms
|
|
20
|
+
✓ src/utils/grackleHostStyleVariables.test.ts (2 tests) 540ms
|
|
21
|
+
✓ grackleHostStyleVariables > always returns the MCP-standard fallback variables 516ms
|
|
22
|
+
✓ src/components/display/McpAppWidget.test.tsx (3 tests) 501ms
|
|
23
|
+
✓ McpAppWidget > mounts an iframe host for the widget 321ms
|
|
23
24
|
|
|
24
25
|
Test Files 14 passed (14)
|
|
25
26
|
Tests 171 passed (171)
|
|
26
|
-
Start at
|
|
27
|
-
Duration 16.
|
|
27
|
+
Start at 22:02:20
|
|
28
|
+
Duration 16.20s (transform 2.92s, setup 0ms, collect 17.59s, tests 2.03s, environment 14.57s, prepare 5.64s)
|
|
28
29
|
|
|
29
30
|
[test:vitest] Vitest completed.
|
|
30
|
-
[test:storybook-test] Starting Storybook static server on port
|
|
31
|
+
[test:storybook-test] Starting Storybook static server on port 38719...
|
|
31
32
|
[test:storybook-test] Storybook server ready. Running interaction tests...
|
|
32
33
|
jest-haste-map: duplicate manual mock found: adapter-manager
|
|
33
34
|
The following files share their name; please delete one of them:
|
|
@@ -120,5 +121,5 @@ jest-haste-map: duplicate manual mock found: utils/network
|
|
|
120
121
|
* <rootDir>/packages/server/src/__mocks__/utils/network.ts
|
|
121
122
|
|
|
122
123
|
[test:storybook-test] Storybook interaction tests completed.
|
|
123
|
-
---- test finished (
|
|
124
|
-
-------------------- Finished (
|
|
124
|
+
---- test finished (115.550s) ----
|
|
125
|
+
-------------------- Finished (115.554s) --------------------
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
.bar {
|
|
8
8
|
@include surface-panel;
|
|
9
9
|
display: flex;
|
|
10
|
-
align-items:
|
|
10
|
+
align-items: flex-end;
|
|
11
11
|
gap: var(--space-sm);
|
|
12
12
|
padding: var(--space-sm) var(--space-md);
|
|
13
13
|
border-top: 1px solid var(--border-subtle);
|
|
@@ -22,9 +22,17 @@
|
|
|
22
22
|
// Form controls
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
24
24
|
|
|
25
|
-
.
|
|
25
|
+
.textarea {
|
|
26
26
|
@include input-field;
|
|
27
27
|
flex: 1;
|
|
28
|
+
// Auto-resizing multiline composer: starts at one row and grows (JS sets the
|
|
29
|
+
// inline height); max-height must match MAX_COMPOSER_HEIGHT_PX in ChatInput.tsx.
|
|
30
|
+
box-sizing: border-box;
|
|
31
|
+
resize: none;
|
|
32
|
+
max-height: 200px;
|
|
33
|
+
overflow-y: auto;
|
|
34
|
+
line-height: var(--line-height);
|
|
35
|
+
font-family: var(--font-ui);
|
|
28
36
|
|
|
29
37
|
@include mobile {
|
|
30
38
|
min-width: 0;
|
|
@@ -134,3 +134,38 @@ export const SendModeActive: Story = {
|
|
|
134
134
|
await expect(canvas.getByRole("button", { name: "Send" })).toBeDisabled();
|
|
135
135
|
},
|
|
136
136
|
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The composer is multiline: a plain Enter inserts a newline and does NOT submit.
|
|
140
|
+
*/
|
|
141
|
+
export const ComposerEnterInsertsNewline: Story = {
|
|
142
|
+
args: {
|
|
143
|
+
mode: "send",
|
|
144
|
+
sessionId: "sess-1",
|
|
145
|
+
environmentId: "local",
|
|
146
|
+
},
|
|
147
|
+
play: async ({ canvas, args }) => {
|
|
148
|
+
const input = canvas.getByPlaceholderText("Type a message...");
|
|
149
|
+
await userEvent.type(input, "line one{Enter}line two");
|
|
150
|
+
await expect(input).toHaveValue("line one\nline two");
|
|
151
|
+
await expect(args.onSendInput).not.toHaveBeenCalled();
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Ctrl/Cmd+Enter submits the composer (mirrors clicking Send) and clears the box.
|
|
157
|
+
*/
|
|
158
|
+
export const ComposerCtrlEnterSubmits: Story = {
|
|
159
|
+
args: {
|
|
160
|
+
mode: "send",
|
|
161
|
+
sessionId: "sess-1",
|
|
162
|
+
environmentId: "local",
|
|
163
|
+
},
|
|
164
|
+
play: async ({ canvas, args }) => {
|
|
165
|
+
const input = canvas.getByPlaceholderText("Type a message...");
|
|
166
|
+
await userEvent.type(input, "hello there");
|
|
167
|
+
await userEvent.keyboard("{Control>}{Enter}{/Control}");
|
|
168
|
+
await expect(args.onSendInput).toHaveBeenCalledWith("sess-1", "hello there");
|
|
169
|
+
await expect(input).toHaveValue("");
|
|
170
|
+
},
|
|
171
|
+
};
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import { useState, type FormEvent, type JSX } from "react";
|
|
1
|
+
import { useState, useLayoutEffect, useRef, type FormEvent, type KeyboardEvent, type JSX } from "react";
|
|
2
2
|
import type { ToastVariant } from "../../context/ToastContext.js";
|
|
3
3
|
import type { Environment, PersonaData } from "../../hooks/types.js";
|
|
4
4
|
import styles from "./ChatInput.module.scss";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Maximum height (px) the composer grows to before scrolling internally.
|
|
8
|
+
* Must stay in sync with `max-height` on `.textarea` in ChatInput.module.scss.
|
|
9
|
+
*/
|
|
10
|
+
const MAX_COMPOSER_HEIGHT_PX: number = 200;
|
|
11
|
+
|
|
6
12
|
// --- Helpers ---
|
|
7
13
|
|
|
8
14
|
/** Returns true when the environment with the given ID is disconnected or in error. */
|
|
@@ -41,6 +47,77 @@ function DisconnectedBanner({ environmentId, onReconnect }: DisconnectedBannerPr
|
|
|
41
47
|
);
|
|
42
48
|
}
|
|
43
49
|
|
|
50
|
+
/** Props for the auto-resizing chat composer textarea. */
|
|
51
|
+
interface ComposerTextAreaProps {
|
|
52
|
+
/** Current text value. */
|
|
53
|
+
value: string;
|
|
54
|
+
/** Called on every keystroke with the new value. */
|
|
55
|
+
onChange: (value: string) => void;
|
|
56
|
+
/** Called when the user submits via Ctrl/Cmd+Enter. */
|
|
57
|
+
onSubmit: () => void;
|
|
58
|
+
/** Placeholder text shown when empty. */
|
|
59
|
+
placeholder: string;
|
|
60
|
+
/** Whether the textarea is disabled. */
|
|
61
|
+
disabled?: boolean;
|
|
62
|
+
/** Whether to auto-focus the textarea on mount. */
|
|
63
|
+
autoFocus?: boolean;
|
|
64
|
+
/** Accessible label for the textarea. */
|
|
65
|
+
ariaLabel: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Auto-resizing multiline chat composer.
|
|
70
|
+
*
|
|
71
|
+
* Enter inserts a newline; Ctrl/Cmd+Enter submits (the Send button submits too).
|
|
72
|
+
* The textarea grows with its content up to {@link MAX_COMPOSER_HEIGHT_PX}, then
|
|
73
|
+
* scrolls internally.
|
|
74
|
+
*/
|
|
75
|
+
function ComposerTextArea({
|
|
76
|
+
value,
|
|
77
|
+
onChange,
|
|
78
|
+
onSubmit,
|
|
79
|
+
placeholder,
|
|
80
|
+
disabled,
|
|
81
|
+
autoFocus,
|
|
82
|
+
ariaLabel,
|
|
83
|
+
}: ComposerTextAreaProps): JSX.Element {
|
|
84
|
+
const ref = useRef<HTMLTextAreaElement>(null);
|
|
85
|
+
|
|
86
|
+
// Auto-resize: collapse to measure natural height, then grow to fit (capped).
|
|
87
|
+
useLayoutEffect(() => {
|
|
88
|
+
const el = ref.current;
|
|
89
|
+
if (!el) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
el.style.height = "auto";
|
|
93
|
+
el.style.height = `${Math.min(el.scrollHeight, MAX_COMPOSER_HEIGHT_PX)}px`;
|
|
94
|
+
}, [value]);
|
|
95
|
+
|
|
96
|
+
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>): void => {
|
|
97
|
+
// Ctrl/Cmd+Enter submits; plain Enter falls through to insert a newline.
|
|
98
|
+
// The isComposing guard avoids submitting mid-IME composition (e.g. CJK input).
|
|
99
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey) && !e.nativeEvent.isComposing) {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
onSubmit();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<textarea
|
|
107
|
+
ref={ref}
|
|
108
|
+
rows={1}
|
|
109
|
+
value={value}
|
|
110
|
+
onChange={(e) => onChange(e.target.value)}
|
|
111
|
+
onKeyDown={handleKeyDown}
|
|
112
|
+
placeholder={placeholder}
|
|
113
|
+
disabled={disabled}
|
|
114
|
+
autoFocus={autoFocus}
|
|
115
|
+
className={styles.textarea}
|
|
116
|
+
aria-label={ariaLabel}
|
|
117
|
+
/>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
44
121
|
// --- Main component ---
|
|
45
122
|
|
|
46
123
|
/** Chat input mode determines the action performed on submit. */
|
|
@@ -92,8 +169,8 @@ export function ChatInput({
|
|
|
92
169
|
|
|
93
170
|
const envDisconnected = isEnvDisconnected(environmentId, environments);
|
|
94
171
|
|
|
95
|
-
|
|
96
|
-
|
|
172
|
+
/** Performs the mode-specific submit action. Called by the form and Ctrl/Cmd+Enter. */
|
|
173
|
+
const submit = (): void => {
|
|
97
174
|
if (!text.trim()) {
|
|
98
175
|
return;
|
|
99
176
|
}
|
|
@@ -122,6 +199,11 @@ export function ChatInput({
|
|
|
122
199
|
}
|
|
123
200
|
};
|
|
124
201
|
|
|
202
|
+
const handleSubmit = (e: FormEvent): void => {
|
|
203
|
+
e.preventDefault();
|
|
204
|
+
submit();
|
|
205
|
+
};
|
|
206
|
+
|
|
125
207
|
// --- spawn mode ---
|
|
126
208
|
if (mode === "spawn") {
|
|
127
209
|
return (
|
|
@@ -129,7 +211,7 @@ export function ChatInput({
|
|
|
129
211
|
<span className={styles.badge}>
|
|
130
212
|
new chat
|
|
131
213
|
</span>
|
|
132
|
-
<
|
|
214
|
+
<ComposerTextArea value={text} onChange={setText} onSubmit={submit} placeholder="Enter prompt..." autoFocus ariaLabel="Enter prompt" />
|
|
133
215
|
{showPersonaSelect && (
|
|
134
216
|
<select value={spawnPersonaId} onChange={(e) => setSpawnPersonaId(e.target.value)} className={styles.select} aria-label="Select persona">
|
|
135
217
|
<option value="">(Default)</option>
|
|
@@ -147,7 +229,7 @@ export function ChatInput({
|
|
|
147
229
|
if (mode === "start") {
|
|
148
230
|
return (
|
|
149
231
|
<form onSubmit={handleSubmit} className={styles.bar}>
|
|
150
|
-
<
|
|
232
|
+
<ComposerTextArea value={text} onChange={setText} onSubmit={submit} placeholder="Type a message..." autoFocus ariaLabel="Type a message" />
|
|
151
233
|
<button type="submit" disabled={!text.trim()} className={styles.btnPrimary}>Send</button>
|
|
152
234
|
</form>
|
|
153
235
|
);
|
|
@@ -159,7 +241,7 @@ export function ChatInput({
|
|
|
159
241
|
{envDisconnected && environmentId && (
|
|
160
242
|
<DisconnectedBanner environmentId={environmentId} onReconnect={onProvisionEnvironment} />
|
|
161
243
|
)}
|
|
162
|
-
<
|
|
244
|
+
<ComposerTextArea value={text} onChange={setText} onSubmit={submit} placeholder="Type a message..." autoFocus={!envDisconnected} disabled={envDisconnected} ariaLabel="Type a message" />
|
|
163
245
|
<span title={envDisconnected ? "Environment is unavailable — reconnect first" : undefined}>
|
|
164
246
|
<button type="submit" disabled={!text.trim() || envDisconnected} className={styles.btnPrimary}>Send</button>
|
|
165
247
|
</span>
|
|
@@ -81,91 +81,7 @@
|
|
|
81
81
|
padding-left: var(--space-xs);
|
|
82
82
|
line-height: var(--line-height);
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
h1, h2, h3, h4, h5, h6 {
|
|
87
|
-
margin: var(--space-sm) 0 var(--space-xs);
|
|
88
|
-
font-weight: var(--font-weight-bold);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
h1 { font-size: 1.4em; }
|
|
92
|
-
h2 { font-size: 1.2em; }
|
|
93
|
-
h3 { font-size: 1.1em; }
|
|
94
|
-
|
|
95
|
-
p {
|
|
96
|
-
margin: var(--space-xs) 0;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
pre {
|
|
100
|
-
@include surface-inset;
|
|
101
|
-
padding: var(--space-sm);
|
|
102
|
-
margin: var(--space-xs) 0;
|
|
103
|
-
overflow: auto;
|
|
104
|
-
font-size: 11px;
|
|
105
|
-
white-space: pre-wrap;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
code {
|
|
109
|
-
font-family: var(--font-mono);
|
|
110
|
-
font-size: 0.9em;
|
|
111
|
-
background: var(--bg-overlay);
|
|
112
|
-
padding: 1px 4px;
|
|
113
|
-
border-radius: var(--radius-sm);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
pre code {
|
|
117
|
-
background: none;
|
|
118
|
-
padding: 0;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
table {
|
|
122
|
-
border-collapse: collapse;
|
|
123
|
-
margin: var(--space-xs) 0;
|
|
124
|
-
font-size: var(--font-size-sm);
|
|
125
|
-
width: 100%;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
th, td {
|
|
129
|
-
border: 1px solid var(--border-subtle);
|
|
130
|
-
padding: var(--space-xs) var(--space-sm);
|
|
131
|
-
text-align: left;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
th {
|
|
135
|
-
background: var(--bg-overlay);
|
|
136
|
-
font-weight: var(--font-weight-bold);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
ul, ol {
|
|
140
|
-
padding-left: var(--space-lg);
|
|
141
|
-
margin: var(--space-xs) 0;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
li {
|
|
145
|
-
margin: 2px 0;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
strong {
|
|
149
|
-
font-weight: var(--font-weight-bold);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
a {
|
|
153
|
-
color: var(--accent-blue);
|
|
154
|
-
text-decoration: underline;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
hr {
|
|
158
|
-
border: none;
|
|
159
|
-
border-top: 1px solid var(--border-subtle);
|
|
160
|
-
margin: var(--space-sm) 0;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
blockquote {
|
|
164
|
-
border-left: 3px solid var(--border-subtle);
|
|
165
|
-
padding-left: var(--space-sm);
|
|
166
|
-
margin: var(--space-xs) 0;
|
|
167
|
-
color: var(--text-secondary);
|
|
168
|
-
}
|
|
84
|
+
@include markdown-content;
|
|
169
85
|
}
|
|
170
86
|
|
|
171
87
|
// --- Copy button positioning ---
|
|
@@ -220,6 +136,23 @@
|
|
|
220
136
|
font-size: var(--font-size-sm);
|
|
221
137
|
line-height: var(--line-height);
|
|
222
138
|
|
|
139
|
+
@include markdown-content;
|
|
140
|
+
|
|
141
|
+
// Collapse outer block margins so the bubble hugs its markdown content.
|
|
142
|
+
& > :first-child {
|
|
143
|
+
margin-top: 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
& > :last-child {
|
|
147
|
+
margin-bottom: 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Keep wide block content (code, tables) within the bubble bounds.
|
|
151
|
+
pre,
|
|
152
|
+
table {
|
|
153
|
+
max-width: 100%;
|
|
154
|
+
}
|
|
155
|
+
|
|
223
156
|
@include mobile {
|
|
224
157
|
max-width: 90%;
|
|
225
158
|
}
|
|
@@ -170,6 +170,22 @@ export const MarkdownParagraphWrapping: Story = {
|
|
|
170
170
|
},
|
|
171
171
|
};
|
|
172
172
|
|
|
173
|
+
/** User input events render as markdown (bold, lists, inline code) inside the bubble. */
|
|
174
|
+
export const UserMessageMarkdown: Story = {
|
|
175
|
+
args: {
|
|
176
|
+
event: makeEvent({
|
|
177
|
+
eventType: "user_input",
|
|
178
|
+
content: "Please fix **the bug** in:\n\n- `index.ts`\n- `app.ts`",
|
|
179
|
+
}),
|
|
180
|
+
},
|
|
181
|
+
play: async ({ canvas }) => {
|
|
182
|
+
await expect(canvas.getByTestId("user-input-content")).toBeInTheDocument();
|
|
183
|
+
await expect(canvas.getByText("the bug").tagName).toBe("STRONG");
|
|
184
|
+
await expect(canvas.getAllByRole("listitem")).toHaveLength(2);
|
|
185
|
+
await expect(canvas.getByText("index.ts").tagName).toBe("CODE");
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
173
189
|
/** System context event renders as collapsible prompt. */
|
|
174
190
|
export const SystemContext: Story = {
|
|
175
191
|
args: {
|
|
@@ -107,13 +107,25 @@ const markdownComponents: Record<string, typeof CodeBlockWrapper> = {
|
|
|
107
107
|
pre: CodeBlockWrapper,
|
|
108
108
|
};
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Renders a markdown string with GFM support and syntax-highlighted code blocks.
|
|
112
|
+
*
|
|
113
|
+
* Shared by both assistant text events and user input events so the two render
|
|
114
|
+
* through an identical pipeline.
|
|
115
|
+
*/
|
|
116
|
+
function MarkdownContent({ content }: { content: string }): JSX.Element {
|
|
117
|
+
return (
|
|
118
|
+
<Markdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypePrismPlus]} components={markdownComponents}>
|
|
119
|
+
{content}
|
|
120
|
+
</Markdown>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
110
124
|
/** Renders an assistant text output event with markdown formatting. */
|
|
111
125
|
function TextEvent({ content }: { content: string }): JSX.Element {
|
|
112
126
|
return (
|
|
113
127
|
<div className={styles.textEvent}>
|
|
114
|
-
<
|
|
115
|
-
{content}
|
|
116
|
-
</Markdown>
|
|
128
|
+
<MarkdownContent content={content} />
|
|
117
129
|
</div>
|
|
118
130
|
);
|
|
119
131
|
}
|
|
@@ -140,11 +152,13 @@ function StatusEvent({ content }: { content: string }): JSX.Element {
|
|
|
140
152
|
);
|
|
141
153
|
}
|
|
142
154
|
|
|
143
|
-
/** Renders a user input event, right-aligned to distinguish it from agent output. */
|
|
155
|
+
/** Renders a user input event as markdown, right-aligned to distinguish it from agent output. */
|
|
144
156
|
function UserInputEvent({ content }: { content: string }): JSX.Element {
|
|
145
157
|
return (
|
|
146
158
|
<div className={styles.userInputEvent}>
|
|
147
|
-
<
|
|
159
|
+
<div className={styles.userInputContent} data-testid="user-input-content">
|
|
160
|
+
<MarkdownContent content={content} />
|
|
161
|
+
</div>
|
|
148
162
|
</div>
|
|
149
163
|
);
|
|
150
164
|
}
|
|
@@ -5,6 +5,8 @@ import type { DisplayEvent } from "../../utils/sessionEvents.js";
|
|
|
5
5
|
import { makeEvent, makeSession, makeEnvironment } from "../../test-utils/storybook-helpers.js";
|
|
6
6
|
|
|
7
7
|
const sampleEvents: DisplayEvent[] = [
|
|
8
|
+
// User messages render as markdown, same as agent output.
|
|
9
|
+
makeEvent({ eventType: "user_input", content: "Can you check **`auth.ts`**? The `refreshToken` flow looks broken:\n\n1. token isn't refreshed before expiry\n2. the retry has an off-by-one", timestamp: "2026-01-01T00:00:00Z" }),
|
|
8
10
|
makeEvent({ eventType: "text", content: "First message", timestamp: "2026-01-01T00:00:01Z" }),
|
|
9
11
|
makeEvent({ eventType: "text", content: "Second message", timestamp: "2026-01-01T00:00:02Z" }),
|
|
10
12
|
makeEvent({ eventType: "text", content: "Third message", timestamp: "2026-01-01T00:00:03Z" }),
|
|
@@ -12,7 +14,7 @@ const sampleEvents: DisplayEvent[] = [
|
|
|
12
14
|
|
|
13
15
|
/** A richer set of events including non-content types for selection mode tests. */
|
|
14
16
|
const mixedEvents: DisplayEvent[] = [
|
|
15
|
-
makeEvent({ eventType: "user_input", content: "Fix the bug", timestamp: "2026-01-01T00:00:01Z" }),
|
|
17
|
+
makeEvent({ eventType: "user_input", content: "Fix the **login bug** in `auth.ts`:\n\n- token isn't refreshed\n- expiry check is off-by-one", timestamp: "2026-01-01T00:00:01Z" }),
|
|
16
18
|
makeEvent({ eventType: "text", content: "Looking into it.", timestamp: "2026-01-01T00:00:02Z" }),
|
|
17
19
|
makeEvent({ eventType: "status", content: "running", timestamp: "2026-01-01T00:00:03Z" }),
|
|
18
20
|
makeEvent({ eventType: "text", content: "Found the issue in auth.ts", timestamp: "2026-01-01T00:00:04Z" }),
|
package/src/mocks/mockData.ts
CHANGED
|
@@ -265,6 +265,12 @@ export const MOCK_EVENTS: SessionEvent[] = [
|
|
|
265
265
|
content: "You are a senior backend engineer. Focus on security best practices and thorough testing.\nProject: acme/alpha — Node.js + Express REST API\nTask: Implement auth middleware using JWT tokens\n\nNote: A previous session attempted this task but was interrupted. Pick up where it left off.",
|
|
266
266
|
raw: JSON.stringify({ systemContext: true }),
|
|
267
267
|
},
|
|
268
|
+
{
|
|
269
|
+
sessionId: "sess-001",
|
|
270
|
+
eventType: "user_input",
|
|
271
|
+
timestamp: "2026-02-27T08:15:03Z",
|
|
272
|
+
content: "Pick up the **JWT auth migration** from the previous session. A few requirements:\n\n- use `jsonwebtoken`, not `jose`\n- access tokens expire in `24h`\n- store refresh tokens in the DB\n\nMake sure the existing tests still pass.",
|
|
273
|
+
},
|
|
268
274
|
{
|
|
269
275
|
sessionId: "sess-001",
|
|
270
276
|
eventType: "text",
|
|
@@ -492,6 +498,12 @@ export const MOCK_EVENTS: SessionEvent[] = [
|
|
|
492
498
|
timestamp: "2026-02-27T08:15:42Z",
|
|
493
499
|
content: "All 14 tests pass. Here's a summary of the changes:\n\n### Changes made\n\n| File | Action |\n|------|--------|\n| `src/middleware/auth.ts` | Rewrote to verify JWT Bearer tokens |\n| `src/routes/login.ts` | Now issues JWT access + refresh tokens |\n| `src/routes/protected.ts` | Updated to read `req.user` from JWT payload |\n| `src/middleware/__tests__/auth.test.ts` | Updated tests for JWT verification |\n\n### Key decisions\n- **Access token expiry**: 24 hours (configurable via `JWT_SECRET` env var)\n- **Refresh tokens**: 48-byte random hex, stored in DB\n- **Error handling**: Distinguishes expired tokens (401) from invalid tokens (403)",
|
|
494
500
|
},
|
|
501
|
+
{
|
|
502
|
+
sessionId: "sess-001",
|
|
503
|
+
eventType: "user_input",
|
|
504
|
+
timestamp: "2026-02-27T08:15:50Z",
|
|
505
|
+
content: "Looks great! Two follow-ups:\n\n1. add a `/refresh` endpoint that swaps a refresh token for a new access token\n2. note the token flow in `README.md`",
|
|
506
|
+
},
|
|
495
507
|
|
|
496
508
|
// ── sess-002: completed unit test session ──
|
|
497
509
|
|
package/src/styles/mixins.scss
CHANGED
|
@@ -264,6 +264,99 @@ $tablet: 1024px;
|
|
|
264
264
|
font-weight: var(--font-weight-medium);
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
// =============================================================================
|
|
268
|
+
// Markdown Content
|
|
269
|
+
// =============================================================================
|
|
270
|
+
|
|
271
|
+
/// Styles for a rendered react-markdown element tree (headings, code, tables,
|
|
272
|
+
/// lists, links, etc.). Applied to any container that renders markdown output,
|
|
273
|
+
/// e.g. agent text events and user message bubbles.
|
|
274
|
+
@mixin markdown-content {
|
|
275
|
+
h1, h2, h3, h4, h5, h6 {
|
|
276
|
+
margin: var(--space-sm) 0 var(--space-xs);
|
|
277
|
+
font-weight: var(--font-weight-bold);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
h1 { font-size: 1.4em; }
|
|
281
|
+
h2 { font-size: 1.2em; }
|
|
282
|
+
h3 { font-size: 1.1em; }
|
|
283
|
+
|
|
284
|
+
p {
|
|
285
|
+
margin: var(--space-xs) 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
pre {
|
|
289
|
+
@include surface-inset;
|
|
290
|
+
padding: var(--space-sm);
|
|
291
|
+
margin: var(--space-xs) 0;
|
|
292
|
+
overflow: auto;
|
|
293
|
+
font-size: 11px;
|
|
294
|
+
white-space: pre-wrap;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
code {
|
|
298
|
+
font-family: var(--font-mono);
|
|
299
|
+
font-size: 0.9em;
|
|
300
|
+
background: var(--bg-overlay);
|
|
301
|
+
padding: 1px 4px;
|
|
302
|
+
border-radius: var(--radius-sm);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
pre code {
|
|
306
|
+
background: none;
|
|
307
|
+
padding: 0;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
table {
|
|
311
|
+
border-collapse: collapse;
|
|
312
|
+
margin: var(--space-xs) 0;
|
|
313
|
+
font-size: var(--font-size-sm);
|
|
314
|
+
width: 100%;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
th, td {
|
|
318
|
+
border: 1px solid var(--border-subtle);
|
|
319
|
+
padding: var(--space-xs) var(--space-sm);
|
|
320
|
+
text-align: left;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
th {
|
|
324
|
+
background: var(--bg-overlay);
|
|
325
|
+
font-weight: var(--font-weight-bold);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
ul, ol {
|
|
329
|
+
padding-left: var(--space-lg);
|
|
330
|
+
margin: var(--space-xs) 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
li {
|
|
334
|
+
margin: 2px 0;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
strong {
|
|
338
|
+
font-weight: var(--font-weight-bold);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
a {
|
|
342
|
+
color: var(--accent-blue);
|
|
343
|
+
text-decoration: underline;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
hr {
|
|
347
|
+
border: none;
|
|
348
|
+
border-top: 1px solid var(--border-subtle);
|
|
349
|
+
margin: var(--space-sm) 0;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
blockquote {
|
|
353
|
+
border-left: 3px solid var(--border-subtle);
|
|
354
|
+
padding-left: var(--space-sm);
|
|
355
|
+
margin: var(--space-xs) 0;
|
|
356
|
+
color: var(--text-secondary);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
267
360
|
// =============================================================================
|
|
268
361
|
// Animation Helpers
|
|
269
362
|
// =============================================================================
|