Skip to content

Commit d21b2ad

Browse files
committed
feat(platform): refine integration ideas and shared control behavior
1 parent 9b6c3ab commit d21b2ad

53 files changed

Lines changed: 456 additions & 322 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/sim/platform/AGENTS.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,18 @@ Use `ControlRow` for the sidebar, identity controls, panel tabs, recent links, w
2929

3030
Use `Stack` for compact people or integration artwork: show at most three items, a passive `+N` overflow count, and each item’s name on hover. Counts remain intrinsic-width and use compact notation from one thousand; never replace them with an ellipsis action. Each hovered item shifts left by the shared inset without moving its siblings. Use the shared motion duration and respect reduced motion. `AvatarGroup` supplies people to the same stack for integration usage and calendar attendees.
3131

32-
Use `ActivityRow` for a first-line glyph, avatar or artwork stack, title/detail and centered trailing metadata. Its trailing metadata is either text or people, never both; independent actions remain siblings of its link or button. Use the chart, calendar, composer or attachment owner when those parts have a different interaction. See the [component ownership map](docs/component-ownership.md) before adding another family.
32+
Use `ActivityRow` for a first-line glyph, avatar or artwork stack, title/detail and centered trailing metadata. Its trailing metadata is either text or people, never both. A navigating row ends at its chevron and has no sibling action or ellipsis. Put record actions in its detail; a passive row with independent controls uses ActivityBody and has no whole-row hover or click target. Use the chart, calendar, composer or attachment owner when those parts have a different interaction. See the [component ownership map](docs/component-ownership.md) before adding another family.
3333

34-
Inputs and textareas share the neutral field frame, resting border, 8px text inset and stable frame-owned hover. Focus alone preserves resting paint. Required placeholders describe the expected input without replacing the label. `viewOnly` supplies native read-only behavior and the shared copy action in the sidebar’s 24px trailing slot; copy feedback remains visible for four seconds. Invite recipient fields share the textarea’s 76px default height and insets. Quiet `SearchField` is the explicit focus exception: its reserved underline appears on hover or focus while the background remains unchanged. Counts inherit adjacent label metrics and align on the text baseline.
34+
Inputs and textareas share the neutral field frame, resting border and 8px text inset. Hover changes only the frame border; the fill remains unchanged. Focus alone preserves resting paint. Required placeholders describe the expected input without replacing the label. `viewOnly` supplies native read-only behavior and the shared copy action in the sidebar’s 24px trailing slot; copy feedback remains visible for four seconds. Read-only fields show a not-allowed cursor without dimming, while their copy action remains interactive. TextArea defaultSize selects regular (76px), expanded (112px) or large (240px); skill instruction editors use large. Invite recipient fields share the regular textarea height and insets. Quiet `SearchField` is the explicit focus exception: its subtle underline starts at the typing edge, excluding the leading icon and trailing controls, and appears on hover or focus while the background remains unchanged. Counts inherit adjacent label metrics, use ordinary inline spacing and align on the text baseline. Disabled dropdowns show a not-allowed cursor.
3535

3636
Toolbar choices use `ChoiceMenu`, the same compact quiet button and menu as Home's activity period. A toolbar action uses the existing quiet compact Button or IconButton. Page-level actions live in the control bar beneath the title and description; an action for a particular record belongs with that record. `Select` and `SelectField` provide field chrome for forms and settings; do not place a full-width field picker in a control bar or invent a toolbar variant to disguise it. Reuse the existing owner for the interaction and compare its actual trigger with Home before accepting it.
3737

3838
Measure the actual scroll viewport with `ScrollArea`, or attach `useScrollEdges` directly to a native or Base UI scroll owner. Use the shared 16px mask only on edges with more content, updating after scroll, resize, input, reset or filtering. Platform and Settings navigation hide the native scrollbar and keep their edge fades, keyboard, wheel and touch scrolling. Other scroll areas keep their existing scrollbar behavior. Insets establish resting layout; they cannot substitute for an edge fade. Home list widgets and notification panels use `--ui-widget-max-size` as a ceiling, not a forced height. The Home activity chart grows with its content and has no nested scroll viewport. Keep the dashboard free to scroll and let each overflowing list or schedule scroll independently beneath its stationary heading. Shared ScrollArea viewports and the Home calendar suppress rubber-band overscroll at their boundaries. Plain tooltips preview at most three lines; retain the complete accessible text. A structured people popover owns its content and uses the dropdown’s popup padding and row insets.
3939

