@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
@@ -0,0 +1,278 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { styled } from 'styled-components'
3
+ import { Button } from '../common/buttons/Button.js'
4
+ import { FlexCol, FlexRow } from '../common/containers/flex.js'
5
+ import { Text } from '../common/typography/Text.js'
6
+
7
+ export interface QuestionOptionView {
8
+ value: string
9
+ label: string
10
+ description?: string
11
+ }
12
+
13
+ export interface QuestionView {
14
+ id: string
15
+ question: string
16
+ multiple?: boolean
17
+ custom?: boolean
18
+ options: QuestionOptionView[]
19
+ }
20
+
21
+ export interface QuestionRequestView {
22
+ id: string
23
+ questions: QuestionView[]
24
+ rejectRequiresReason?: boolean
25
+ }
26
+
27
+ export interface QuestionRequestListProps {
28
+ requests: QuestionRequestView[]
29
+ onAnswer?: (requestId: string, answers: string[][]) => void | Promise<void>
30
+ onReject?: (requestId: string, reason?: string) => void | Promise<void>
31
+ }
32
+
33
+ const StyledList = styled(FlexCol)`
34
+ gap: 6px;
35
+ `
36
+
37
+ const StyledPrompt = styled(FlexCol)`
38
+ gap: var(--platform-spacing-sm);
39
+ padding: 10px;
40
+ border: 1px solid var(--platform-colors-border);
41
+ border-radius: var(--platform-radius-sm);
42
+ background: var(--platform-colors-bg);
43
+ `
44
+
45
+ const StyledQuestion = styled(Text)`
46
+ line-height: 1.35;
47
+ `
48
+
49
+ const StyledOptionGrid = styled.div`
50
+ display: grid;
51
+ gap: 6px;
52
+ `
53
+
54
+ const StyledOptionButton = styled.button<{ $selected: boolean }>`
55
+ display: grid;
56
+ gap: 2px;
57
+ width: 100%;
58
+ padding: 8px 10px;
59
+ border: 1px solid
60
+ ${({ $selected }) =>
61
+ $selected
62
+ ? 'var(--platform-colors-accent)'
63
+ : 'var(--platform-colors-border)'};
64
+ border-radius: var(--platform-radius-sm);
65
+ background: ${({ $selected }) =>
66
+ $selected
67
+ ? 'color-mix(in srgb, var(--platform-colors-accent) 10%, var(--platform-colors-bg))'
68
+ : 'var(--platform-colors-surface)'};
69
+ color: var(--platform-colors-text);
70
+ text-align: left;
71
+ cursor: pointer;
72
+ transition: background var(--platform-transition-fast),
73
+ border-color var(--platform-transition-fast);
74
+
75
+ &:hover:not(:disabled) {
76
+ background: var(--platform-colors-surface-hover);
77
+ }
78
+
79
+ &:focus {
80
+ outline: none;
81
+ }
82
+
83
+ &:focus-visible {
84
+ outline: 1px solid var(--platform-colors-border-strong);
85
+ outline-offset: 2px;
86
+ }
87
+
88
+ &:disabled {
89
+ opacity: 0.55;
90
+ cursor: not-allowed;
91
+ }
92
+ `
93
+
94
+ const StyledInput = styled.input`
95
+ min-width: 0;
96
+ flex: 1 1 auto;
97
+ height: 28px;
98
+ padding: 0 9px;
99
+ border: 1px solid var(--platform-colors-border);
100
+ border-radius: var(--platform-radius-sm);
101
+ background: var(--platform-colors-surface);
102
+ color: var(--platform-colors-text);
103
+ font: inherit;
104
+ font-size: var(--platform-typography-font-size-sm);
105
+
106
+ &:focus {
107
+ outline: none;
108
+ border-color: var(--platform-colors-accent);
109
+ }
110
+ `
111
+
112
+ function QuestionRequest({
113
+ request,
114
+ onAnswer,
115
+ onReject,
116
+ }: {
117
+ request: QuestionRequestView
118
+ onAnswer?: QuestionRequestListProps['onAnswer']
119
+ onReject?: QuestionRequestListProps['onReject']
120
+ }): React.ReactElement {
121
+ const [answers, setAnswers] = useState<string[][]>([])
122
+ const [rejectReason, setRejectReason] = useState('')
123
+ const [submitting, setSubmitting] = useState(false)
124
+
125
+ useEffect(() => {
126
+ setAnswers(request.questions.map(() => []))
127
+ setRejectReason('')
128
+ setSubmitting(false)
129
+ }, [request.id, request.questions.length])
130
+
131
+ const setQuestionAnswer = (index: number, value: string): void => {
132
+ const question = request.questions[index]
133
+ setAnswers(current => {
134
+ const next = [...current]
135
+ const selected = next[index] ?? []
136
+ next[index] = question.multiple
137
+ ? selected.includes(value)
138
+ ? selected.filter(item => item !== value)
139
+ : [...selected, value]
140
+ : [value]
141
+ return next
142
+ })
143
+ }
144
+
145
+ const submitAnswer = async (): Promise<void> => {
146
+ if (!onAnswer || submitting || !answers.every(answer => answer.length)) {
147
+ return
148
+ }
149
+ setSubmitting(true)
150
+ try {
151
+ await onAnswer(request.id, answers)
152
+ } finally {
153
+ setSubmitting(false)
154
+ }
155
+ }
156
+
157
+ const submitReject = async (): Promise<void> => {
158
+ if (!onReject || submitting) return
159
+ const reason = rejectReason.trim()
160
+ if (request.rejectRequiresReason && !reason) return
161
+ setSubmitting(true)
162
+ try {
163
+ await onReject(request.id, reason || undefined)
164
+ } finally {
165
+ setSubmitting(false)
166
+ }
167
+ }
168
+
169
+ const canAnswer =
170
+ Boolean(onAnswer) && answers.length > 0 && answers.every(answer => answer.length)
171
+ const canReject =
172
+ Boolean(onReject) &&
173
+ (!request.rejectRequiresReason || rejectReason.trim().length > 0)
174
+
175
+ return (
176
+ <StyledPrompt>
177
+ {request.questions.map((question, index) => (
178
+ <FlexCol key={question.id} $gap="sm">
179
+ <StyledQuestion size="sm" weight="medium">
180
+ {question.question}
181
+ </StyledQuestion>
182
+ <StyledOptionGrid>
183
+ {question.options.map(option => {
184
+ const selected = answers[index]?.includes(option.value) ?? false
185
+ return (
186
+ <StyledOptionButton
187
+ key={option.value}
188
+ type="button"
189
+ disabled={submitting}
190
+ $selected={selected}
191
+ aria-pressed={selected}
192
+ onClick={() => setQuestionAnswer(index, option.value)}
193
+ >
194
+ <Text size="sm" weight="medium">
195
+ {option.label}
196
+ </Text>
197
+ {option.description ? (
198
+ <Text size="sm" tone="secondary">
199
+ {option.description}
200
+ </Text>
201
+ ) : null}
202
+ </StyledOptionButton>
203
+ )
204
+ })}
205
+ </StyledOptionGrid>
206
+ {question.custom ? (
207
+ <StyledInput
208
+ value={answers[index]?.[0] ?? ''}
209
+ disabled={submitting}
210
+ placeholder="Custom answer"
211
+ aria-label="Custom answer"
212
+ onChange={event => {
213
+ const value = event.target.value
214
+ setAnswers(current => {
215
+ const next = [...current]
216
+ next[index] = value.trim() ? [value] : []
217
+ return next
218
+ })
219
+ }}
220
+ />
221
+ ) : null}
222
+ </FlexCol>
223
+ ))}
224
+ <FlexRow $align="center" $justify="between" $gap="sm">
225
+ {request.rejectRequiresReason ? (
226
+ <StyledInput
227
+ value={rejectReason}
228
+ disabled={submitting}
229
+ placeholder="Decline reason"
230
+ aria-label="Decline reason"
231
+ onChange={event => setRejectReason(event.target.value)}
232
+ />
233
+ ) : null}
234
+ {onReject ? (
235
+ <Button
236
+ size="sm"
237
+ variant="subtle"
238
+ disabled={!canReject || submitting}
239
+ loading={submitting}
240
+ onClick={() => void submitReject()}
241
+ >
242
+ Decline
243
+ </Button>
244
+ ) : null}
245
+ <Button
246
+ size="sm"
247
+ variant="primary"
248
+ disabled={!canAnswer || submitting}
249
+ loading={submitting}
250
+ onClick={() => void submitAnswer()}
251
+ >
252
+ Answer
253
+ </Button>
254
+ </FlexRow>
255
+ </StyledPrompt>
256
+ )
257
+ }
258
+
259
+ export function QuestionRequestList({
260
+ requests,
261
+ onAnswer,
262
+ onReject,
263
+ }: QuestionRequestListProps): React.ReactElement | null {
264
+ if (requests.length === 0) return null
265
+
266
+ return (
267
+ <StyledList aria-label="Questions from agent">
268
+ {requests.map(request => (
269
+ <QuestionRequest
270
+ key={request.id}
271
+ request={request}
272
+ onAnswer={onAnswer}
273
+ onReject={onReject}
274
+ />
275
+ ))}
276
+ </StyledList>
277
+ )
278
+ }
@@ -4,7 +4,10 @@ import { FlexCol, FlexRow } from '../common/containers/flex.js'
4
4
  export const StyledAgentDrawer = styled(FlexCol)`
5
5
  flex: 1 1 auto;
6
6
  min-height: 0;
7
+ min-width: 0;
7
8
  width: 100%;
9
+ box-sizing: border-box;
10
+ overflow: hidden;
8
11
  background: var(--platform-colors-app-viewport);
9
12
  margin: 0;
10
13
  `
