Skip to content

Commit a247900

Browse files
andresdjassoclaude
andcommitted
feat(mothership): activity view, chat resource persistence, embedded page wiring
- Activity view components (agent identity, parallel agents, shimmer status) with model + tests, plus a standalone /activity-preview render harness - Chat resource persistence/types extended through the copilot contract, post handler, and resource tool handlers - Embedded pages (Tables, Knowledge, Integrations) accept open-callbacks so the chat resource panel can navigate within tabs instead of routing away - Chat history list extracted into a shared searchable, recency-bucketed component used by the All Chats tray and the title-bar switcher Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent e5aae91 commit a247900

25 files changed

Lines changed: 2323 additions & 196 deletions

File tree

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
'use client'
2+
3+
/**
4+
* TEMPORARY preview harness (delete before merge). Designs the in-chat working
5+
* indicator: ONE shimmering status line by default, escalating to a per-agent
6+
* breakout ONLY while ≥2 agents run concurrently, then collapsing back to a
7+
* single line and the reply.
8+
*/
9+
import { useEffect, useState } from 'react'
10+
import {
11+
ParallelAgents,
12+
ShimmerStatus,
13+
} from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/activity-view'
14+
15+
type Frame =
16+
| { kind: 'line'; text: string }
17+
| { kind: 'parallel'; header: string; agents: { label: string; phrase: string }[] }
18+
19+
interface Scene {
20+
key: string
21+
label: string
22+
prompt: string
23+
frames: Frame[]
24+
reply: string
25+
}
26+
27+
const line = (text: string): Frame => ({ kind: 'line', text })
28+
29+
const SCENES: Scene[] = [
30+
{
31+
key: 'crm',
32+
label: 'Build CRM',
33+
prompt: 'build a simple crm page',
34+
frames: [
35+
line('Reviewing UX requirements and data model'),
36+
line('Exploring CRM page structure and data flow'),
37+
line('Drafting a clean CRM page layout'),
38+
line('Wiring up the contacts table'),
39+
],
40+
reply: 'Done — your CRM page is ready. Open it on the right.',
41+
},
42+
{
43+
key: 'parallel',
44+
label: 'Parallel agents',
45+
prompt: 'Polish my profile — refresh my skills and bio',
46+
frames: [
47+
line('Reviewing your profile'),
48+
{
49+
kind: 'parallel',
50+
header: 'Profile scan · 2 agents',
51+
agents: [
52+
{ label: 'Skills', phrase: 'Scanning your experience' },
53+
{ label: 'Biography', phrase: 'Reading your current bio' },
54+
],
55+
},
56+
{
57+
kind: 'parallel',
58+
header: 'Profile scan · 2 agents',
59+
agents: [
60+
{ label: 'Skills', phrase: 'Determining relevant changes' },
61+
{ label: 'Biography', phrase: 'Crafting a proposal' },
62+
],
63+
},
64+
{
65+
kind: 'parallel',
66+
header: 'Profile scan · 2 agents',
67+
agents: [
68+
{ label: 'Skills', phrase: 'Finalizing skill updates' },
69+
{ label: 'Biography', phrase: 'Polishing the wording' },
70+
],
71+
},
72+
line('Wrapping up'),
73+
],
74+
reply: 'Updated your skills and bio — review the changes on the right.',
75+
},
76+
{
77+
key: 'edit',
78+
label: 'Edit dialog',
79+
prompt: 'Add an edit dialog to update contact details without deleting them',
80+
frames: [
81+
line('Reviewing edit dialog integration plans'),
82+
line('Adding the edit form'),
83+
line('Saving changes in place'),
84+
],
85+
reply: 'Added an edit dialog — contacts now update in place.',
86+
},
87+
]
88+
89+
const FRAME_MS = 1900
90+
91+
export default function ActivityPreviewPage() {
92+
const [sceneKey, setSceneKey] = useState(SCENES[0].key)
93+
const [idx, setIdx] = useState(0)
94+
const [playing, setPlaying] = useState(true)
95+
96+
const scene = SCENES.find((s) => s.key === sceneKey) ?? SCENES[0]
97+
const total = scene.frames.length
98+
const done = idx >= total
99+
const frame = done ? null : scene.frames[idx]
100+
101+
useEffect(() => {
102+
setIdx(0)
103+
setPlaying(true)
104+
}, [sceneKey])
105+
106+
useEffect(() => {
107+
if (!playing) return
108+
if (idx >= total) {
109+
setPlaying(false)
110+
return
111+
}
112+
const t = setTimeout(() => setIdx((i) => Math.min(i + 1, total)), FRAME_MS)
113+
return () => clearTimeout(t)
114+
}, [playing, idx, total])
115+
116+
return (
117+
<div className='flex h-screen flex-col bg-[var(--bg)] p-[24px]'>
118+
<div className='mb-[16px] flex flex-wrap items-center gap-[8px]'>
119+
{SCENES.map((s) => (
120+
<button
121+
key={s.key}
122+
type='button'
123+
onClick={() => setSceneKey(s.key)}
124+
className={`rounded-[6px] px-[10px] py-[5px] text-[13px] ${
125+
s.key === sceneKey
126+
? 'bg-[var(--surface-6)] text-[var(--text-primary)]'
127+
: 'bg-[var(--surface-4)] text-[var(--text-secondary)]'
128+
}`}
129+
>
130+
{s.label}
131+
</button>
132+
))}
133+
<div className='ml-auto flex items-center gap-[6px]'>
134+
<button
135+
type='button'
136+
onClick={() => {
137+
setPlaying(false)
138+
setIdx((i) => Math.max(0, i - 1))
139+
}}
140+
className='rounded-[6px] bg-[var(--surface-4)] px-[10px] py-[5px] text-[13px] text-[var(--text-secondary)]'
141+
>
142+
‹ Prev
143+
</button>
144+
<button
145+
type='button'
146+
onClick={() => {
147+
if (done) {
148+
setIdx(0)
149+
setPlaying(true)
150+
} else {
151+
setPlaying((p) => !p)
152+
}
153+
}}
154+
className='rounded-[6px] bg-[var(--surface-6)] px-[12px] py-[5px] text-[13px] text-[var(--text-primary)]'
155+
>
156+
{done ? '↻ Replay' : playing ? 'Pause' : 'Play'}
157+
</button>
158+
<button
159+
type='button'
160+
onClick={() => {
161+
setPlaying(false)
162+
setIdx((i) => Math.min(total, i + 1))
163+
}}
164+
className='rounded-[6px] bg-[var(--surface-4)] px-[10px] py-[5px] text-[13px] text-[var(--text-secondary)]'
165+
>
166+
Next ›
167+
</button>
168+
<span className='ml-[4px] w-[44px] text-right text-[12px] text-[var(--text-muted)]'>
169+
{Math.min(idx, total)}/{total}
170+
</span>
171+
</div>
172+
</div>
173+
174+
<div className='flex min-h-0 flex-1 justify-center overflow-y-auto'>
175+
<div className='flex w-full max-w-[640px] flex-col gap-[20px] py-[24px]'>
176+
<div className='flex justify-end'>
177+
<div className='max-w-[80%] rounded-[14px] bg-[var(--surface-5)] px-[14px] py-[10px] text-[14px] text-[var(--text-primary)]'>
178+
{scene.prompt}
179+
</div>
180+
</div>
181+
182+
{done ? (
183+
<p className='animate-stream-fade-in text-[15px] text-[var(--text-primary)] leading-[24px]'>
184+
{scene.reply}
185+
</p>
186+
) : frame?.kind === 'parallel' ? (
187+
<ParallelAgents header={frame.header} agents={frame.agents} active={playing} />
188+
) : (
189+
<ShimmerStatus text={frame?.text ?? ''} active={playing} />
190+
)}
191+
</div>
192+
</div>
193+
</div>
194+
)
195+
}

apps/sim/app/api/copilot/chat/resources/route.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,20 @@ const VALID_RESOURCE_TYPES = new Set<ResourceType>([
2727
'workflow',
2828
'knowledgebase',
2929
'folder',
30+
'filefolder',
3031
'log',
3132
'integration',
33+
'page',
34+
])
35+
const GENERIC_TITLES = new Set([
36+
'Table',
37+
'File',
38+
'Workflow',
39+
'Knowledge Base',
40+
'Folder',
41+
'File Folder',
42+
'Log',
3243
])
33-
const GENERIC_TITLES = new Set(['Table', 'File', 'Workflow', 'Knowledge Base', 'Folder', 'Log'])
3444

3545
export const POST = withRouteHandler(async (req: NextRequest) => {
3646
try {

0 commit comments

Comments
 (0)