4040
Form dialogs use `Modal` or `MultiStepModal`. Pass a title, content, action labels and handlers. A confirm handler that returns a promise holds the dialog open as pending, closes it on success, and keeps the draft with an inline message on failure; nothing dismisses it while pending. Returning `false` keeps it open at the dialog's next stage, and `onBack` turns the quiet footer action into a step back. The shared borderless surface positions its title at the logical start and supplies one quiet Cancel/Back. Command search and previews use the same shadow and dismissal layer. Form controls cannot construct or style a form header, footer or Cancel button. Opening a dialog or advancing its step focuses the first available field. Confirmation dialogs focus Cancel, and read-only previews focus Close. A nested dialog handles Escape one layer at a time. While a form is open, the global search shortcut leaves its draft and keyboard focus in place. Closing returns focus to the invoking control. Textareas scroll or grow within their owner and never show a manual resize handle. Recipient fields use the textarea’s height, draft typography and inset. Committed addresses use compact caption text and an always-visible neutral ×, like a panel tab. The remove button sits inside the tag beside its label; long addresses yield space to the button. Comma or Enter commits a draft, pasting a list commits its addresses, and Backspace from an empty draft removes the last tag.
4141

42+
MessageBubble owns the normal user-message surface and selectable integration ideas. Idea messages sit centered over full-width artwork; selecting one appends to the Home draft without submitting or discarding existing text or attachments.
43+
4244
Use `useClipboard` / `CopyButton` for successful copy feedback and `useFeedbackExpiry` for temporary status. Copied indicators and transient notices dwell for four seconds; repeated events restart the timer. Pause an actionable notice during hover or focus and keep its action available in the stack afterward. Icon color changes use `--ui-motion-color` (120ms, zero under reduced motion). Do not put literal timers or CSS durations in product components.
4345

4446
Opening a side-by-side panel keeps the chat title at the same inline and block position. A full-width panel takes over the reading surface until the reader returns to chat. Run disclosure controls thinking and tool activity. Assistant prose stays visible when activity collapses. Each chat message owns a continuous hover area from its content through its action row. Its timestamp and actions stay available when navigating by keyboard and on touch. Chat thinking, tool summaries and prose paragraphs share one 12px block gap. Expanded payloads have equal block padding; the final message keeps a section of clearance from the composer. Render pasted text, files and images with ControlRow and a sibling remove action. Tool summaries use one body-size label with quieter ink. A run divider, expanded tool rail and split-panel edge are structural rules with one shared hairline role. Ordinary list items remain separated by gap alone. Panel tabs, action controls and key values have their own semantics; the same CopyButton copies message text and either tool payload. Composer and jump-to-latest share their border, background and shadow; leave space between them.

‎apps/sim/platform/app/(shell)/_components/chat/message/message.module.css‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
gap: var(--ui-section-gap);
88
}
99
.answer p,
10-
.bubble,
1110
.result code {
1211
white-space: pre-wrap;
1312
overflow-wrap: anywhere;
@@ -20,11 +19,6 @@
2019
justify-items: end;
2120
max-inline-size: min(100%, var(--container-chat-message));
2221
}
23-
.bubble {
24-
background: var(--control-avatar);
25-
border-radius: var(--radius-dialog);
26-
padding: var(--spacing-12) var(--spacing-16);
27-
}
2822
.responseActions,
2923
.userActions,
3024
.result dt,

