Skip to content

feat(office): redesign the live office workspace#212

Open
mczabca-boop wants to merge 13 commits intomainfrom
v0.0.12-dev
Open

feat(office): redesign the live office workspace#212
mczabca-boop wants to merge 13 commits intomainfrom
v0.0.12-dev

Conversation

@mczabca-boop
Copy link
Collaborator

PR Title

@vercel
Copy link

vercel bot commented Mar 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
tinyoffice Ready Ready Preview, Comment Mar 13, 2026 11:19pm

Request Review

@greptile-apps
Copy link

greptile-apps bot commented Mar 13, 2026

Greptile Summary

This PR is a large-scale redesign of the /office page, replacing the old static sprite-based layout with a fully SVG-rendered pixel-art workspace. It introduces distinct rooms (Boss Room, Archives, Agent Lounge, Task Station area), a live SSE-driven agent work session model that animates characters between the lounge and their desks, and an embedded conversation panel with per-agent tab filtering and history from getAgentMessages.

Key changes and concerns:

  • Dead SVG components in pixel-office-scene.tsx: QueuePanel, ControlConsole, RoutingPanel, TaskSummaryPanel, and ResponseDock are fully implemented (~200 lines) but never called inside PixelOfficeScene's SVG. The seven corresponding props (queue, connected, statusLabel, routeRoot, routeTargets, taskSummaries, responses) are accepted by the component but have no effect on rendered output. All the useMemo computation in page.tsx that feeds these props is wasted on every render tick.
  • Silent sendMessage failures: The catch block was removed from handleSend. If the API call throws, the error propagates out of the try-finally and is swallowed by void handleSend() at both call sites, producing an unhandled promise rejection with no user feedback.
  • agentHistories polling instability: Passing [agents] as deps to usePolling causes the interval to be reset on every agents refresh (every 5 s). Using Promise.all also means a single failing agent history fetch drops all histories for that cycle.
  • New pixel-office-char.tsx is a clean pure-rendering component with no logic concerns.

Confidence Score: 3/5

  • Needs fixes before merge — silent send failures and ~200 lines of dead SVG components should be resolved.
  • The redesign is architecturally sound and the animation/session model is well-structured, but the missing catch in handleSend causes unhandled promise rejections with no user feedback, and the five unused SVG panel components represent a significant chunk of dead code that either needs to be wired into the scene or removed to avoid confusion.
  • Pay close attention to tinyoffice/src/components/pixel-office-scene.tsx (dead components) and tinyoffice/src/app/office/page.tsx (handleSend error handling, agentHistories polling).

Important Files Changed

Filename Overview
tinyoffice/src/app/office/page.tsx Major redesign of the office page — introduces agent work sessions, animated movement, conversation panel with history, archive overlays, and SSE-driven bubbles. Missing catch in handleSend causes silent unhandled rejections; agentHistories polling resets its interval on every agents refresh and uses Promise.all which fails entirely on a single agent error.
tinyoffice/src/components/pixel-office-scene.tsx New SVG scene component with room layout, lounge, task stations, and animated agents. Five fully-implemented components (QueuePanel, ControlConsole, RoutingPanel, TaskSummaryPanel, ResponseDock) are defined but never rendered inside PixelOfficeScene, and the seven corresponding props are accepted but unused — making a large portion of this file dead code.
tinyoffice/src/components/pixel-office-char.tsx New pure rendering component for pixel-art agent characters — renders head, arms, legs, and hat sprites as SVG rects from palette strings, with animation support for idle, walk, type, celebrate, error, and sleep states. No logic issues found.

Sequence Diagram

sequenceDiagram
    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
Loading

Last reviewed commit: df44ca4

Comment on lines +881 to +908
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[];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 PixelOfficeSceneconnected, 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} />

Comment on lines 387 to +401
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]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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]);

Comment on lines +201 to +211
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],
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants