Skip to content
Open
7 changes: 6 additions & 1 deletion src/browser/components/WorkspaceHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useCallback, useEffect, useState } from "react";
import { Pencil } from "lucide-react";
import { WorkspaceStatusIndicator } from "./WorkspaceStatusIndicator";
import { GitStatusIndicator } from "./GitStatusIndicator";
import { RuntimeBadge } from "./RuntimeBadge";
import { Tooltip, TooltipTrigger, TooltipContent } from "./ui/tooltip";
Expand Down Expand Up @@ -79,7 +80,11 @@ export const WorkspaceHeader: React.FC<WorkspaceHeaderProps> = ({
{namedWorkspacePath}
</span>
</div>
<div className="flex items-center">
<div className="flex min-w-0 items-center gap-2">
<div className="min-w-0">
<WorkspaceStatusIndicator workspaceId={workspaceId} />
</div>

{editorError && <span className="text-danger-soft mr-2 text-xs">{editorError}</span>}
<Tooltip>
<TooltipTrigger asChild>
Expand Down
9 changes: 7 additions & 2 deletions src/browser/components/tools/StatusSetToolCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,16 @@ export const StatusSetToolCall: React.FC<StatusSetToolCallProps> = ({
? String(result.error)
: undefined;

const iconEmoji = "📡";

const pollLabel = args.poll_interval_s === undefined ? "once" : `${args.poll_interval_s}s`;
const summary = `poll=${pollLabel}: ${args.script.split(/\r?\n/)[0] ?? ""}`;

return (
<ToolContainer expanded={false}>
<ToolHeader>
<ToolIcon emoji={args.emoji} toolName="status_set" />
<span className="text-muted-foreground italic">{args.message}</span>
<ToolIcon emoji={iconEmoji} toolName="status_set" />
<span className="text-muted-foreground italic">{summary}</span>
{errorMessage && <span className="text-error-foreground">({errorMessage})</span>}
<StatusIndicator status={status}>{statusDisplay}</StatusIndicator>
</ToolHeader>
Expand Down
28 changes: 28 additions & 0 deletions src/browser/stores/WorkspaceStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,34 @@ describe("WorkspaceStore", () => {
});
});

describe("agent status updates", () => {
it("should surface agent-status-update events in workspace sidebar state", async () => {
const workspaceId = "status-workspace";

mockOnChat.mockImplementation(async function* (): AsyncGenerator<
WorkspaceChatMessage,
void,
unknown
> {
await Promise.resolve();
yield { type: "caught-up" };
yield {
type: "agent-status-update",
workspaceId,
status: { message: "Building", url: "https://example.com/pr/1" },
};
});

createAndAddWorkspace(store, workspaceId);

// Wait for the async iterator loop to process yields
await new Promise((resolve) => setTimeout(resolve, 10));

const state = store.getWorkspaceSidebarState(workspaceId);
expect(state.agentStatus).toEqual({ message: "Building", url: "https://example.com/pr/1" });
});
});

describe("syncWorkspaces", () => {
it("should add new workspaces", () => {
const metadata1: FrontendWorkspaceMetadata = {
Expand Down
15 changes: 12 additions & 3 deletions src/browser/stores/WorkspaceStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export interface WorkspaceState {
currentModel: string | null;
recencyTimestamp: number | null;
todos: TodoItem[];
agentStatus: { emoji: string; message: string; url?: string } | undefined;
agentStatus: { emoji?: string; message: string; url?: string } | undefined;
pendingStreamStartTime: number | null;
}

Expand All @@ -55,7 +55,7 @@ export interface WorkspaceSidebarState {
canInterrupt: boolean;
currentModel: string | null;
recencyTimestamp: number | null;
agentStatus: { emoji: string; message: string; url?: string } | undefined;
agentStatus: { emoji?: string; message: string; url?: string } | undefined;
}

/**
Expand Down Expand Up @@ -262,6 +262,15 @@ export class WorkspaceStore {
aggregator.handleUsageDelta(data as never);
this.usageStore.bump(workspaceId);
},
"agent-status-update": (workspaceId, aggregator, data) => {
// Agent status updates are delivered as chat events (not persisted in history).
// They must:
// 1) be buffered during initial history replay (so they aren't dropped), and
// 2) bump UI state so sidebar/header indicators update immediately.
aggregator.handleMessage(data);
this.states.bump(workspaceId);
},

"init-start": (workspaceId, aggregator, data) => {
aggregator.handleMessage(data);
this.states.bump(workspaceId);
Expand Down Expand Up @@ -943,7 +952,7 @@ export class WorkspaceStore {

/**
* Check if data is a buffered event type by checking the handler map.
* This ensures isStreamEvent() and processStreamEvent() can never fall out of sync.
* This ensures buffering and processStreamEvent() can never fall out of sync.
*/
private isBufferedEvent(data: WorkspaceChatMessage): boolean {
return "type" in data && data.type in this.bufferedEventHandlers;
Expand Down
5 changes: 2 additions & 3 deletions src/browser/stories/App.chat.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,8 @@ export const WithAgentStatus: AppStory = {
toolCalls: [
createStatusTool(
"call-1",
"🚀",
"PR #1234 waiting for CI",
"https://github.com/example/repo/pull/1234"
"echo '🚀 PR #1234 waiting for CI https://github.com/example/repo/pull/1234'",
5
),
],
}
Expand Down
5 changes: 2 additions & 3 deletions src/browser/stories/App.demo.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,8 @@ export const Comprehensive: AppStory = {
toolCalls: [
createStatusTool(
"call-4",
"🚀",
"PR #1234 waiting for CI",
"https://github.com/example/repo/pull/1234"
"echo '🚀 PR #1234 waiting for CI https://github.com/example/repo/pull/1234'",
5
),
],
}),
Expand Down
9 changes: 4 additions & 5 deletions src/browser/stories/mockFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,17 +291,16 @@ export function createTerminalTool(

export function createStatusTool(
toolCallId: string,
emoji: string,
message: string,
url?: string
script: string,
pollIntervalS?: number
): MuxPart {
return {
type: "dynamic-tool",
toolCallId,
toolName: "status_set",
state: "output-available",
input: { emoji, message, url },
output: { success: true, emoji, message, url },
input: { script, ...(pollIntervalS !== undefined ? { poll_interval_s: pollIntervalS } : {}) },
output: { success: true },
};
}

Expand Down
Loading
Loading