Skip to content

Commit eff3a7c

Browse files
committed
Run live chat and Search on the Platform
Conversation surfaces read a ChatRuntime, filled by the existing chat hook on live data and by session state on fixtures. toPlatformTurns projects the hook's transcript into ordered parts with per-tool status and subagent lanes. Rebuild rendering on Platform EMCN: markdown, every tool state, lanes, tool approval, question, credential and assistant review cards. The composer uploads, persists drafts and carries effort, model and plan behind their flags. The panel reads real resources through ?resource=. Add /search and /search/chat/[id], and hand Home's Build and Search entries to them.
1 parent 080a7ed commit eff3a7c

88 files changed

Lines changed: 5717 additions & 478 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: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ Use [the shared Platform time formatters](lib/time.ts) for displayed ages, clock
5656

5757
## Phase 3 data seam
5858

59-
Only `lib/data/**` may import from outside this project: query hooks under `@/hooks/queries/<domain>`, contracts and clients under `@/lib/api/<module>`, and the named helpers listed in `scripts/check-platform-design.ts`. `@/app/<route>`, `@/components/<name>` and the legacy `@sim/emcn` barrel stay banned there too. Add a helper to that list only after reading its runtime import closure.
59+
Only `lib/data/**` may import from outside this project: query hooks under `@/hooks/queries/<domain>`, contracts and clients under `@/lib/api/<module>`, and the named helpers listed in `scripts/check-platform-design.ts`. `@/app/<route>`, `@/components/<name>` and the legacy `@sim/emcn` barrel stay banned there too, apart from named modules on that list, such as the chat hook. Add a helper to that list only after reading its runtime import closure.
60+
61+
The chat hook runs here unchanged. Views read a `ChatRuntime` (`(shell)/_components/chat/model.ts`), filled by `lib/data/chat.ts` on live data and by the session on fixtures. What the hook reaches for a canvas this app does not have, `canvasShims` in `next.config.ts` replaces with a named module under `lib/data/shims/` saying what it drops.
6062

