@puredesktop/puredesktop-ui-bridge 2.1.7 → 2.1.9

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 (119) hide show
  1. package/package.json +89 -5
  2. package/src/agents/mcpServerPresets.test.ts +105 -0
  3. package/src/agents/mcpServerPresets.ts +380 -0
  4. package/src/agents/runtime/index.ts +7 -0
  5. package/src/agents/runtime/mapAgentSnapshot.ts +11 -8
  6. package/src/agents/runtime/pendingAsks.test.ts +117 -0
  7. package/src/agents/runtime/pendingAsks.ts +110 -0
  8. package/src/bridge/agentScopeDefaults.ts +65 -0
  9. package/src/bridge/agentTypes.ts +57 -3
  10. package/src/bridge/agents.d.mts +6 -0
  11. package/src/bridge/agents.mjs +10 -0
  12. package/src/bridge/agents.ts +4 -0
  13. package/src/bridge/collectionImagePaste.ts +19 -1
  14. package/src/bridge/context.mjs +32 -0
  15. package/src/bridge/credentials.d.mts +62 -0
  16. package/src/bridge/credentials.mjs +53 -0
  17. package/src/bridge/documents.d.mts +76 -0
  18. package/src/bridge/documents.mjs +40 -0
  19. package/src/bridge/events.d.mts +1 -0
  20. package/src/bridge/events.mjs +1 -0
  21. package/src/bridge/googleAuth.ts +7 -152
  22. package/src/bridge/mcpClient.d.mts +74 -0
  23. package/src/bridge/mcpClient.mjs +79 -0
  24. package/src/bridge/methods.d.mts +45 -0
  25. package/src/bridge/methods.mjs +46 -0
  26. package/src/bridge/notifications.d.mts +12 -0
  27. package/src/bridge/notifications.mjs +36 -0
  28. package/src/bridge/operations.d.mts +44 -0
  29. package/src/bridge/operations.mjs +40 -0
  30. package/src/bridge/pureRender/base.css +44 -0
  31. package/src/bridge/pureRender/document.ts +29 -2
  32. package/src/bridge/pureRender/index.ts +3 -0
  33. package/src/bridge/pureRender/normalize.ts +167 -0
  34. package/src/bridge/pureRender/sanitizeExportBody.test.ts +51 -0
  35. package/src/bridge/pureRender/sanitizeExportBody.ts +61 -0
  36. package/src/bridge/pureRender/styles.ts +125 -5
  37. package/src/bridge/pureRender/theme.test.ts +259 -0
  38. package/src/bridge/pureRender/theme.ts +19 -1
  39. package/src/bridge/pureRender/types.ts +33 -0
  40. package/src/bridge/pureRender/visualLayoutInspection.test.ts +26 -0
  41. package/src/bridge/pureRender/visualLayoutInspection.ts +75 -0
  42. package/src/bridge/react/useDocumentHotkeys.ts +41 -0
  43. package/src/bridge/react/useDocumentLifecycle.tsx +359 -0
  44. package/src/bridge/react/usePlatformAgentSessionDrive.test.tsx +29 -10
  45. package/src/bridge/react/usePlatformAgentSessionDrive.tsx +10 -81
  46. package/src/bridge/react/usePlatformAppSettings.test.tsx +105 -0
  47. package/src/bridge/react/usePlatformAppSettings.tsx +104 -0
  48. package/src/bridge/react/usePlatformJsonStore.test.tsx +152 -0
  49. package/src/bridge/react/usePlatformJsonStore.tsx +149 -0
  50. package/src/bridge/react/usePlatformOperations.tsx +75 -0
  51. package/src/bridge/storage.d.mts +23 -10
  52. package/src/bridge/storage.mjs +6 -3
  53. package/src/bridge/storage.test.ts +6 -6
  54. package/src/bridge/types.ts +7 -0
  55. package/src/components/agents/AgentComposer.tsx +10 -6
  56. package/src/components/agents/AgentDrawerPanel.test.tsx +2 -205
  57. package/src/components/agents/AgentDrawerPanel.tsx +23 -62
  58. package/src/components/agents/AgentMessageBubble.tsx +59 -33
  59. package/src/components/agents/AgentMessageList.tsx +4 -4
  60. package/src/components/agents/AgentQuestionPromptList.tsx +34 -212
  61. package/src/components/agents/AgentToolApprovalActions.tsx +13 -11
  62. package/src/components/agents/AgentToolCallCard.tsx +20 -20
  63. package/src/components/agents/AgentToolPendingList.tsx +6 -6
  64. package/src/components/agents/AgentToolPendingReview.tsx +6 -6
  65. package/src/components/agents/ContextPicker.tsx +239 -0
  66. package/src/components/agents/QuestionRequestList.tsx +278 -0
  67. package/src/components/agents/agentPanelStyles.ts +8 -31
  68. package/src/components/agents/agentTypes.ts +25 -41
  69. package/src/components/agents/index.ts +4 -0
  70. package/src/components/assets/asset-library/sidebar/AssetLibraryList.tsx +10 -4
  71. package/src/components/chrome/WorkspaceTabStrip.tsx +22 -4
  72. package/src/components/chrome/workspaceTabTypes.ts +2 -0
  73. package/src/components/common/chat/ChatBox.tsx +9 -9
  74. package/src/components/common/chat/ChatModelMenu.tsx +34 -4
  75. package/src/components/common/chat/ChatThread.tsx +23 -7
  76. package/src/components/common/connections/ProviderConnection.test.tsx +208 -0
  77. package/src/components/common/connections/ProviderConnection.tsx +277 -0
  78. package/src/components/common/connections/index.ts +5 -0
  79. package/src/components/common/containers/AppFrame.test.tsx +4 -4
  80. package/src/components/common/containers/AppFrame.tsx +26 -10
  81. package/src/components/common/containers/AppHeader.tsx +37 -15
  82. package/src/components/common/documents/DocumentHeaderActions.tsx +206 -0
  83. package/src/components/common/documents/DocumentSwitcher.tsx +1181 -0
  84. package/src/components/common/documents/index.ts +8 -0
  85. package/src/components/common/dropdown/MenuDropdownItem.tsx +14 -2
  86. package/src/components/common/dropdown/MenuPortal.tsx +3 -1
  87. package/src/components/common/dropdown/menuViewportPosition.test.ts +20 -10
  88. package/src/components/common/dropdown/menuViewportPosition.ts +35 -27
  89. package/src/components/common/overlays/Modal.tsx +13 -2
  90. package/src/components/common/research/EvidenceDossier.tsx +1232 -150
  91. package/src/components/common/research/index.ts +2 -0
  92. package/src/components/editor/EditorLinkPromptDialog.tsx +1 -1
  93. package/src/components/print-design/PrintDesignPanel.test.tsx +430 -40
  94. package/src/components/print-design/PrintDesignPanel.tsx +1770 -207
  95. package/src/components/print-design/index.ts +2 -0
  96. package/src/components/print-design/previewContent.test.ts +32 -0
  97. package/src/components/print-design/previewContent.ts +31 -0
  98. package/src/context/buildContextDocument.test.ts +240 -0
  99. package/src/context/buildContextDocument.ts +309 -0
  100. package/src/context/contextAccess.ts +66 -0
  101. package/src/context/contextConfig.ts +72 -0
  102. package/src/context/contextDocument.test.ts +155 -0
  103. package/src/context/contextDocument.ts +300 -0
  104. package/src/context/contextSections.test.ts +88 -0
  105. package/src/context/contextSections.ts +84 -0
  106. package/src/context/htmlToContextMarkdown.test.ts +134 -0
  107. package/src/context/htmlToContextMarkdown.ts +369 -0
  108. package/src/ics/generateIcs.test.ts +153 -0
  109. package/src/ics/generateIcs.ts +197 -0
  110. package/src/index.ts +3 -0
  111. package/src/net/httpRetry.test.ts +117 -0
  112. package/src/net/httpRetry.ts +111 -0
  113. package/src/theme/appAccents.test.ts +36 -35
  114. package/src/theme/appAccents.ts +93 -104
  115. package/src/theme/appIdentityCss.mjs +178 -229
  116. package/src/theme/appIdentityCss.ts +222 -231
  117. package/src/theme/themes/dark.ts +3 -1
  118. package/src/theme/themes/light.ts +3 -1
  119. package/src/bridge/googleProviderConfig.ts +0 -73
