feat(office): redesign the live office workspace#212
feat(office): redesign the live office workspace#212mczabca-boop wants to merge 13 commits intomainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR is a large-scale redesign of the Key changes and concerns:
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant SSE as SSE Stream
participant Page as OfficePage
participant Sessions as AgentWorkSessions
participant Scene as PixelOfficeScene
User->>SSE: sends message
SSE->>Page: message_enqueued (messageId, message)
Page->>Sessions: rootSessionsRef.set(messageId, {startedAt, agentIds})
Page->>Page: setBubbles (user bubble)
SSE->>Page: chain_step_start (agentId)
Page->>Sessions: attachAgentToLatestRoot(agentId)
Sessions->>Page: setAgentWorkSessions({rootMessageId, startedAt})
Page->>Scene: sceneAgent.anim = walk → type
SSE->>Page: agent_message (agentId, content)
Page->>Sessions: attachAgentToLatestRoot(agentId)
Page->>Page: setBubbles (agent bubble)
Page->>Scene: overlayBubble shown above agent
SSE->>Page: chain_handoff (fromAgent, toAgent)
Page->>Sessions: attachAgentToLatestRoot(fromAgent + toAgent)
SSE->>Page: response_ready (messageId)
Page->>Sessions: rootSession.completedAt = timestamp
Sessions->>Page: setAgentWorkSessions completedAt for all agentIds
Page->>Scene: sceneAgent.anim = idle → walk (return) → idle/sleep
Note over Sessions,Scene: After AGENT_SESSION_RELEASE_MS (6.2s), session removed
Last reviewed commit: df44ca4 |
| export function PixelOfficeScene({ | ||
| frame, | ||
| connected, | ||
| statusLabel, | ||
| queue, | ||
| bossRoom, | ||
| archiveRoom, | ||
| routeRoot, | ||
| routeTargets, | ||
| lounge, | ||
| taskStations, | ||
| taskSummaries, | ||
| responses, | ||
| agents, | ||
| }: { | ||
| frame: number; | ||
| connected: boolean; | ||
| statusLabel: string; | ||
| queue: SceneQueueSnapshot; | ||
| bossRoom: SceneBossRoom; | ||
| archiveRoom: SceneArchiveRoom; | ||
| routeRoot: string; | ||
| routeTargets: SceneRouteTarget[]; | ||
| lounge: SceneLounge; | ||
| taskStations: SceneTaskStation[]; | ||
| taskSummaries: SceneTaskSummary[]; | ||
| responses: SceneResponseItem[]; | ||
| agents: SceneAgent[]; |
There was a problem hiding this comment.
Dead SVG components never rendered in the scene
Five fully-implemented SVG components — QueuePanel (line 406), ControlConsole (line 452), RoutingPanel (line 511), TaskSummaryPanel (line 814), and ResponseDock (line 846) — are defined in this file but are never called inside PixelOfficeScene's <svg> block.
The corresponding props on PixelOfficeScene — connected, statusLabel, queue, routeRoot, routeTargets, taskSummaries, and responses — are accepted but never forwarded to any of these components, making them unused as well.
Additionally, page.tsx computes queueSnapshot, statusLabel, routeRoot, routeTargets, taskSummaries, and responseItems every render tick just to pass them to these props, all of which currently have zero effect on the rendered output.
If these panels are intentional future work, please add a comment noting that. If they were accidentally left out, the PixelOfficeScene SVG block should include:
<QueuePanel frame={frame} queue={queue} />
<ControlConsole frame={frame} connected={connected} statusLabel={statusLabel} />
<RoutingPanel frame={frame} connected={connected} routeRoot={routeRoot} routeTargets={routeTargets} />
<TaskSummaryPanel summaries={taskSummaries} />
<ResponseDock responses={responses} />| const handleSend = useCallback(async () => { | ||
| if (!chatInput.trim() || sending) return; | ||
| setSending(true); | ||
| try { | ||
| await sendMessage({ message: chatInput, sender: "Web", channel: "web" }); | ||
| const message = | ||
| conversationFilter !== "all" && !chatInput.trim().startsWith("@") | ||
| ? `@${conversationFilter} ${chatInput.trim()}` | ||
| : chatInput.trim(); | ||
|
|
||
| await sendMessage({ message, sender: "Web", channel: "web" }); | ||
| setChatInput(""); | ||
| } catch { | ||
| // errors surface via SSE events | ||
| } finally { | ||
| setSending(false); | ||
| } | ||
| }, [chatInput, sending]); | ||
| }, [chatInput, conversationFilter, sending]); |
There was a problem hiding this comment.
Missing error handling causes silent unhandled rejection
The catch block was removed from handleSend. If sendMessage throws (e.g. network failure), the error re-throws after finally, and the call sites use void handleSend() — meaning the rejected promise is silently discarded with no user feedback.
The original code handled this explicitly (the now-removed comment noted "errors surface via SSE events"), but without a catch, the promise rejection surfaces as an unhandled rejection in the browser console and the user sees no indication that the send failed.
| const handleSend = useCallback(async () => { | |
| if (!chatInput.trim() || sending) return; | |
| setSending(true); | |
| try { | |
| await sendMessage({ message: chatInput, sender: "Web", channel: "web" }); | |
| const message = | |
| conversationFilter !== "all" && !chatInput.trim().startsWith("@") | |
| ? `@${conversationFilter} ${chatInput.trim()}` | |
| : chatInput.trim(); | |
| await sendMessage({ message, sender: "Web", channel: "web" }); | |
| setChatInput(""); | |
| } catch { | |
| // errors surface via SSE events | |
| } finally { | |
| setSending(false); | |
| } | |
| }, [chatInput, sending]); | |
| }, [chatInput, conversationFilter, sending]); | |
| const handleSend = useCallback(async () => { | |
| if (!chatInput.trim() || sending) return; | |
| setSending(true); | |
| try { | |
| const message = | |
| conversationFilter !== "all" && !chatInput.trim().startsWith("@") | |
| ? `@${conversationFilter} ${chatInput.trim()}` | |
| : chatInput.trim(); | |
| await sendMessage({ message, sender: "Web", channel: "web" }); | |
| setChatInput(""); | |
| } catch { | |
| // errors surface via SSE events | |
| } finally { | |
| setSending(false); | |
| } | |
| }, [chatInput, conversationFilter, sending]); |
| const { data: agentHistories } = usePolling<Record<string, AgentMessage[]>>( | ||
| async () => { | ||
| if (!agents) return {}; | ||
| const entries = await Promise.all( | ||
| Object.keys(agents).map(async (agentId) => [agentId, await getAgentMessages(agentId, 40)] as const), | ||
| ); | ||
| return Object.fromEntries(entries); | ||
| }, | ||
| 5000, | ||
| [agents], | ||
| ); |
There was a problem hiding this comment.
agentHistories polling resets its interval on every agents refresh
usePolling wraps the fetcher in a useCallback with the provided deps, then re-runs a useEffect whenever that callback identity changes. Since [agents] is passed as deps and agents is a new object reference on every 5-second agents poll, the agentHistories interval gets torn down and recreated each time agents refreshes.
Additionally, if a single getAgentMessages call fails, the entire Promise.all rejects and all history is lost until the next cycle.
Consider using a stable agentIdKey string as the dependency (e.g. Object.keys(agents ?? {}).sort().join(",")) and using Promise.allSettled instead of Promise.all to allow partial results when individual agent fetches fail.
# Conflicts: # tinyoffice/src/app/office/page.tsx
PR Title