@@ -24,15 +27,14 @@ export const StyledAgentThreadHost = styled.div`
24
27
  `
25
28
 
26
29
  export const StyledAgentScroll = styled.div`
27
- position: relative;
30
+ flex: 1 1 auto;
28
31
  min-height: 0;
32
+ min-width: 0;
33
+ width: 100%;
34
+ box-sizing: border-box;
35
+ overflow: hidden;
29
36
  display: flex;
30
37
  flex-direction: column;
31
- --platform-agent-thread-header-height: 0px;
32
-
33
- &[data-thread-header] {
34
- --platform-agent-thread-header-height: 34px;
35
- }
36
38
  `
37
39
 
38
40
  export const StyledAgentFooter = styled.div`
@@ -44,31 +46,6 @@ export const StyledAgentFooter = styled.div`
44
46
  background: var(--platform-colors-surface);
45
47
  `
46
48
 
47
- /** Delay footer reserve collapse so the portaled chatbox can lead the motion. */
48
- export const StyledComposerReserveFooter = styled(StyledAgentFooter)`
49
- overflow: hidden;
50
- gap: var(--platform-spacing-sm);
51
- padding-block: 8px 12px;
52
- border-top: 1px solid var(--platform-colors-border);
53
- transition: padding-block 0.5s linear, gap 0.5s linear,
54
- border-color 0.5s linear;
55
-
56
- [data-pure-assistant-tab-active='true'] & {
57
- gap: 0;
58
- padding-block: 0;
59
- border-top-color: transparent;
60
- }
61
- `
62
-
63
- export const StyledComposerFooterSpacer = styled.div`
64
- min-height: 112px;
65
- transition: min-height 0.5s linear;
66
-
67
- [data-pure-assistant-tab-active='true'] & {
68
- min-height: 0;
69
- }
70
- `
71
-
72
49
  export const StyledToolCard = styled.div`
