[codex] add batch close rpc for workspace tabs (#165)

* style: add subtle teal tint to dark mode surfaces

Replace neutral gray surfaces with a teal-tinted palette across
the app and website, giving Paseo a warmer, more recognizable feel.
App uses a restrained tint (G-R ~3), website is slightly stronger
(G-R ~6) as a brand showcase.

* fix(opencode): handle message.part.delta events for assistant text streaming

OpenCode v2 streams assistant text via `message.part.delta` events (with
field "text" or "reasoning"), but the translator only handled
`message.part.updated`. This caused assistant messages to be silently
dropped during live streaming.

* feat: show agent short ID in tab context menu and tooltip (#161)

Add agent short ID to the "Copy agent id" menu item as trailing hint
text and to the tab tooltip next to the title. Add leading icons to
all workspace tab context menu items.

* add batch close rpc for workspace tabs
This commit is contained in:
Mohamed Boudra
2026-04-01 14:31:38 +08:00
committed by GitHub
parent 8d55764313
commit f53c770a71
22 changed files with 1253 additions and 131 deletions
@@ -1,8 +1,46 @@
import type { DaemonClient } from "@server/client/daemon-client";
import { QueryClient } from "@tanstack/react-query";
import { describe, expect, it } from "vitest";
import { __private__ } from "./use-archive-agent";
import { beforeEach, describe, expect, it } from "vitest";
import type { Agent } from "@/stores/session-store";
import { useSessionStore } from "@/stores/session-store";
import { __private__, applyArchivedAgentCloseResults } from "./use-archive-agent";
function makeAgent(overrides: Partial<Agent> = {}): Agent {
return {
serverId: "server-a",
id: "agent-1",
provider: "codex",
status: "running",
createdAt: new Date("2026-04-01T03:00:00.000Z"),
updatedAt: new Date("2026-04-01T03:00:00.000Z"),
lastUserMessageAt: null,
lastActivityAt: new Date("2026-04-01T03:00:00.000Z"),
capabilities: {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: true,
supportsMcpServers: true,
supportsReasoningStream: true,
supportsToolInvocations: true,
},
currentModeId: null,
availableModes: [],
pendingPermissions: [],
persistence: null,
title: "Agent 1",
cwd: "/repo",
model: null,
labels: {},
archivedAt: null,
...overrides,
};
}
describe("useArchiveAgent", () => {
beforeEach(() => {
useSessionStore.setState((state) => ({ ...state, sessions: {} }));
});
it("tracks pending archive state in shared react-query cache", () => {
const queryClient = new QueryClient();
@@ -63,4 +101,42 @@ describe("useArchiveAgent", () => {
expect(next.entries).toEqual([{ agent: { id: "agent-2" } }]);
expect(next.pageInfo).toEqual({ hasMore: false });
});
it("applies archived agent close results to session state and cached lists", async () => {
const queryClient = new QueryClient();
useSessionStore
.getState()
.initializeSession("server-a", {} as DaemonClient);
useSessionStore.getState().setAgents(
"server-a",
new Map([
[
"agent-1",
makeAgent(),
],
]),
);
queryClient.setQueryData(["sidebarAgentsList", "server-a"], {
entries: [{ agent: { id: "agent-1" } }, { agent: { id: "agent-2" } }],
});
queryClient.setQueryData(["allAgents", "server-a"], {
entries: [{ agent: { id: "agent-1" } }, { agent: { id: "agent-2" } }],
});
applyArchivedAgentCloseResults({
queryClient,
serverId: "server-a",
results: [{ agentId: "agent-1", archivedAt: "2026-04-01T04:00:00.000Z" }],
});
expect(
useSessionStore.getState().sessions["server-a"]?.agents.get("agent-1")?.archivedAt?.toISOString(),
).toBe("2026-04-01T04:00:00.000Z");
expect(queryClient.getQueryData(["sidebarAgentsList", "server-a"])).toEqual({
entries: [{ agent: { id: "agent-2" } }],
});
expect(queryClient.getQueryData(["allAgents", "server-a"])).toEqual({
entries: [{ agent: { id: "agent-2" } }],
});
});
});
+41 -10
View File
@@ -9,6 +9,11 @@ export interface ArchiveAgentInput {
agentId: string;
}
export interface ArchivedAgentCloseResult {
agentId: string;
archivedAt: string;
}
type ArchiveAgentPendingState = Record<string, true>;
interface SetAgentArchivingInput extends ArchiveAgentInput {
@@ -130,6 +135,39 @@ function markAgentArchivedInStore(input: ArchiveAgentInput & { archivedAt: strin
});
}
interface ApplyArchivedAgentCloseResultsInput {
queryClient: QueryClient;
serverId: string;
results: ArchivedAgentCloseResult[];
}
export function applyArchivedAgentCloseResults(
input: ApplyArchivedAgentCloseResultsInput,
): void {
if (input.results.length === 0) {
return;
}
for (const result of input.results) {
markAgentArchivedInStore({
serverId: input.serverId,
agentId: result.agentId,
archivedAt: result.archivedAt,
});
removeAgentFromCachedLists(input.queryClient, {
serverId: input.serverId,
agentId: result.agentId,
});
}
void input.queryClient.invalidateQueries({
queryKey: ["sidebarAgentsList", input.serverId],
});
void input.queryClient.invalidateQueries({
queryKey: ["allAgents", input.serverId],
});
}
export function clearArchiveAgentPending(input: IsAgentArchivingInput): void {
setAgentArchiving({
...input,
@@ -165,17 +203,10 @@ export function useArchiveAgent() {
});
},
onSuccess: (result, input) => {
markAgentArchivedInStore({
applyArchivedAgentCloseResults({
queryClient,
serverId: input.serverId,
agentId: input.agentId,
archivedAt: result.archivedAt,
});
removeAgentFromCachedLists(queryClient, input);
void queryClient.invalidateQueries({
queryKey: ["sidebarAgentsList", input.serverId],
});
void queryClient.invalidateQueries({
queryKey: ["allAgents", input.serverId],
results: [{ agentId: input.agentId, archivedAt: result.archivedAt }],
});
},
onSettled: (_result, _error, input) => {