Skip to content

Commit b362abe

Browse files
committed
Updates
1 parent f8e5df5 commit b362abe

8 files changed

Lines changed: 504 additions & 27 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -181,32 +181,32 @@ export function AgentGroup({
181181
<button
182182
type='button'
183183
onClick={toggleExpanded}
184-
className='group/agent flex cursor-pointer items-center gap-2'
184+
className='group/agent flex w-full min-w-0 cursor-pointer items-center gap-2 text-left'
185185
>
186186
<div className='flex size-[16px] flex-shrink-0 items-center justify-center'>
187187
<AgentIcon className='size-[16px] text-[var(--text-icon)]' />
188188
</div>
189189
{isWorking ? (
190-
<ShimmerText className='text-sm'>{headerText}</ShimmerText>
190+
<ShimmerText className='min-w-0 truncate text-sm'>{headerText}</ShimmerText>
191191
) : (
192-
<span className='text-[var(--text-body)] text-sm'>{headerText}</span>
192+
<span className='min-w-0 truncate text-[var(--text-body)] text-sm'>{headerText}</span>
193193
)}
194194
<ChevronDown
195195
className={cn(
196-
'size-[14px] text-[var(--text-icon)] opacity-0 transition-[transform,opacity] duration-150 group-hover/agent:opacity-100 group-focus-visible/agent:opacity-100',
196+
'size-[14px] flex-shrink-0 text-[var(--text-icon)] opacity-0 transition-[transform,opacity] duration-150 group-hover/agent:opacity-100 group-focus-visible/agent:opacity-100',
197197
!expanded && '-rotate-90'
198198
)}
199199
/>
200200
</button>
201201
) : (
202-
<div className='flex items-center gap-2'>
202+
<div className='flex min-w-0 items-center gap-2'>
203203
<div className='flex size-[16px] flex-shrink-0 items-center justify-center'>
204204
<AgentIcon className='size-[16px] text-[var(--text-icon)]' />
205205
</div>
206206
{isWorking ? (
207-
<ShimmerText className='text-sm'>{headerText}</ShimmerText>
207+
<ShimmerText className='min-w-0 truncate text-sm'>{headerText}</ShimmerText>
208208
) : (
209-
<span className='text-[var(--text-body)] text-sm'>{headerText}</span>
209+
<span className='min-w-0 truncate text-[var(--text-body)] text-sm'>{headerText}</span>
210210
)}
211211
</div>
212212
)}

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -236,19 +236,19 @@ export function ToolCallItem({
236236
}
237237

238238
return (
239-
<div className='flex items-center gap-[6px] pl-6'>
239+
<div className='flex min-w-0 items-center gap-[6px] pl-6'>
240240
{BlockIcon && (
241241
<BlockIcon
242242
className='size-[14px] flex-shrink-0 text-[var(--text-icon)]'
243243
style={getBareIconStyle(BlockIcon)}
244244
/>
245245
)}
246246
{isExecuting ? (
247-
<ShimmerText className='text-[13px] [--shimmer-rest:var(--text-secondary)]'>
247+
<ShimmerText className='min-w-0 truncate text-[13px] [--shimmer-rest:var(--text-secondary)]'>
248248
{title}
249249
</ShimmerText>
250250
) : (
251-
<span className='text-[13px] text-[var(--text-secondary)]'>{title}</span>
251+
<span className='min-w-0 truncate text-[13px] text-[var(--text-secondary)]'>{title}</span>
252252
)}
253253
</div>
254254
)

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,3 +1218,159 @@ describe('parser properties', () => {
12181218
}
12191219
})
12201220
})
1221+
1222+
describe('flattened options payload recovery', () => {
1223+
// Observed in the wild: no <options> wrapper, and the LAST entry lost its
1224+
// braces so its title sits on the numeric key with the description hoisted.
1225+
const flattened =
1226+
'options {"1": {"title": "Test files and a second turn in the same DM thread", "description": "watermark + file-delta live check"}, "2": {"title": "Put /chat/the-elder on Brain with a password", "description": "finish chat cutover"}, "3": "Add thinking-status keepalive for long Brain runs", "description": "refresh shimmer past 2 minutes"}'
1227+
1228+
it('renders it as an options card instead of raw JSON', () => {
1229+
const { segments } = parseSpecialTags(flattened, false)
1230+
const options = segments.find((segment) => segment.type === 'options')
1231+
expect(options).toBeDefined()
1232+
expect(Object.keys((options as { data: Record<string, unknown> }).data)).toEqual([
1233+
'1',
1234+
'2',
1235+
'3',
1236+
])
1237+
})
1238+
1239+
it('rebuilds the flattened entry from the hoisted description', () => {
1240+
const { segments } = parseSpecialTags(flattened, false)
1241+
const data = (segments.find((s) => s.type === 'options') as { data: Record<string, unknown> })
1242+
.data
1243+
expect(data['3']).toEqual({
1244+
title: 'Add thinking-status keepalive for long Brain runs',
1245+
description: 'refresh shimmer past 2 minutes',
1246+
})
1247+
})
1248+
1249+
it('drops the bare "options" label rather than leaving it as prose', () => {
1250+
const { segments } = parseSpecialTags(flattened, false)
1251+
expect(segments.some((segment) => segment.type === 'text')).toBe(false)
1252+
})
1253+
1254+
it('repairs the same corruption inside a well-formed tag', () => {
1255+
const tagged =
1256+
'<options>{"1": {"title": "A", "description": "a"}, "2": "B", "description": "b"}</options>'
1257+
const { segments } = parseSpecialTags(tagged, false)
1258+
const data = (segments.find((s) => s.type === 'options') as { data: Record<string, unknown> })
1259+
.data
1260+
expect(data['2']).toEqual({ title: 'B', description: 'b' })
1261+
})
1262+
1263+
it('leaves prose JSON alone', () => {
1264+
const prose = 'Here is the config: {"name": "x", "description": "y"}'
1265+
const { segments } = parseSpecialTags(prose, false)
1266+
expect(segments.some((segment) => segment.type === 'options')).toBe(false)
1267+
})
1268+
1269+
it('does not fire mid-stream', () => {
1270+
expect(parseSpecialTags(flattened, true).segments.some((s) => s.type === 'options')).toBe(false)
1271+
})
1272+
})
1273+
1274+
describe('bare options with a capitalized label', () => {
1275+
// Well-formed payload, but no wrapper and a "Options:" label instead of the
1276+
// lowercase bare word — the second shape seen in the wild.
1277+
const labeled =
1278+
'Options: {"1": {"title": "Live-test ASK top-level, ASK thread, and a mention in another channel", "description": "confirm the new accept rule in Slack"}, "2": {"title": "Test files and a second turn in the same DM thread", "description": "watermark + file-delta live check"}}'
1279+
1280+
it('renders it as an options card', () => {
1281+
const { segments } = parseSpecialTags(labeled, false)
1282+
const options = segments.find((segment) => segment.type === 'options')
1283+
expect(options).toBeDefined()
1284+
expect(Object.keys((options as { data: Record<string, unknown> }).data)).toEqual(['1', '2'])
1285+
})
1286+
1287+
it('leaves no stray "Options:" prose above the card', () => {
1288+
const { segments } = parseSpecialTags(labeled, false)
1289+
expect(segments.some((segment) => segment.type === 'text')).toBe(false)
1290+
})
1291+
1292+
it('keeps real prose that precedes the payload', () => {
1293+
const withProse = `Here is what I suggest.\n\n${labeled}`
1294+
const { segments } = parseSpecialTags(withProse, false)
1295+
const text = segments.find((segment) => segment.type === 'text') as { content: string }
1296+
expect(text.content).toBe('Here is what I suggest.')
1297+
expect(segments.some((segment) => segment.type === 'options')).toBe(true)
1298+
})
1299+
})
1300+
1301+
describe('bare options JSON with no label at all', () => {
1302+
const naked =
1303+
'{"1": {"title": "Live-test ASK top-level, ASK thread, and a mention in another channel", "description": "confirm the new accept rule in Slack"}, "2": {"title": "Test files and a second turn in the same DM thread", "description": "watermark + file-delta live check"}}'
1304+
1305+
it('renders the payload alone as an options card', () => {
1306+
const { segments } = parseSpecialTags(naked, false)
1307+
const options = segments.find((segment) => segment.type === 'options')
1308+
expect(options).toBeDefined()
1309+
expect(Object.keys((options as { data: Record<string, unknown> }).data)).toEqual(['1', '2'])
1310+
expect(segments.some((segment) => segment.type === 'text')).toBe(false)
1311+
})
1312+
1313+
it('renders it after prose with no label between them', () => {
1314+
const { segments } = parseSpecialTags(`Two ways to go from here.\n\n${naked}`, false)
1315+
const text = segments.find((segment) => segment.type === 'text') as { content: string }
1316+
expect(text.content).toBe('Two ways to go from here.')
1317+
expect(segments.some((segment) => segment.type === 'options')).toBe(true)
1318+
})
1319+
1320+
it('recovers a flattened payload with no label either', () => {
1321+
const flattenedNaked = '{"1": {"title": "A", "description": "a"}, "2": "B", "description": "b"}'
1322+
const { segments } = parseSpecialTags(flattenedNaked, false)
1323+
const data = (segments.find((s) => s.type === 'options') as { data: Record<string, unknown> })
1324+
.data
1325+
expect(data['2']).toEqual({ title: 'B', description: 'b' })
1326+
})
1327+
})
1328+
1329+
describe('bare question payload recovery', () => {
1330+
const bare =
1331+
'{"type": "single_select", "prompt": "Which channel should the bot post to?", "options": [{"id": "a", "label": "#general"}, {"id": "b", "label": "#alerts"}]}'
1332+
1333+
it('renders an unwrapped question payload as a question card', () => {
1334+
const { segments } = parseSpecialTags(bare, false)
1335+
const question = segments.find((segment) => segment.type === 'question')
1336+
expect(question).toBeDefined()
1337+
expect((question as { data: Array<{ prompt: string }> }).data[0].prompt).toBe(
1338+
'Which channel should the bot post to?'
1339+
)
1340+
expect(segments.some((segment) => segment.type === 'text')).toBe(false)
1341+
})
1342+
1343+
it('accepts an array payload and strips a bare label', () => {
1344+
const { segments } = parseSpecialTags(`Question: [${bare}]`, false)
1345+
expect(segments.some((segment) => segment.type === 'question')).toBe(true)
1346+
expect(segments.some((segment) => segment.type === 'text')).toBe(false)
1347+
})
1348+
1349+
it('keeps prose that precedes the payload', () => {
1350+
const { segments } = parseSpecialTags(`I need one detail.\n\n${bare}`, false)
1351+
const text = segments.find((segment) => segment.type === 'text') as { content: string }
1352+
expect(text.content).toBe('I need one detail.')
1353+
})
1354+
1355+
it('does not fire mid-stream', () => {
1356+
expect(parseSpecialTags(bare, true).segments.some((s) => s.type === 'question')).toBe(false)
1357+
})
1358+
})
1359+
1360+
describe('ordinary JSON in prose is never turned into a card', () => {
1361+
const prose = [
1362+
'Here is the config: {"name": "elder", "description": "the bot", "enabled": true}',
1363+
'The API returned {"1": "ok", "2": "ok"}',
1364+
'Response shape: {"type": "object", "prompt": "n/a", "options": []}',
1365+
'Use {"type": "single_select"} as the discriminator.',
1366+
'Rows: [{"id": "1", "label": "one"}, {"id": "2", "label": "two"}]',
1367+
'Payload: {"data": {"title": "x", "description": "y"}}',
1368+
'Env: {"0": {"title": "a", "description": "b"}}',
1369+
]
1370+
1371+
it.each(prose)('leaves %s as text', (content) => {
1372+
const { segments } = parseSpecialTags(content, false)
1373+
expect(segments.some((s) => s.type === 'options' || s.type === 'question')).toBe(false)
1374+
expect(segments.some((s) => s.type === 'text')).toBe(true)
1375+
})
1376+
})

0 commit comments

Comments
 (0)