‎apps/sim/platform/app/(shell)/_components/chat/message/response-actions.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export function ResponseActions({ turn }: ResponseActionsProps) {
9494
<TextArea
9595
label='Details'
9696
placeholder='Tell us what was missing or how the response could improve.'
97-
density='expanded'
97+
defaultSize='expanded'
9898
name='details'
9999
/>
100100
</FieldFrame>

‎apps/sim/platform/app/(shell)/_components/chat/message/user-message.tsx‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { MessageTime } from '@/platform/app/(shell)/_components/chat/message/mes
33
import type { PlatformTurn } from '@/platform/app/(shell)/_components/chat/model'
44
import { MessageAttachments } from '@/platform/app/(shell)/_components/composer/message-attachments'
55
import { CopyButton } from '@/platform/design-system/components/button/copy-button'
6+
import { MessageBubble } from '@/platform/design-system/components/message-bubble/message-bubble'
67

78
interface UserMessageProps {
89
turn: Extract<PlatformTurn, { role: 'user' }>
@@ -11,11 +12,7 @@ export function UserMessage({ turn }: UserMessageProps) {
1112
return (
1213
<div className={styles.user}>
1314
<MessageAttachments items={turn.prompt.attachments} />
14-
{turn.prompt.text && (
15-
<p className={styles.bubble} data-type='T048'>
16-
{turn.prompt.text}
17-
</p>
18-
)}
15+
{turn.prompt.text && <MessageBubble>{turn.prompt.text}</MessageBubble>}
1916
<div className={styles.userActions} data-type='T060'>
2017
{turn.sentAt !== undefined && <MessageTime sentAt={turn.sentAt} timeZone={turn.timeZone} />}
2118
<CopyButton label='Copy message' text={turn.prompt.text} />

‎apps/sim/platform/app/(shell)/_components/product-session.tsx‎

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { NotificationSession } from '@/platform/app/(shell)/_components/notifica
88
import { PlatformShell } from '@/platform/app/(shell)/_components/platform-shell/platform-shell'
99
import { ChatChangesProvider } from '@/platform/app/(shell)/_components/session/chat-changes'
1010
import { ComposerDrafts } from '@/platform/app/(shell)/_components/session/composer-drafts'
11+
import { ComposerSession } from '@/platform/app/(shell)/_components/session/composer-host'
1112
import { IntegrationCenterChanges } from '@/platform/app/(shell)/_components/session/integration-center-changes'
1213
import { IntegrationChanges } from '@/platform/app/(shell)/_components/session/integration-changes'
1314
import { InterfacePreferences } from '@/platform/app/(shell)/_components/session/interface-preferences'
@@ -58,15 +59,17 @@ export function ProductSession({
5859
<NotificationSession>
5960
<ChatChangesProvider>
6061
<ComposerDrafts>
61-
<PlatformSurface theme={live?.appearance ?? readAppearance(search.get('theme'))}>
62-
<IntegrationChanges>
63-
<IntegrationCenterChanges enabled={Boolean(fixture) && !live}>
64-
<PlatformShell data={data} banner={banner}>
65-
{children}
66-
</PlatformShell>
67-
</IntegrationCenterChanges>
68-
</IntegrationChanges>
69-
</PlatformSurface>
62+
<ComposerSession>
63+
<PlatformSurface theme={live?.appearance ?? readAppearance(search.get('theme'))}>
64+
<IntegrationChanges>
65+
<IntegrationCenterChanges enabled={Boolean(fixture) && !live}>
66+
<PlatformShell data={data} banner={banner}>
67+
{children}
68+
</PlatformShell>
69+
</IntegrationCenterChanges>
70+
</IntegrationChanges>
71+
</PlatformSurface>
72+
</ComposerSession>
7073
</ComposerDrafts>
7174
</ChatChangesProvider>
7275
</NotificationSession>

‎apps/sim/platform/app/(shell)/_components/session/composer-host.tsx‎

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { draftTitle, userTurn } from '@/platform/app/(shell)/_components/compose
1313
import { useDesktopHost } from '@/platform/app/(shell)/_components/desktop/desktop-host'
1414
import { useChatChanges } from '@/platform/app/(shell)/_components/session/chat-changes'
1515
import { useComposerDraft } from '@/platform/app/(shell)/_components/session/composer-drafts'
16+
import { useInterfacePreferences } from '@/platform/app/(shell)/_components/session/interface-preferences'
1617
import { platformHref } from '@/platform/design-system/navigation'
1718

1819
export interface ComposerOptions extends Partial<ComposerInventory> {
@@ -33,6 +34,7 @@ export interface ComposerHost {
3334
useStartChat: () => (draft: ComposerDraft) => undefined | false
3435
/** Opens Search with a query, where Home's Search segment leads; null when the viewer can't. */
3536
useStartSearch: () => ((query: string) => void) | null
37+
useAppendHomePrompt: () => ((prompt: string) => void) | null
3638
}
3739

3840
function attachmentFor(file: File): ChatAttachment {
@@ -115,13 +117,33 @@ function useMemoryStartChat() {
115117
}
116118
}
117119

120+
function useMemoryAppendHomePrompt() {
121+
const [, updateDraft] = useComposerDraft('home', { text: '', attachments: [] })
122+
return (prompt: string) =>
123+
updateDraft((draft) => ({
124+
...draft,
125+
text: draft.text ? `${draft.text}\n\n${prompt}` : prompt,
126+
}))
127+
}
128+
118129
const MEMORY_HOST: ComposerHost = {
119130
useComposer: useMemoryComposer,
120131
useStartChat: useMemoryStartChat,
121132
useStartSearch: () => null,
133+
useAppendHomePrompt: useMemoryAppendHomePrompt,
134+
}
135+
136+
const HostContext = createContext<ComposerHost | null>(null)
137+
138+
interface ComposerSessionProps {
139+
children: ReactNode
122140
}
123141

124-
const HostContext = createContext<ComposerHost>(MEMORY_HOST)
142+
/** Product sessions inherit their live host or own an in-memory fixture composer. */
143+
export function ComposerSession({ children }: ComposerSessionProps) {
144+
const host = useContext(HostContext) ?? MEMORY_HOST
145+
return <HostContext.Provider value={host}>{children}</HostContext.Provider>
146+
}
125147

126148
export function ComposerHostProvider({
127149
host,
@@ -134,5 +156,24 @@ export function ComposerHostProvider({
134156
}
135157

136158
export function useComposerHost(): ComposerHost {
137-
return useContext(HostContext)
159+
return useContext(HostContext) ?? MEMORY_HOST
160+
}
161+
162+
function useUnavailableHomePrompt() {
163+
return null
164+
}
165+
166+
/** Prepare an editable Home draft without sending it or replacing existing work. */
167+
export function useHomePrompt() {
168+
const useAppendPrompt = useContext(HostContext)?.useAppendHomePrompt ?? useUnavailableHomePrompt
169+
const append = useAppendPrompt()
170+
const preferences = useInterfacePreferences()
171+
const router = useRouter()
172+
const search = useSearchParams()
173+
if (!append) return null
174+
return (prompt: string) => {
175+
append(prompt)
176+
preferences?.setHomeMode('Build')
177+
router.push(platformHref('/home', search))
178+
}
138179
}

‎apps/sim/platform/app/(shell)/_components/session/integration-center-changes.tsx‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,14 @@ function resourceMetadata(resource: IntegrationResource): IntegrationResource {
2929
const identity = { id, name, description, scope, owner, people, canEdit }
3030
switch (resource.kind) {
3131
case 'skill':
32-
return { ...identity, kind: 'skill', instructions: resource.instructions }
32+
return {
33+
...identity,
34+
kind: 'skill',
35+
instructions: resource.instructions,
36+
source: resource.source
37+
? { providerId: resource.source.providerId, name: resource.source.name }
38+
: undefined,
39+
}
3340
case 'mcp':
3441
return { ...identity, kind: 'mcp', url: resource.url, auth: resource.auth }
3542
case 'credential':

‎apps/sim/platform/app/(shell)/integrations/_components/connected-credential/connected-credential.tsx‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
'use client'
22

3-
import { useState } from 'react'
3+
import { type ComponentType, useState } from 'react'
44
import styles from '@/platform/app/(shell)/integrations/_components/connected-credential/connected-credential.module.css'
5-
import { ActionMenu, ActivityRow, Icon, Modal } from '@/platform/design-system/components'
5+
import { IntegrationMark } from '@/platform/app/(shell)/integrations/_components/integration-mark/integration-mark'
6+
import { ActionMenu, ActivityRow, Modal } from '@/platform/design-system/components'
67
import { ActivityBody } from '@/platform/design-system/components/activity-row/activity-body'
78
import { CountedLabel } from '@/platform/design-system/components/overflow-text/counted-label'
89
import type { IntegrationAccount } from '@/platform/lib/data/integrations-types'
@@ -15,6 +16,8 @@ interface CredentialRow extends IntegrationAccount {
1516

1617
interface ConnectedCredentialProps {
1718
integrationName: string
19+
providerId: string
20+
icon?: ComponentType<{ className?: string }>
1821
accounts: readonly CredentialRow[]
1922
pending: boolean
2023
onReconnect: (accountId: string) => void
@@ -23,6 +26,8 @@ interface ConnectedCredentialProps {
2326

2427
export function ConnectedCredential({
2528
integrationName,
29+
providerId,
30+
icon,
2631
accounts,
2732
pending,
2833
onReconnect,
@@ -67,14 +72,13 @@ export function ConnectedCredential({
6772
<ActivityRow
6873
key={account.id}
6974
href={account.href}
70-
leading={<Icon name='I137' />}
75+
leading={<IntegrationMark providerId={providerId} icon={icon} />}
7176
title={account.label}
7277
detail={account.statusLabel}
73-
trailing={action}
7478
/>
7579
) : (
7680
<li key={account.id} className={styles.row}>
77-
<ActivityBody leading={<Icon name='I137' />}>
81+
<ActivityBody leading={<IntegrationMark providerId={providerId} icon={icon} />}>
7882
<span data-type='T059'>{account.label}</span>
7983
<span className={styles.muted} data-type='T050'>
8084
{account.statusLabel}

0 commit comments

Comments
 (0)