73
50
  display: flex;
74
51
  flex-direction: column;
@@ -12,14 +12,22 @@ export type AgentUiRunState =
12
12
 
13
13
  export type AgentUiMessageRole = 'user' | 'assistant' | 'tool'
14
14
 
15
- export interface AgentUiToolCall {
16
- id: string
17
- name: string
18
- arguments: Record<string, unknown>
19
- result?: string
20
- status?: 'running' | 'done'
21
- delegatedActivity?: AgentDelegatedActivity
22
- }
15
+ export interface AgentUiToolCall {
16
+ id: string
17
+ name: string
18
+ arguments: Record<string, unknown>
19
+ result?: string
20
+ status?: 'running' | 'done'
21
+ delegatedActivity?: AgentDelegatedActivity
22
+ serviceActivity?: AgentToolActivity
23
+ }
24
+
25
+ export interface AgentToolActivity {
26
+ detail?: string
27
+ id: string
28
+ status: 'done' | 'failed' | 'info' | 'running'
29
+ title: string
30
+ }
23
31
 
24
32
  export const AgentLivePhase = {
25
33
  Idle: 'idle',
@@ -92,48 +100,24 @@ export interface AgentUiRunUsage {
92
100
  export interface AgentDrawerPanelProps {
93
101
  messages: AgentUiMessage[]
94
102
  assistantLabel?: string
95
- liveTurn?: AgentLiveTurn | null
96
103
  pendingToolCalls: AgentUiToolCall[]
97
- pendingQuestionToolCalls?: AgentUiToolCall[]
98
- runState: AgentUiRunState | null
99
- busy: boolean
100
104
  error: string | null
101
- composerValue: string
102
- composerDisabled?: boolean
103
- composerInputDisabled?: boolean
104
- composerSendDisabled?: boolean
105
- composerLeading?: React.ReactNode
106
- followUps?: AgentUiFollowUp[]
105
+ composerSlot?: React.ReactNode
107
106
  activityHint?: string | null
108
- onComposerChange: (value: string) => void
109
- onSend: () => void
110
- onStop?: () => void
111
- onRemoveFollowUp?: (followUpId: string) => void
112
- onSendFollowUpNow?: (followUpId: string) => void
113
107
  onApproveTools: () => void
114
108
  onRejectTools: () => void
115
109
  onApproveOne?: (toolCallId: string) => void
116
110
  onRejectOne?: (toolCallId: string) => void
117
- onApproveAlwaysForSession?: (
118
- qualifiedName: string,
119
- toolCallId: string,
120
- ) => void
121
- onAnswerQuestionTool?: (
122
- toolCallId: string,
123
- selectedOptionIds: readonly string[],
124
- ) => void | Promise<void>
125
- onDeclineQuestionTool?: (
126
- toolCallId: string,
127
- declineReason: string,
128
- ) => void | Promise<void>
111
+ /** Label for the standing-approval option ("Always approve for Writer"). */
112
+ approveAlwaysScopeLabel?: string
113
+ onApproveAlwaysForScope?: (qualifiedName: string, toolCallId: string) => void
129
114
  storageHint?: string | null
130
- usage?: AgentUiRunUsage | null
115
+ storageHintAction?: React.ReactNode
116
+ onStorageHintDismiss?: () => void
131
117
  /** Pure Assistant workspace tab is active — shell sets this; UI leaves layout to the host. */
132
118
  pureAssistantTabActive?: boolean
133
- /** Agent drawer panel is open (including forced-open on Pure Assistant). */
134
- drawerOpen?: boolean
135
- /** Composer is portaled by the shell — drawer keeps a footer spacer only. */
136
- composerPortaled?: boolean
137
- /** Shell thread chrome (scope, session actions) — anchored to the message column. */
119
+ /** Shell thread chrome rendered above the scrollable message column. */
138
120
  threadHeader?: React.ReactNode
121
+ /** Shell-owned content rendered between the message thread and interaction footer. */
122
+ bottomPanel?: React.ReactNode
139
123
  }
@@ -3,6 +3,10 @@ export { AgentMessageList, formatAssistantLabel } from './AgentMessageList.js'
3
3
  export { AgentMessageBubble } from './AgentMessageBubble.js'
4
4
  export { AgentToolPendingList } from './AgentToolPendingList.js'
5
5
  export { AgentComposer } from './AgentComposer.js'
6
+ export {
7
+ QuestionRequestList,
8
+ type QuestionRequestView,
9
+ } from './QuestionRequestList.js'
6
10
  export {
7
11
  AgentContextUsageBar,
8
12
  formatAgentTokenCount,
@@ -12,20 +12,26 @@ const StyledList = styled.div`
12
12
  flex: 1;
13
13
  min-height: 0;
14
14
  overflow: auto;
15
- padding: 14px 24px 24px;
15
+ /* No top padding: it's inside the scrollport, so a sticky header pinned at
16
+ top:0 would leave/scroll a gap above it (the "jumps down / white space"). */
17
+ padding: 0 24px 24px;
16
18
  `
17
19
 
18
20
  const StyledSectionHeading = styled.div`
19
- padding: var(--platform-spacing-sm) 2px var(--platform-spacing-xs);
21
+ /* Full-bleed opaque bar: negative inline margin + matching padding makes the
22
+ background span the whole sidebar so rows scroll fully under it (they used
23
+ to overrun/bleed through the label); text stays aligned with the rows. */
24
+ margin: 0 -24px;
25
+ padding: var(--platform-spacing-sm) 24px var(--platform-spacing-xs);
20
26
  font-size: var(--platform-typography-font-size-xs);
21
27
  font-weight: var(--platform-typography-font-weight-medium);
22
28
  letter-spacing: 0.04em;
23
29
  text-transform: uppercase;
24
30
  color: var(--platform-colors-text-secondary);
25
- background: transparent;
31
+ background: var(--platform-colors-surface);
26
32
  position: sticky;
27
33
  top: 0;
28
- z-index: 1;
34
+ z-index: 2;
29
35
  `
30
36
 
31
37
  const StyledEmptyList = styled.div`
@@ -28,7 +28,7 @@ const StyledScroll = styled.div`
28
28
  flex: 1;
29
29
  min-width: 0;
30
30
  gap: 2px;
31
- padding: 5px 8px 0;
31
+ padding: 5px 10px 0 12px;
32
32
  overflow-x: auto;
33
33
  overflow-y: hidden;
34
34
  scrollbar-width: thin;
@@ -46,7 +46,7 @@ const StyledTab = styled.button<{
46
46
  height: 31px;
47
47
  max-width: 260px;
48
48
  margin-bottom: -1px;
49
- padding: 0 8px 0 9px;
49
+ padding: 0 8px 0 12px;
50
50
  border: 1px solid transparent;
51
51
  border-bottom-color: transparent;
52
52
  border-radius: 11px 11px 0 0;
@@ -120,7 +120,8 @@ const StyledLabel = styled.span`
120
120
 
121
121
  const StyledTabWordmark = styled(PureAppWordmark)`
122
122
  min-width: 0;
123
- overflow: hidden;
123
+ padding-left: 1px;
124
+ overflow: visible;
124
125
 
125
126
  > span:last-child {
126
127
  overflow: hidden;
@@ -169,6 +170,20 @@ const StyledDocName = styled.span`
169
170
  color: var(--pure-chrome-tertiary, #8a857c);
170
171
  `
171
172
 
173
+ const StyledDevBadge = styled.span`
174
+ display: inline-flex;
175
+ align-items: center;
176
+ justify-content: center;
177
+ flex-shrink: 0;
178
+ font-size: 9px;
179
+ font-weight: 600;
180
+ line-height: 1;
181
+ letter-spacing: 0.04em;
182
+ text-transform: uppercase;
183
+ color: var(--pure-chrome-tertiary, #8a857c);
184
+ opacity: 0.72;
185
+ `
186
+
172
187
  const StyledClose = styled(X)`
173
188
  cursor: pointer;
174
189
  opacity: 0.7;
@@ -226,7 +241,7 @@ export function WorkspaceTabStrip({
226
241
  <StyledScroll role="tablist" aria-label={ariaLabel}>
227
242
  {tabs.map((tab, index) => {
228
243
  const active = tab.id === activeTabId
229
- const closable = !tab.pinned && onClose
244
+ const closable = onClose
230
245
  return (
231
246
  <StyledTab
232
247
  key={tab.id}
@@ -306,6 +321,9 @@ export function WorkspaceTabStrip({
306
321
  tab.label
307
322
  )}
308
323
  </StyledLabel>
324
+ {tab.underDevelopment ? (
325
+ <StyledDevBadge title="Under development">dev</StyledDevBadge>
326
+ ) : null}
309
327
  {closable ? (
310
328
  <StyledClose
311
329
  type="button"
@@ -15,6 +15,8 @@ export interface WorkspaceTabItem {
15
15
  letter: string
16
16
  label?: string
17
17
  }
18
+ /** True while the tab's app is owned by App Builder's dev lifecycle. */
19
+ underDevelopment?: boolean
18
20
  pinned?: boolean
19
21
  }
20
22
 
@@ -4,10 +4,10 @@ import { Send, Square } from 'lucide-react'
4
4
  import { FlexCol } from '../containers/flex.js'
5
5
  import { FlexRow } from '../containers/flex.js'
6
6
 
7
- export interface ChatBoxProps {
8
- value: string
9
- onChange: (value: string) => void
10
- onSend: () => void
7
+ export interface ChatBoxProps {
8
+ value: string
9
+ onChange: (value: string) => void
10
+ onSend: () => void
11
11
  onStop?: () => void
12
12
  sending?: boolean
13
13
  disabled?: boolean
@@ -22,10 +22,10 @@ export interface ChatBoxProps {
22
22
 
23
23
  export const ChatBox = forwardRef<HTMLTextAreaElement, ChatBoxProps>(
24
24
  function ChatBox(
25
- {
26
- value,
27
- onChange,
28
- onSend,
25
+ {
26
+ value,
27
+ onChange,
28
+ onSend,
29
29
  onStop,
30
30
  sending = false,
31
31
  disabled = false,
@@ -54,7 +54,7 @@ export const ChatBox = forwardRef<HTMLTextAreaElement, ChatBoxProps>(
54
54
  const showStop = sending && onStop
55
55
 
56
56
  return (
57
- <Root $gap="xs">
57
+ <Root $gap="xs">
58
58
  <EditorWrapper>
59
59
  {topContent ?? null}
60
60
  <Textarea
@@ -2,6 +2,7 @@ import { ChevronDown } from 'lucide-react'
2
2
  import { Fragment, useMemo } from 'react'
3
3
  import { styled } from 'styled-components'
4
4
  import { PlatformIcon } from '../../chrome/PlatformIcon.js'
5
+ import { CredentialProviderIcon } from '../../credentials/credentialProviderIcon.js'
5
6
  import { SelectMenuControl } from '../dropdown/SelectMenuControl.js'
6
7
  import type { SelectMenuPlacement } from '../dropdown/SelectMenu.js'
7
8
  import { useSelectMenuAnchor } from '../dropdown/useSelectMenuAnchor.js'
@@ -11,6 +12,7 @@ export interface ChatModelMenuOption {
11
12
  label: string
12
13
  groupLabel?: string
13
14
  meta?: string
15
+ provider?: string
14
16
  disabled?: boolean
15
17
  contextWindowTokens?: number
16
18
  supportsImageInput?: boolean
@@ -175,6 +177,12 @@ export function ChatModelMenu({
175
177
  title={triggerLabel ?? current?.label ?? ariaLabel}
176
178
  onClick={toggle}
177
179
  >
180
+ {current?.provider ? (
181
+ <CredentialProviderIcon
182
+ credentialId={`${current.provider}.apiKey`}
183
+ label={current.provider}
184
+ />
185
+ ) : null}
178
186
  <StyledLabel>{triggerLabel ?? current?.label ?? 'Model'}</StyledLabel>
179
187
  <PlatformIcon icon={ChevronDown} size={12} strokeWidth={1.75} />
180
188
  </StyledTrigger>
@@ -234,10 +242,20 @@ function ModelMenuOptions({
234
242
  onClose()
235
243
  }}
236
244
  >
237
- <StyledOptionLabel>{option.label}</StyledOptionLabel>
238
- {option.meta ? (
239
- <StyledOptionMeta>{option.meta}</StyledOptionMeta>
240
- ) : null}
245
+ <StyledOptionContent>
246
+ {option.provider ? (
247
+ <CredentialProviderIcon
248
+ credentialId={`${option.provider}.apiKey`}
249
+ label={option.provider}
250
+ />
251
+ ) : null}
252
+ <StyledOptionText>
253
+ <StyledOptionLabel>{option.label}</StyledOptionLabel>
254
+ {option.meta ? (
255
+ <StyledOptionMeta>{option.meta}</StyledOptionMeta>
256
+ ) : null}
257
+ </StyledOptionText>
258
+ </StyledOptionContent>
241
259
  </StyledOption>
242
260
  </Fragment>
243
261
  )
@@ -269,6 +287,18 @@ const StyledOption = styled.button<{ $selected?: boolean }>`
269
287
  }
270
288
  `
271
289
 
290
+ const StyledOptionContent = styled.span`
291
+ display: flex;
292
+ align-items: flex-start;
293
+ gap: 7px;
294
+ `
295
+
296
+ const StyledOptionText = styled.span`
297
+ display: flex;
298
+ flex-direction: column;
299
+ gap: 2px;
300
+ `
301
+
272
302
  const StyledOptionLabel = styled.span`
273
303
  font-size: var(--platform-typography-font-size-sm);
274
304
  line-height: 1.25;