Skip to content

Commit 226db8a

Browse files
max-drakeTodePondmimecuvalokostyafarbersteveruizok
authored
Fairy T-4 days (tldraw#7230)
Describe what your pull request does. If you can, add GIFs or images showing the before and after of your change. ### Change type - [ ] `bugfix` - [ ] `improvement` - [ ] `feature` - [ ] `api` - [x] `other` ### Release notes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds a new project chat view and orchestration flow, introduces canvas linting and updated todo/task actions, refactors agent modes/serialization, and polishes UI/i18n. > > - **UI/UX** > - **Project View**: New `FairyProjectView` with `FairyProjectChatContent`, mini avatars, whisper input, cancel-to-disband behavior; integrated into `FairyHUD` flow (select orchestrator; replaces group chat for multi/active projects). > - **Inputs**: Replace `FairyBasicInput` with `FairySingleChatInput` (interrupt-aware send/cancel, random no-input text). > - **Menus**: Add wake option; disband uses `editor`; reset-all skips sleeping agents. > - **Sprites/Indicators**: New mini avatar components; remote fairies hide when sleeping. > - **Styles**: Hide chat scrollbars; add project chat styles; misc layout tweaks. > - **Projects** > - Utilities: `disbandProject` now editor-based; add `disbandAllProjectsWithAgents`, `getProjectOrchestrator`, `resumeProject` (duo/team), and bad-state rectifier. > - **Agent/Core** > - Modes: Add `one-shotting-pausing`; refine timing, cancel flow, pose setting; clear histories/todos on transitions. > - State: Persist/restore `waitingFor` via (de)serialization; new helpers to clear user history and todos. > - **Actions & Schema** > - New: `upsert-personal-todo-item`, `delete-personal-todo-items`, `delete-project-task` (with UI handlers); remove legacy personal todo action. > - Wiring across client utils, shared schemas/definitions, worker flags, and rules. > - **Prompt Parts** > - Add `CanvasLintsPart` (growY, overlapping text, friendless arrows) and include in active modes. > - **Shape format** > - Rename focused `draw` to `pen` across converters and context labels; adjust arrow bend/label handling. > - **i18n** > - Add keys: `Locate`, `Wake`, `Disband projects`; remove/replace `Visit`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit fc4a40f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Lu[ke] Wilson <l2wilson94@gmail.com> Co-authored-by: Mime Čuvalo <mimecuvalo@gmail.com> Co-authored-by: Kostya Farber <kostya.farber@gmail.com> Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
1 parent 945ce5d commit 226db8a

43 files changed

Lines changed: 1890 additions & 208 deletions

Some content is hidden

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

apps/dotcom/client/public/tla/locales-compiled/en.json

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,12 @@
159159
"value": "Instruct the group…"
160160
}
161161
],
162+
"20dfc4f904": [
163+
{
164+
"type": 0,
165+
"value": "Locate"
166+
}
167+
],
162168
"23f879f41a": [
163169
{
164170
"type": 0,
@@ -559,12 +565,6 @@
559565
"value": "We failed to upload some of the content you created before you signed in."
560566
}
561567
],
562-
"5e706a9b7e": [
563-
{
564-
"type": 0,
565-
"value": "Visit"
566-
}
567-
],
568568
"5ea8bb44be": [
569569
{
570570
"type": 0,
@@ -645,6 +645,12 @@
645645
"value": "Enter your email address"
646646
}
647647
],
648+
"688761c83e": [
649+
{
650+
"type": 0,
651+
"value": "Wake"
652+
}
653+
],
648654
"68d390535e": [
649655
{
650656
"type": 0,
@@ -1723,6 +1729,12 @@
17231729
"value": "Viewer"
17241730
}
17251731
],
1732+
"fd11eafb92": [
1733+
{
1734+
"type": 0,
1735+
"value": "Disband projects"
1736+
}
1737+
],
17261738
"fd1be3efcf": [
17271739
{
17281740
"type": 0,

apps/dotcom/client/public/tla/locales/en.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@
7777
"1fe250ea06": {
7878
"translation": "Instruct the group…"
7979
},
80+
"20dfc4f904": {
81+
"translation": "Locate"
82+
},
8083
"23f879f41a": {
8184
"translation": "Your weekly fairy usage limit has been reached. It will reset at the start of next week."
8285
},
@@ -251,9 +254,6 @@
251254
"5d26ae7550": {
252255
"translation": "We failed to upload some of the content you created before you signed in."
253256
},
254-
"5e706a9b7e": {
255-
"translation": "Visit"
256-
},
257257
"5ea8bb44be": {
258258
"translation": "You have been invited to join group:"
259259
},
@@ -290,6 +290,9 @@
290290
"6832ac5936": {
291291
"translation": "Enter your email address"
292292
},
293+
"688761c83e": {
294+
"translation": "Wake"
295+
},
293296
"68d390535e": {
294297
"translation": "Enter your message"
295298
},
@@ -747,6 +750,9 @@
747750
"fb15c53f22": {
748751
"translation": "Viewer"
749752
},
753+
"fd11eafb92": {
754+
"translation": "Disband projects"
755+
},
750756
"fd1be3efcf": {
751757
"translation": "Are you sure you want to delete this file?"
752758
},

apps/dotcom/client/src/fairy/CONTEXT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ Actions are the operations fairies can perform on the canvas. Each action extend
301301
- `MessageActionUtil`: Send messages to users
302302
- `ThinkActionUtil`: Display thinking process
303303
- `ReviewActionUtil`: Review and analyze canvas content
304-
- `PersonalTodoListActionUtil`: Manage personal todo list
304+
- `UpsertPersonalTodoItemActionUtil`: Manage personal todo list
305305

306306
**System**
307307

apps/dotcom/client/src/fairy/FairyApp.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { TldrawApp } from '../tla/app/TldrawApp'
1313
import { useApp } from '../tla/hooks/useAppState'
1414
import { useTldrawUser } from '../tla/hooks/useUser'
1515
import { FairyAgent } from './fairy-agent/agent/FairyAgent'
16-
import { $fairyProjects } from './FairyProjects'
16+
import { $fairyProjects, disbandAllProjectsWithAgents } from './FairyProjects'
1717
import { FairyTaskDragTool } from './FairyTaskDragTool'
1818
import { $fairyTasks, $showCanvasFairyTasks } from './FairyTaskList'
1919
import { FairyThrowTool } from './FairyThrowTool'
@@ -133,6 +133,12 @@ export function FairyApp({
133133
// Cleanup: dispose all agents only when component unmounts
134134
useEffect(() => {
135135
return () => {
136+
// Disband all projects - this interrupts agents, updates their state,
137+
// and clears associated tasks. Uses interrupt() which safely transitions
138+
// agents out of active modes without throwing.
139+
disbandAllProjectsWithAgents(agentsRef.current)
140+
141+
// Now it's safe to dispose agents since they're no longer in active modes
136142
agentsRef.current.forEach((agent) => agent.dispose())
137143
}
138144
}, [])

apps/dotcom/client/src/fairy/FairyGroupChat.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { CancelIcon, FairyProject, LipsIcon } from '@tldraw/fairy-shared'
22
import { useCallback, useEffect, useRef, useState } from 'react'
3-
import { uniqueId, useValue } from 'tldraw'
3+
import { uniqueId, useEditor, useValue } from 'tldraw'
44
import { F, useMsg } from '../tla/utils/i18n'
55
import { FairyAgent } from './fairy-agent/agent/FairyAgent'
66
import { fairyMessages } from './fairy-messages'
@@ -14,6 +14,7 @@ export function FairyGroupChat({
1414
onStartProject(orchestratorAgent: FairyAgent): void
1515
}) {
1616
const leaderAgentId = agents[0]?.id ?? null
17+
const editor = useEditor()
1718

1819
const [instruction, setInstruction] = useState('')
1920
const instructionTextareaRef = useRef<HTMLTextAreaElement>(null)
@@ -86,7 +87,7 @@ Make sure to give the approximate locations of the work to be done, if relevant,
8687

8788
if (shouldCancel) {
8889
if (!currentProject) return
89-
disbandProject(currentProject.id, agents)
90+
disbandProject(currentProject.id, editor)
9091
return
9192
}
9293

@@ -136,7 +137,7 @@ Make sure to give the approximate locations of the work to be done, if relevant,
136137
// Clear the input
137138
setInstruction('')
138139
},
139-
[getGroupChatPrompt, leaderAgent, followerAgents, onStartProject, agents, shouldCancel]
140+
[getGroupChatPrompt, leaderAgent, followerAgents, onStartProject, editor, shouldCancel]
140141
)
141142

142143
const handleButtonClick = () => {

apps/dotcom/client/src/fairy/FairyHUD.tsx

Lines changed: 97 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { FairyProject, FairyTask } from '@tldraw/fairy-shared'
2-
import { MouseEvent, useCallback, useEffect, useRef, useState } from 'react'
2+
import { MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
33
import {
44
PORTRAIT_BREAKPOINT,
55
TldrawUiButton,
@@ -15,11 +15,12 @@ import '../tla/styles/fairy.css'
1515
import { F, useMsg } from '../tla/utils/i18n'
1616
import { FairyAgent } from './fairy-agent/agent/FairyAgent'
1717
import { FairyChatHistory } from './fairy-agent/chat/FairyChatHistory'
18-
import { FairyBasicInput } from './fairy-agent/input/FairyBasicInput'
18+
import { FairySingleChatInput } from './fairy-agent/input/FairySingleChatInput'
1919
import { fairyMessages } from './fairy-messages'
2020
import { FairyDropdownContent } from './FairyDropdownContent'
21-
import { FairyGroupChat } from './FairyGroupChat'
2221
import { FairyListSidebar } from './FairyListSidebar'
22+
import { $fairyProjects, getProjectOrchestrator } from './FairyProjects'
23+
import { FairyProjectView } from './FairyProjectView'
2324
import { $fairyTasks } from './FairyTaskList'
2425
import { FairyTaskListDropdownContent } from './FairyTaskListDropdownContent'
2526
import { FairyTaskListInline } from './FairyTaskListInline'
@@ -55,9 +56,7 @@ function FairyHUDHeader({
5556
const project = useValue('project', () => shownFairy?.getProject(), [shownFairy])
5657

5758
// Check if the project has been started (has an orchestrator)
58-
const isProjectStarted = project?.members.some(
59-
(member) => member.role === 'orchestrator' || member.role === 'duo-orchestrator'
60-
)
59+
const isProjectStarted = project && getProjectOrchestrator(project)
6160

6261
const fairyClickable = useValue(
6362
'fairy clickable',
@@ -190,6 +189,22 @@ export function FairyHUD({ agents }: { agents: FairyAgent[] }) {
190189
[agents]
191190
)
192191

192+
const activeOrchestratorAgent = useValue(
193+
'shown-orchestrator',
194+
() => {
195+
if (!shownFairy) return null
196+
const project = shownFairy.getProject()
197+
if (!project) return null
198+
199+
const orchestratorMember = getProjectOrchestrator(project)
200+
if (!orchestratorMember) return null
201+
202+
// Return the actual FairyAgent, not just the member
203+
return agents.find((agent) => agent.id === orchestratorMember.id) ?? null
204+
},
205+
[shownFairy, agents]
206+
)
207+
193208
// Update the chosen fairy when the selected fairies change
194209
useEffect(() => {
195210
if (selectedFairies.length === 1) {
@@ -223,9 +238,7 @@ export function FairyHUD({ agents }: { agents: FairyAgent[] }) {
223238
}
224239

225240
// Check if project has an orchestrator (meaning it's been started)
226-
const orchestratorMember = project.members.find(
227-
(member) => member.role === 'orchestrator' || member.role === 'duo-orchestrator'
228-
)
241+
const orchestratorMember = getProjectOrchestrator(project)
229242

230243
if (orchestratorMember) {
231244
// Project has been started, show the orchestrator's chat
@@ -303,10 +316,25 @@ export function FairyHUD({ agents }: { agents: FairyAgent[] }) {
303316
const handleDoubleClickFairy = useCallback(
304317
(clickedAgent: FairyAgent) => {
305318
clickedAgent.zoomTo()
319+
320+
// If the clicked fairy is part of an active project, select the orchestrator instead
321+
const project = clickedAgent.getProject()
322+
if (project) {
323+
const orchestratorMember = getProjectOrchestrator(project)
324+
if (orchestratorMember) {
325+
const orchestratorAgent = agents.find((agent) => agent.id === orchestratorMember.id)
326+
if (orchestratorAgent) {
327+
selectFairy(orchestratorAgent)
328+
setPanelState('fairy')
329+
return
330+
}
331+
}
332+
}
333+
306334
selectFairy(clickedAgent)
307335
setPanelState('fairy')
308336
},
309-
[selectFairy]
337+
[selectFairy, agents]
310338
)
311339

312340
const handleTogglePanel = useCallback(() => {
@@ -387,6 +415,38 @@ export function FairyHUD({ agents }: { agents: FairyAgent[] }) {
387415
return () => window.removeEventListener('resize', updatePosition)
388416
}, [isMobile])
389417

418+
// Unused atm, wip
419+
// Check if any fairies in projects are idling
420+
const projects = useValue('fairy-projects', () => $fairyProjects.get(), [$fairyProjects])
421+
const badStateProjectIds = useMemo(() => {
422+
const projectIds = new Set<string>()
423+
for (const project of projects) {
424+
for (const member of project.members) {
425+
const agent = agents.find((a) => a.id === member.id)
426+
if (agent && agent.getMode() === 'idling') {
427+
projectIds.add(project.id)
428+
}
429+
}
430+
}
431+
return Array.from(projectIds)
432+
}, [agents, projects])
433+
useEffect(() => {
434+
for (const project of projects) {
435+
if (badStateProjectIds.includes(project.id)) {
436+
for (const member of project.members) {
437+
const agent = agents.find((a) => a.id === member.id)
438+
if (agent && agent.getMode() === 'idling') {
439+
const fairyName = agent.$fairyConfig.get()?.name ?? 'unknown'
440+
// eslint-disable-next-line no-console
441+
console.log(
442+
`Fairy in project but idling: projectId=${project.id}, fairyId=${agent.id}, name=${fairyName}`
443+
)
444+
}
445+
}
446+
}
447+
}
448+
}, [badStateProjectIds, agents, projects])
449+
390450
return (
391451
<>
392452
<div
@@ -436,22 +496,44 @@ export function FairyHUD({ agents }: { agents: FairyAgent[] }) {
436496
<F defaultMessage="Select a fairy on the right to chat with" />
437497
</div>
438498
)}
499+
{/* Solo fairy mode - no project */}
439500
{panelState === 'fairy' &&
440501
selectedFairies.length <= 1 &&
441-
shownFairy && ( // if there's a single shown fairy, still show the chat history and input even if the user deselects it
502+
shownFairy &&
503+
!activeOrchestratorAgent && (
442504
<>
443505
<FairyChatHistory agent={shownFairy} />
444-
<FairyBasicInput agent={shownFairy} onCancel={() => setPanelState('closed')} />
506+
<FairySingleChatInput
507+
agent={shownFairy}
508+
onCancel={() => setPanelState('closed')}
509+
/>
445510
</>
446511
)}
447512

513+
{/* Project mode - ongoing project with orchestrator */}
514+
{panelState === 'fairy' &&
515+
selectedFairies.length <= 1 &&
516+
shownFairy &&
517+
activeOrchestratorAgent && (
518+
<FairyProjectView
519+
editor={editor}
520+
agents={agents}
521+
orchestratorAgent={activeOrchestratorAgent}
522+
onClose={() => setPanelState('closed')}
523+
/>
524+
)}
525+
526+
{/* Pre-project mode - multiple fairies selected, no project yet */}
448527
{panelState === 'fairy' && selectedFairies.length > 1 && (
449-
<FairyGroupChat
528+
<FairyProjectView
529+
editor={editor}
450530
agents={selectedFairies}
451-
onStartProject={(orchestratorAgent) => {
452-
selectFairy(orchestratorAgent)
531+
orchestratorAgent={null}
532+
onProjectStarted={(orchestrator) => {
533+
selectFairy(orchestrator)
453534
setPanelState('fairy')
454535
}}
536+
onClose={() => setPanelState('closed')}
455537
/>
456538
)}
457539

0 commit comments

Comments
 (0)