6163
Each `lib/data/<domain>.ts` wraps the existing hook and maps its contract shape to a Platform view model with a pure adapter. Never fork a hook, key factory, invalidation or contract; a hook that lacks a needed option gains it additively. A module's first import of a query hook that toasts resolves through `lib/data/emcn-shim.ts`, which webpack substitutes for `@sim/emcn` and which exports only `toast`, bridged to the notification tray. Any other name from that package fails the build, which is what proves no legacy UI entered.
6264

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import styles from '@/platform/app/(shell)/_components/chat/cards/cards.module.css'
5+
import type {
6+
AssistantReceipt,
7+
AssistantReviewAction,
8+
} from '@/platform/app/(shell)/_components/chat/model'
9+
import { useChatRuntime } from '@/platform/app/(shell)/_components/session/chat-runtime'
10+
import { Button, InlineNotice, StatusTag } from '@/platform/design-system/components'
11+
import { Input, TextArea } from '@/platform/design-system/components/field/inputs'
12+
13+
type Edits = Record<string, Record<string, string>>
14+
15+
function ActionCard({
16+
action,
17+
edits,
18+
onEdit,
19+
disabled,
20+
onDecide,
21+
}: {
22+
action: AssistantReviewAction
23+
edits: Record<string, string> | undefined
24+
onEdit: (name: string, value: string) => void
25+
disabled: boolean
26+
onDecide: (decision: 'allow' | 'skip') => void
27+
}) {
28+
const [editing, setEditing] = useState(false)
29+
const inactive = action.account?.active === false
30+
return (
31+
<div className={styles.card}>
32+
<div className={styles.row}>
33+
<span className={styles.title} data-type='T048'>
34+
{action.title}
35+
</span>
36+
{action.fields.some((field) => field.editable) && (
37+
<Button variant='muted' size='compact' onClick={() => setEditing((value) => !value)}>
38+
{editing ? 'Done' : 'Edit'}
39+
</Button>
40+
)}
41+
</div>
42+
{action.account && (
43+
<p className={styles.quiet} data-type='T052'>
44+
{action.account.name}
45+
{inactive ? ' needs to be reconnected' : ''}
46+
</p>
47+
)}
48+
<dl className={styles.fields}>
49+
{action.fields.map((field) => {
50+
const value = edits?.[field.name] ?? field.value
51+
return (
52+
<div key={field.name}>
53+
<dt data-type='T052'>{field.label}</dt>
54+
<dd data-type='T048'>
55+
{editing && field.editable ? (
56+
field.multiline ? (
57+
<TextArea
58+
label={field.label}
59+
value={value}
60+
onChange={(next) => onEdit(field.name, next)}
61+
/>
62+
) : (
63+
<Input
64+
label={field.label}
65+
value={value}
66+
onChange={(next) => onEdit(field.name, next)}
67+
/>
68+
)
69+
) : (
70+
value
71+
)}
72+
</dd>
73+
</div>
74+
)
75+
})}
76+
</dl>
77+
<div className={styles.actions}>
78+
<Button
79+
variant='primary'
80+
size='compact'
81+
disabled={disabled || inactive}
82+
onClick={() => onDecide('allow')}
83+
>
84+
Allow
85+
</Button>
86+
<Button variant='muted' size='compact' disabled={disabled} onClick={() => onDecide('skip')}>
87+
Cancel
88+
</Button>
89+
</div>
90+
</div>
91+
)
92+
}
93+
94+
function Receipt({ receipt }: { receipt: AssistantReceipt }) {
95+
const intent =
96+
receipt.state === 'failed'
97+
? 'danger'
98+
: receipt.state === 'uncertain'
99+
? 'warning'
100+
: receipt.state === 'succeeded'
101+
? 'success'
102+
: 'neutral'
103+
return (
104+
<div className={styles.card}>
105+
<div className={styles.row}>
106+
<span className={styles.title} data-type='T048'>
107+
{receipt.title}
108+
</span>
109+
<StatusTag label={receipt.stateLabel} intent={intent} />
110+
</div>
111+
{receipt.detail && (
112+
<p className={styles.quiet} data-type='T052'>
113+
{receipt.detail}
114+
</p>
115+
)}
116+
{receipt.state === 'uncertain' && (
117+
<p className={styles.quiet} data-type='T052'>
118+
Check the app before retrying.
119+
</p>
120+
)}
121+
{receipt.warning && (
122+
<p className={styles.quiet} data-type='T052'>
123+
{receipt.warning}
124+
</p>
125+
)}
126+
</div>
127+
)
128+
}
129+
130+
/**
131+
* The actions an Assistant turn proposed, held until the person allows or cancels them. Rendered on
132+
* the turn they belong to; failure to load is shown rather than read as nothing to review.
133+
*/
134+
export function AssistantReview({ turnId }: { turnId: string }) {
135+
const { review } = useChatRuntime()
136+
const [edits, setEdits] = useState<Edits>({})
137+
const [failure, setFailure] = useState('')
138+
if (!review || review.turnId !== turnId) return null
139+
const { actions, receipts, status, pending, retry, decide } = review
140+
async function run(ids: readonly string[], decision: 'allow' | 'skip') {
141+
setFailure('')
142+
try {
143+
await decide(ids, decision, decision === 'allow' ? edits : undefined)
144+
setEdits({})
145+
} catch (error) {
146+
setFailure(error instanceof Error ? error.message : 'Could not save this review')
147+
}
148+
}
149+
if (status === 'error')
150+
return (
151+
<InlineNotice
152+
intent='danger'
153+
message='Could not load action review'
154+
action={
155+
<Button variant='muted' size='compact' onClick={retry}>
156+
Retry
157+
</Button>
158+
}
159+
/>
160+
)
161+
if (!actions.length && !receipts.length) return null
162+
const allowDisabled = pending || actions.some((action) => action.account?.active === false)
163+
return (
164+
<section className={styles.review} aria-label='Actions to review'>
165+
{receipts.map((receipt) => (
166+
<Receipt key={receipt.id} receipt={receipt} />
167+
))}
168+
{actions.map((action) => (
169+
<ActionCard
170+
key={action.id}
171+
action={action}
172+
edits={edits[action.id]}
173+
disabled={pending}
174+
onEdit={(name, value) =>
175+
setEdits((current) => ({
176+
...current,
177+
[action.id]: { ...current[action.id], [name]: value },
178+
}))
179+
}
180+
onDecide={(decision) => void run([action.id], decision)}
181+
/>
182+
))}
183+
{actions.length > 1 && (
184+
<div className={styles.actions}>
185+
<Button
186+
variant='primary'
187+
size='compact'
188+
disabled={allowDisabled}
189+
onClick={() =>
190+
void run(
191+
actions.map((action) => action.id),
192+
'allow'
193+
)
194+
}
195+
>
196+
{`Allow ${actions.length} shown`}
197+
</Button>
198+
<Button
199+
variant='muted'
200+
size='compact'
201+
disabled={pending}
202+
onClick={() =>
203+
void run(
204+
actions.map((action) => action.id),
205+
'skip'
206+
)
207+
}
208+
>
209+
{`Cancel ${actions.length} shown`}
210+
</Button>
211+
</div>
212+
)}
213+
{failure && <InlineNotice intent='danger' message={failure} />}
214+
</section>
215+
)
216+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
.card,
2+
.review,
3+
.fields,
4+
.options {
5+
display: grid;
6+
min-inline-size: 0;
7+
gap: var(--ui-row-gap);
8+
}
9+
.review {
10+
gap: var(--ui-section-gap);
11+
}
12+
.card {
13+
padding: var(--spacing-12) var(--spacing-16);
14+
border-radius: var(--radius-dialog);
15+
background: var(--control-avatar);
16+
}
17+
.row,
18+
.actions {
19+
display: flex;
20+
align-items: center;
21+
gap: var(--ui-action-gap);
22+
min-inline-size: 0;
23+
flex-wrap: wrap;
24+
}
25+
.row {
26+
justify-content: space-between;
27+
}
28+
.title {
29+
flex: 1;
30+
min-inline-size: 0;
31+
overflow-wrap: anywhere;
32+
}
33+
.glyph {
34+
display: inline-flex;
35+
flex: none;
36+
color: var(--ui-secondary);
37+
}
38+
.quiet,
39+
.fields dt {
40+
color: var(--ui-secondary);
41+
}
42+
.fields div {
43+
display: grid;
44+
gap: var(--ui-stack-gap);
45+
}
46+
.fields dd {
47+
white-space: pre-wrap;
48+
overflow-wrap: anywhere;
49+
}
50+
.link {
51+
text-decoration: underline;
52+
}

0 commit comments

Comments
 (0)