@@ -12,7 +12,6 @@ import {
12
12
  import { AgentDrawerPanel } from './AgentDrawerPanel.js'
13
13
  import {
14
14
  AgentLivePhase,
15
- AgentUiRunState,
16
15
  type AgentDrawerPanelProps,
17
16
  } from './agentTypes.js'
18
17
 
@@ -27,12 +26,7 @@ function baseProps(
27
26
  messages: [],
28
27
  assistantLabel: 'Assistant',
29
28
  pendingToolCalls: [],
30
- runState: AgentUiRunState.AwaitingUser,
31
- busy: false,
32
29
  error: null,
33
- composerValue: 'draft',
34
- onComposerChange: vi.fn(),
35
- onSend: vi.fn(),
36
30
  onApproveTools: vi.fn(),
37
31
  onRejectTools: vi.fn(),
38
32
  ...overrides,
@@ -64,32 +58,10 @@ describe('AgentDrawerPanel composer and steering behavior', () => {
64
58
  })
65
59
  }
66
60
 
67
- it('keeps the composer textarea editable and replaces send with stop while busy', () => {
68
- render(baseProps({ busy: true, onStop: vi.fn() }))
69
-
70
- const textarea = host.querySelector('textarea')
71
- const stopButton = host.querySelector('button[aria-label="Stop"]')
72
- const sendButton = host.querySelector('button[aria-label="Send"]')
73
-
74
- expect(textarea).toBeInstanceOf(HTMLTextAreaElement)
75
- expect((textarea as HTMLTextAreaElement).disabled).toBe(false)
76
- expect(stopButton).toBeInstanceOf(HTMLButtonElement)
77
- expect((stopButton as HTMLButtonElement).disabled).toBe(false)
78
- expect(sendButton).toBeNull()
79
- })
80
-
81
- it('keeps the composer textarea editable when the run state is ready', () => {
82
- render(baseProps({ runState: AgentUiRunState.Ready }))
83
-
84
- const textarea = host.querySelector('textarea')
85
-
86
- expect(textarea).toBeInstanceOf(HTMLTextAreaElement)
87
- expect((textarea as HTMLTextAreaElement).disabled).toBe(false)
88
- })
89
-
90
- it('renders manual approval controls and composer together', () => {
61
+ it('renders manual approval controls above the composer slot', () => {
91
62
  render(
92
63
  baseProps({
64
+ composerSlot: <textarea aria-label="Composer" />,
93
65
  pendingToolCalls: [
94
66
  {
95
67
  id: 'call-1',
@@ -195,179 +167,4 @@ describe('AgentDrawerPanel composer and steering behavior', () => {
195
167
  expect(host.textContent).toContain('Prepared Summarize')
196
168
  })
197
169
 
198
- it('renders persisted follow-up controls above the composer surface', () => {
199
- const onRemoveFollowUp = vi.fn()
200
- const onSendFollowUpNow = vi.fn()
201
- render(
202
- baseProps({
203
- busy: true,
204
- followUps: [
205
- {
206
- id: 'followup-1',
207
- message: 'Look at the second chapter',
208
- position: 0,
209
- },
210
- ],
211
- onRemoveFollowUp,
212
- onSendFollowUpNow,
213
- }),
214
- )
215
-
216
- const sendNow = host.querySelector(
217
- 'button[aria-label="Send follow-up now"]',
218
- )
219
- const remove = host.querySelector('button[aria-label="Remove follow-up"]')
220
- const textarea = host.querySelector('textarea')
221
- const composerSurface = textarea?.parentElement
222
-
223
- expect(host.textContent).not.toContain('Queued follow-ups')
224
- expect(host.textContent).toContain('Look at the second chapter')
225
- expect(composerSurface?.textContent).not.toContain('Queued follow-ups')
226
- expect(composerSurface?.textContent).not.toContain(
227
- 'Look at the second chapter',
228
- )
229
- expect(sendNow).toBeInstanceOf(HTMLButtonElement)
230
- expect(remove).toBeInstanceOf(HTMLButtonElement)
231
-
232
- act(() => {
233
- sendNow?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
234
- remove?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
235
- })
236
-
237
- expect(onSendFollowUpNow).toHaveBeenCalledWith('followup-1')
238
- expect(onRemoveFollowUp).toHaveBeenCalledWith('followup-1')
239
- })
240
-
241
- it('keeps follow-up play available when the drawer is not busy', () => {
242
- const onSendFollowUpNow = vi.fn()
243
- render(
244
- baseProps({
245
- busy: false,
246
- followUps: [
247
- {
248
- id: 'followup-1',
249
- message: 'Run this next',
250
- position: 0,
251
- },
252
- ],
253
- onSendFollowUpNow,
254
- }),
255
- )
256
-
257
- const sendNow = host.querySelector(
258
- 'button[aria-label="Send follow-up now"]',
259
- )
260
-
261
- expect(sendNow).toBeInstanceOf(HTMLButtonElement)
262
- expect((sendNow as HTMLButtonElement).disabled).toBe(false)
263
-
264
- act(() => {
265
- sendNow?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
266
- })
267
-
268
- expect(onSendFollowUpNow).toHaveBeenCalledWith('followup-1')
269
- })
270
-
271
- it('renders question prompts in the composer without approval controls', async () => {
272
- const onAnswerQuestionTool = vi.fn()
273
- const onDeclineQuestionTool = vi.fn()
274
- render(
275
- baseProps({
276
- busy: true,
277
- pendingQuestionToolCalls: [
278
- {
279
- id: 'question-1',
280
- name: 'harness.questions',
281
- arguments: {
282
- question: 'Which source should I use?',
283
- mode: 'single_choice',
284
- options: [
285
- { id: 'notes', label: 'Notes' },
286
- { id: 'files', label: 'Files' },
287
- ],
288
- },
289
- },
290
- ],
291
- onAnswerQuestionTool,
292
- onDeclineQuestionTool,
293
- }),
294
- )
295
-
296
- const buttons = Array.from(host.querySelectorAll('button'))
297
- const notesButton = buttons.find(button => button.textContent === 'Notes')
298
- const answerButton = buttons.find(button => button.textContent === 'Answer')
299
- const approveButton = buttons.find(
300
- button => button.textContent === 'Approve',
301
- )
302
-
303
- expect(host.textContent).toContain('Which source should I use?')
304
- expect(approveButton).toBeUndefined()
305
- expect(notesButton).toBeInstanceOf(HTMLButtonElement)
306
- expect(notesButton?.disabled).toBe(false)
307
- expect(answerButton).toBeInstanceOf(HTMLButtonElement)
308
- expect(answerButton?.disabled).toBe(true)
309
-
310
- act(() => {
311
- notesButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
312
- })
313
-
314
- expect(answerButton?.disabled).toBe(false)
315
-
316
- await act(async () => {
317
- answerButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
318
- await Promise.resolve()
319
- })
320
-
321
- expect(onAnswerQuestionTool).toHaveBeenCalledWith('question-1', ['notes'])
322
-
323
- const declineInput = host.querySelector(
324
- 'input[aria-label="Decline reason"]',
325
- ) as HTMLInputElement | null
326
- const declineButton = buttons.find(
327
- button => button.textContent === 'Decline',
328
- )
329
- const setter = Object.getOwnPropertyDescriptor(
330
- HTMLInputElement.prototype,
331
- 'value',
332
- )?.set
333
-
334
- await act(async () => {
335
- setter?.call(declineInput, 'Need more context')
336
- declineInput?.dispatchEvent(new InputEvent('input', { bubbles: true }))
337
- })
338
- await act(async () => {
339
- declineButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
340
- await Promise.resolve()
341
- })
342
-
343
- expect(onDeclineQuestionTool).toHaveBeenCalledWith(
344
- 'question-1',
345
- 'Need more context',
346
- )
347
- })
348
-
349
- it('collapses question prompts with preserved exiting content', () => {
350
- const question = {
351
- id: 'question-1',
352
- name: 'harness.questions',
353
- arguments: {
354
- question: 'Which source should I use?',
355
- mode: 'single_choice',
356
- options: [{ id: 'notes', label: 'Notes' }],
357
- },
358
- }
359
-
360
- render(baseProps({ pendingQuestionToolCalls: [question] }))
361
-
362
- const openPresence = host.querySelector('[data-animation-direction="y"]')
363
- expect(openPresence).toBeInstanceOf(HTMLElement)
364
- expect(host.textContent).toContain('Which source should I use?')
365
-
366
- render(baseProps({ pendingQuestionToolCalls: [] }))
367
-
368
- const closingPresence = host.querySelector('[data-animation-direction="y"]')
369
- expect(closingPresence).toBeInstanceOf(HTMLElement)
370
- expect(closingPresence?.getAttribute('data-open')).toBe('false')
371
- expect(host.textContent).toContain('Which source should I use?')
372
- })
373
170
  })
@@ -1,5 +1,4 @@
1
1
  import { InlineBanner } from '../common/feedback/InlineBanner.js'
2
- import { AgentComposer } from './AgentComposer.js'
3
2
  import { AgentMessageList } from './AgentMessageList.js'
4
3
  import { AgentToolPendingList } from './AgentToolPendingList.js'
5
4
  import type { AgentDrawerPanelProps } from './agentTypes.js'
@@ -8,49 +7,31 @@ import {
8
7
  StyledAgentDrawer,
9
8
  StyledAgentFooter,
10
9
  StyledAgentThreadHost,
11
- StyledComposerFooterSpacer,
12
- StyledComposerReserveFooter,
13
10
  } from './agentPanelStyles.js'
14
11
 
15
12
  export function AgentDrawerPanel({
16
13
  messages,
17
14
  assistantLabel,
18
15
  pendingToolCalls,
19
- pendingQuestionToolCalls = [],
20
- busy,
21
16
  error,
22
- composerValue,
23
- composerDisabled = false,
24
- composerInputDisabled = false,
25
- composerSendDisabled = false,
26
- composerLeading,
27
- followUps = [],
17
+ composerSlot = null,
28
18
  activityHint = null,
29
- onComposerChange,
30
- onSend,
31
- onStop,
32
- onRemoveFollowUp,
33
- onSendFollowUpNow,
34
19
  onApproveTools,
35
20
  onRejectTools,
36
21
  onApproveOne,
37
22
  onRejectOne,
38
- onApproveAlwaysForSession,
39
- onAnswerQuestionTool,
40
- onDeclineQuestionTool,
23
+ approveAlwaysScopeLabel,
24
+ onApproveAlwaysForScope,
41
25
  storageHint = null,
42
- usage = null,
43
- liveTurn = null,
26
+ storageHintAction = null,
27
+ onStorageHintDismiss,
44
28
  pureAssistantTabActive = false,
45
- drawerOpen = true,
46
- composerPortaled = false,
47
29
  threadHeader = null,
30
+ bottomPanel = null,
48
31
  }: AgentDrawerPanelProps): React.ReactElement {
49
32
  const awaitingApproval = pendingToolCalls.length > 0
50
- const composerBlocked = composerDisabled
33
+ void pureAssistantTabActive
51
34
 
52
- const showFooterComposer = !composerPortaled
53
- const reserveComposerFooter = composerPortaled && drawerOpen
54
35
  const footerWork = (
55
36
  <>
56
37
  {awaitingApproval ? (
@@ -61,62 +42,42 @@ export function AgentDrawerPanel({
61
42
  onReject={onRejectTools}
62
43
  onApproveOne={onApproveOne}
63
44
  onRejectOne={onRejectOne}
64
- onApproveAlwaysForSession={onApproveAlwaysForSession}
45
+ approveAlwaysScopeLabel={approveAlwaysScopeLabel}
46
+ onApproveAlwaysForScope={onApproveAlwaysForScope}
65
47
  />
66
48
  ) : null}
67
49
  </>
68
50
  )
69
- const hasFooterWork = awaitingApproval
70
51
 
71
52
  return (
72
- <StyledAgentDrawer
73
- data-pure-assistant-tab-active={pureAssistantTabActive || undefined}
74
- >
53
+ <StyledAgentDrawer>
75
54
  <StyledAgentBody>
76
55
  {storageHint ? (
77
- <InlineBanner variant="warning">{storageHint}</InlineBanner>
56
+ <InlineBanner
57
+ variant="warning"
58
+ actions={storageHintAction}
59
+ onDismiss={onStorageHintDismiss}
60
+ >
61
+ {storageHint}
62
+ </InlineBanner>
78
63
  ) : null}
79
64
  {activityHint ? (
80
65
  <InlineBanner variant="warning">{activityHint}</InlineBanner>
81
66
  ) : null}
82
67
  {error ? <InlineBanner variant="danger">{error}</InlineBanner> : null}
68
+ {threadHeader}
83
69
  <StyledAgentThreadHost>
84
70
  <AgentMessageList
85
71
  messages={messages}
86
72
  assistantLabel={assistantLabel}
87
- threadHeader={threadHeader}
88
73
  />
89
74
  </StyledAgentThreadHost>
90
75
  </StyledAgentBody>
91
- {showFooterComposer ? (
92
- <StyledAgentFooter>
93
- {footerWork}
94
- <AgentComposer
95
- value={composerValue}
96
- busy={busy}
97
- disabled={composerBlocked}
98
- inputDisabled={composerInputDisabled}
99
- sendDisabled={composerSendDisabled}
100
- leading={composerLeading}
101
- followUps={followUps}
102
- questionToolCalls={pendingQuestionToolCalls}
103
- usage={usage}
104
- liveTurn={liveTurn}
105
- onChange={onComposerChange}
106
- onSend={onSend}
107
- onStop={onStop}
108
- onRemoveFollowUp={onRemoveFollowUp}
109
- onSendFollowUpNow={onSendFollowUpNow}
110
- onAnswerQuestionTool={onAnswerQuestionTool}
111
- onDeclineQuestionTool={onDeclineQuestionTool}
112
- />
113
- </StyledAgentFooter>
114
- ) : reserveComposerFooter || hasFooterWork ? (
115
- <StyledComposerReserveFooter aria-hidden={!hasFooterWork}>
116
- {footerWork}
117
- {reserveComposerFooter ? <StyledComposerFooterSpacer /> : null}
118
- </StyledComposerReserveFooter>
119
- ) : null}
76
+ {bottomPanel}
77
+ <StyledAgentFooter>
78
+ {footerWork}
79
+ {composerSlot}
80
+ </StyledAgentFooter>
120
81
  </StyledAgentDrawer>
121
82
  )
122
83
  }
@@ -8,16 +8,17 @@ import {
8
8
  } from '../common/chat/index.js'
9
9
  import { AgentThinkingBlock } from './AgentThinkingBlock.js'
10
10
  import {
11
- agentToolResultDetails,
12
- formatAgentToolLabel,
13
- formatAgentToolRowSummary,
14
- } from './agentToolDisplay.js'
15
- import {
16
- AgentLivePhase,
17
- type AgentDelegatedActivity,
18
- type AgentUiMessage,
19
- type AgentUiToolCall,
20
- } from './agentTypes.js'
11
+ agentToolResultDetails,
12
+ formatAgentToolLabel,
13
+ formatAgentToolRowSummary,
14
+ } from './agentToolDisplay.js'
15
+ import {
16
+ AgentLivePhase,
17
+ type AgentDelegatedActivity,
18
+ type AgentToolActivity,
19
+ type AgentUiMessage,
20
+ type AgentUiToolCall,
21
+ } from './agentTypes.js'
21
22
 
22
23
  export interface AgentMessageBubbleProps {
23
24
  message: AgentUiMessage
@@ -57,17 +58,17 @@ export function AgentMessageBubble({
57
58
  ) : null
58
59
  }
59
60
  />
60
- {message.toolCalls?.map(call => (
61
- <ChatToolRow
62
- key={call.id}
63
- icon={
64
- call.status === 'running' || call.delegatedActivity ? (
65
- <RunningIcon size={11} aria-hidden />
66
- ) : (
67
- <Wrench size={11} />
61
+ {message.toolCalls?.map(call => (
62
+ <ChatToolRow
63
+ key={call.id}
64
+ icon={
65
+ toolCallIsIncomplete(call) || call.delegatedActivity ? (
66
+ <RunningIcon size={11} aria-hidden />
67
+ ) : (
68
+ <Wrench size={11} />
68
69
  )
69
70
  }
70
- title={formatAgentToolLabel(call.name)}
71
+ title={formatToolRowTitle(call)}
71
72
  summary={formatToolRowSummary(call)}
72
73
  details={formatToolRowDetails(call)}
73
74
  />
@@ -97,21 +98,46 @@ export function AgentMessageBubble({
97
98
  }
98
99
  }
99
100
 
100
- function formatToolRowSummary(call: AgentUiToolCall): string {
101
- if (call.delegatedActivity) {
102
- return delegatedActivitySummary(call.delegatedActivity)
103
- }
101
+ function formatToolRowSummary(call: AgentUiToolCall): string {
102
+ if (call.serviceActivity) {
103
+ return toolActivitySummary(call.serviceActivity)
104
+ }
105
+ if (call.delegatedActivity) {
106
+ return delegatedActivitySummary(call.delegatedActivity)
107
+ }
104
108
  return formatAgentToolRowSummary(call)
105
- }
106
-
107
- function formatToolRowDetails(call: AgentUiToolCall): string[] | undefined {
108
- if (call.delegatedActivity) {
109
- return delegatedActivityDetails(call.delegatedActivity)
110
- }
111
- return call.result ? agentToolResultDetails(call.result) : undefined
112
- }
113
-
114
- function delegatedActivityStatus(activity: AgentDelegatedActivity): string {
109
+ }
110
+
111
+ function formatToolRowTitle(call: AgentUiToolCall): string {
112
+ if (call.name === 'harness.request_app_development_iteration') {
113
+ return call.arguments.mode === 'ask'
114
+ ? 'Asking app builder'
115
+ : 'Requesting app changes'
116
+ }
117
+ return formatAgentToolLabel(call.name)
118
+ }
119
+
120
+ function formatToolRowDetails(call: AgentUiToolCall): string[] | undefined {
121
+ if (call.serviceActivity) {
122
+ return call.serviceActivity.detail ? [call.serviceActivity.detail] : undefined
123
+ }
124
+ if (call.delegatedActivity) {
125
+ return delegatedActivityDetails(call.delegatedActivity)
126
+ }
127
+ return call.result ? agentToolResultDetails(call.result) : undefined
128
+ }
129
+
130
+ function toolCallIsIncomplete(call: AgentUiToolCall): boolean {
131
+ return call.status !== 'done' && call.result === undefined
132
+ }
133
+
134
+ function toolActivitySummary(activity: AgentToolActivity): string {
135
+ return activity.detail
136
+ ? `${activity.title}: ${activity.detail}`
137
+ : activity.title
138
+ }
139
+
140
+ function delegatedActivityStatus(activity: AgentDelegatedActivity): string {
115
141
  switch (activity.liveTurn.phase) {
116
142
  case AgentLivePhase.Thinking:
117
143
  return 'starting'
@@ -14,20 +14,19 @@ export interface AgentMessageListProps {
14
14
  messages: AgentUiMessage[]
15
15
  assistantLabel?: string
16
16
  emptyMessage?: string
17
- threadHeader?: React.ReactNode
17
+ threadFooter?: React.ReactNode
18
18
  }
19
19
 
20
20
  export const AgentMessageList = memo(function AgentMessageList({
21
21
  messages,
22
22
  assistantLabel = 'assistant',
23
23
  emptyMessage = 'Ask the agent about your workspace. Tool calls wait for your approval.',
24
- threadHeader = null,
24
+ threadFooter = null,
25
25
  }: AgentMessageListProps): React.ReactElement {
26
26
  const scrollKey = messages.map(message => message.id).join('|')
27
27
 
28
28
  return (
29
- <StyledAgentScroll data-thread-header={threadHeader ? '' : undefined}>
30
- {threadHeader}
29
+ <StyledAgentScroll>
31
30
  <ChatThread
32
31
  scrollKey={scrollKey}
33
32
  emptyMessage={
@@ -35,6 +34,7 @@ export const AgentMessageList = memo(function AgentMessageList({
35
34
  <ChatThreadEmpty>{emptyMessage}</ChatThreadEmpty>
36
35
  ) : null
37
36
  }
37
+ footer={threadFooter}
38
38
  >
39
39
  {messages.map(message => (
40
40
  <Fragment key={message.id}>