fix: optimistic archiving/closing and tab UX improvements

- Archive agents optimistically on close with rollback on error
- Close tabs immediately instead of waiting for RPC response
- Kill terminals in background with cache invalidation on failure
- Bulk close fires RPC in background, closes all tabs upfront
- Move new-tab button out of scroll area, rename to "New agent tab"
- Support invalidateQueries option in applyArchivedAgentCloseResults
- Fix workspace descriptor id/projectId types (string not number)
This commit is contained in:
Mohamed Boudra
2026-04-06 08:32:10 +07:00
parent 6362d429c7
commit d3e5d6929d
8 changed files with 212 additions and 135 deletions
@@ -139,4 +139,36 @@ describe("useArchiveAgent", () => {
entries: [{ agent: { id: "agent-2" } }], entries: [{ agent: { id: "agent-2" } }],
}); });
}); });
it("can apply archived agent close results without invalidating cached lists", () => {
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" }],
invalidateQueries: false,
});
expect(queryClient.getQueryState(["sidebarAgentsList", "server-a"])?.isInvalidated).toBe(false);
expect(queryClient.getQueryState(["allAgents", "server-a"])?.isInvalidated).toBe(false);
});
}); });
+116 -9
View File
@@ -29,6 +29,16 @@ interface AgentsListQueryData {
entries?: Array<{ agent?: { id?: string | null } | null } | null>; entries?: Array<{ agent?: { id?: string | null } | null } | null>;
} }
interface ArchivedAgentListCacheSnapshot {
sidebarAgentsList: AgentsListQueryData | undefined;
allAgents: AgentsListQueryData | undefined;
}
interface ArchiveAgentMutationContext {
agent: ReturnType<typeof getStoredAgentSnapshot>;
lists: ArchivedAgentListCacheSnapshot;
}
function toArchiveKey(input: ArchiveAgentInput): string { function toArchiveKey(input: ArchiveAgentInput): string {
const serverId = input.serverId.trim(); const serverId = input.serverId.trim();
const agentId = input.agentId.trim(); const agentId = input.agentId.trim();
@@ -111,6 +121,70 @@ function removeAgentFromCachedLists(queryClient: QueryClient, input: ArchiveAgen
); );
} }
function getStoredAgentSnapshot(input: ArchiveAgentInput) {
return useSessionStore.getState().sessions[input.serverId]?.agents.get(input.agentId);
}
function restoreAgentSnapshot(
input: ArchiveAgentInput & { agent: ReturnType<typeof getStoredAgentSnapshot> },
): void {
const setAgents = useSessionStore.getState().setAgents;
setAgents(input.serverId, (prev) => {
const hasAgent = prev.has(input.agentId);
if (!input.agent) {
if (!hasAgent) {
return prev;
}
const next = new Map(prev);
next.delete(input.agentId);
return next;
}
const current = prev.get(input.agentId);
if (current === input.agent) {
return prev;
}
const next = new Map(prev);
next.set(input.agentId, input.agent);
return next;
});
}
function getArchivedAgentListCacheSnapshot(
queryClient: QueryClient,
serverId: string,
): ArchivedAgentListCacheSnapshot {
return {
sidebarAgentsList: queryClient.getQueryData<AgentsListQueryData | undefined>([
"sidebarAgentsList",
serverId,
]),
allAgents: queryClient.getQueryData<AgentsListQueryData | undefined>(["allAgents", serverId]),
};
}
function restoreCachedListSnapshot(
queryClient: QueryClient,
queryKey: readonly [string, string],
snapshot: AgentsListQueryData | undefined,
): void {
if (snapshot === undefined) {
queryClient.removeQueries({ queryKey, exact: true });
return;
}
queryClient.setQueryData(queryKey, snapshot);
}
function restoreArchivedAgentListCacheSnapshot(
queryClient: QueryClient,
serverId: string,
snapshot: ArchivedAgentListCacheSnapshot,
): void {
restoreCachedListSnapshot(queryClient, ["sidebarAgentsList", serverId], snapshot.sidebarAgentsList);
restoreCachedListSnapshot(queryClient, ["allAgents", serverId], snapshot.allAgents);
}
function markAgentArchivedInStore(input: ArchiveAgentInput & { archivedAt: string }): void { function markAgentArchivedInStore(input: ArchiveAgentInput & { archivedAt: string }): void {
const archivedAt = new Date(input.archivedAt); const archivedAt = new Date(input.archivedAt);
if (Number.isNaN(archivedAt.getTime())) { if (Number.isNaN(archivedAt.getTime())) {
@@ -139,6 +213,7 @@ interface ApplyArchivedAgentCloseResultsInput {
queryClient: QueryClient; queryClient: QueryClient;
serverId: string; serverId: string;
results: ArchivedAgentCloseResult[]; results: ArchivedAgentCloseResult[];
invalidateQueries?: boolean;
} }
export function applyArchivedAgentCloseResults( export function applyArchivedAgentCloseResults(
@@ -160,12 +235,14 @@ export function applyArchivedAgentCloseResults(
}); });
} }
void input.queryClient.invalidateQueries({ if (input.invalidateQueries ?? true) {
queryKey: ["sidebarAgentsList", input.serverId], void input.queryClient.invalidateQueries({
}); queryKey: ["sidebarAgentsList", input.serverId],
void input.queryClient.invalidateQueries({ });
queryKey: ["allAgents", input.serverId], void input.queryClient.invalidateQueries({
}); queryKey: ["allAgents", input.serverId],
});
}
} }
export function clearArchiveAgentPending(input: IsAgentArchivingInput): void { export function clearArchiveAgentPending(input: IsAgentArchivingInput): void {
@@ -195,26 +272,56 @@ export function useArchiveAgent() {
return await client.archiveAgent(input.agentId); return await client.archiveAgent(input.agentId);
}, },
onMutate: (input) => { onMutate: (input) => {
const context: ArchiveAgentMutationContext = {
agent: getStoredAgentSnapshot(input),
lists: getArchivedAgentListCacheSnapshot(queryClient, input.serverId),
};
const archivedAt = new Date().toISOString();
applyArchivedAgentCloseResults({
queryClient,
serverId: input.serverId,
results: [{ agentId: input.agentId, archivedAt }],
invalidateQueries: false,
});
setAgentArchiving({ setAgentArchiving({
queryClient, queryClient,
serverId: input.serverId, serverId: input.serverId,
agentId: input.agentId, agentId: input.agentId,
isArchiving: true, isArchiving: true,
}); });
return context;
}, },
onSuccess: (result, input) => { onSuccess: (result, input) => {
applyArchivedAgentCloseResults({ markAgentArchivedInStore({
queryClient,
serverId: input.serverId, serverId: input.serverId,
results: [{ agentId: input.agentId, archivedAt: result.archivedAt }], agentId: input.agentId,
archivedAt: result.archivedAt,
}); });
}, },
onError: (_error, input, context) => {
if (!context) {
return;
}
restoreAgentSnapshot({
serverId: input.serverId,
agentId: input.agentId,
agent: context.agent,
});
restoreArchivedAgentListCacheSnapshot(queryClient, input.serverId, context.lists);
},
onSettled: (_result, _error, input) => { onSettled: (_result, _error, input) => {
clearArchiveAgentPending({ clearArchiveAgentPending({
queryClient, queryClient,
serverId: input.serverId, serverId: input.serverId,
agentId: input.agentId, agentId: input.agentId,
}); });
void queryClient.invalidateQueries({
queryKey: ["sidebarAgentsList", input.serverId],
});
void queryClient.invalidateQueries({
queryKey: ["allAgents", input.serverId],
});
}, },
}); });
@@ -259,8 +259,8 @@ function getWorkspaceOrderScopeKey(serverId: string, projectKey: string): string
} }
function toWorkspaceDescriptor(payload: { function toWorkspaceDescriptor(payload: {
id: number; id: string;
projectId: number; projectId: string;
projectDisplayName: string; projectDisplayName: string;
projectRootPath: string; projectRootPath: string;
workspaceDirectory: string; workspaceDirectory: string;
@@ -73,7 +73,7 @@ describe("workspace bulk close helpers", () => {
); );
}); });
it("uses one mixed closeItems RPC for agent and terminal tabs, then applies local cleanup", async () => { it("closes all tabs immediately and fires one mixed closeItems RPC in the background", async () => {
const groups = classifyBulkClosableTabs([ const groups = classifyBulkClosableTabs([
makeAgentTab("a1"), makeAgentTab("a1"),
makeTerminalTab("t1"), makeTerminalTab("t1"),
@@ -91,7 +91,7 @@ describe("workspace bulk close helpers", () => {
requestId: "req-1", requestId: "req-1",
})); }));
const result = await closeBulkWorkspaceTabs({ await closeBulkWorkspaceTabs({
groups, groups,
client: { closeItems }, client: { closeItems },
closeTab: async (tabId, action) => { closeTab: async (tabId, action) => {
@@ -109,23 +109,21 @@ describe("workspace bulk close helpers", () => {
agentIds: ["a1"], agentIds: ["a1"],
terminalIds: ["t1", "t2"], terminalIds: ["t1", "t2"],
}); });
expect(result).toEqual({ expect(closedTabIds).toEqual([
agents: [{ agentId: "a1", archivedAt: "2026-04-01T04:00:00.000Z" }], "agent_a1",
terminals: [ "terminal_t1",
{ terminalId: "t1", success: true }, "terminal_t2",
{ terminalId: "t2", success: false }, "file_/repo/README.md",
], ]);
requestId: "req-1",
});
expect(closedTabIds).toEqual(["agent_a1", "terminal_t1", "file_/repo/README.md"]);
expect(cleanupCalls).toEqual([ expect(cleanupCalls).toEqual([
{ tabId: "agent_a1", target: { kind: "agent", agentId: "a1" } }, { tabId: "agent_a1", target: { kind: "agent", agentId: "a1" } },
{ tabId: "terminal_t1", target: { kind: "terminal", terminalId: "t1" } }, { tabId: "terminal_t1", target: { kind: "terminal", terminalId: "t1" } },
{ tabId: "terminal_t2", target: { kind: "terminal", terminalId: "t2" } },
{ tabId: "file_/repo/README.md" }, { tabId: "file_/repo/README.md" },
]); ]);
}); });
it("still closes passive tabs when the mixed closeItems RPC fails", async () => { it("still closes all tabs when the mixed closeItems RPC fails", async () => {
const groups = classifyBulkClosableTabs([ const groups = classifyBulkClosableTabs([
makeAgentTab("a1"), makeAgentTab("a1"),
makeTerminalTab("t1"), makeTerminalTab("t1"),
@@ -135,7 +133,7 @@ describe("workspace bulk close helpers", () => {
const cleanupCalls: Array<{ tabId: string; target?: WorkspaceTabDescriptor["target"] }> = []; const cleanupCalls: Array<{ tabId: string; target?: WorkspaceTabDescriptor["target"] }> = [];
const warn = vi.fn(); const warn = vi.fn();
const result = await closeBulkWorkspaceTabs({ await closeBulkWorkspaceTabs({
groups, groups,
client: { client: {
closeItems: async () => { closeItems: async () => {
@@ -153,9 +151,14 @@ describe("workspace bulk close helpers", () => {
logLabel: "others", logLabel: "others",
}); });
await Promise.resolve();
expect(warn).toHaveBeenCalledTimes(1); expect(warn).toHaveBeenCalledTimes(1);
expect(result).toBeNull(); expect(closedTabIds).toEqual(["agent_a1", "terminal_t1", "file_/repo/README.md"]);
expect(closedTabIds).toEqual(["file_/repo/README.md"]); expect(cleanupCalls).toEqual([
expect(cleanupCalls).toEqual([{ tabId: "file_/repo/README.md" }]); { tabId: "agent_a1", target: { kind: "agent", agentId: "a1" } },
{ tabId: "terminal_t1", target: { kind: "terminal", terminalId: "t1" } },
{ tabId: "file_/repo/README.md" },
]);
}); });
}); });
@@ -7,8 +7,6 @@ export type BulkClosableTabGroups = {
otherTabs: Array<{ tabId: string }>; otherTabs: Array<{ tabId: string }>;
}; };
type CloseItemsPayload = Awaited<ReturnType<DaemonClient["closeItems"]>>;
interface CloseWorkspaceTabWithCleanupInput { interface CloseWorkspaceTabWithCleanupInput {
tabId: string; tabId: string;
target?: WorkspaceTabDescriptor["target"]; target?: WorkspaceTabDescriptor["target"];
@@ -68,47 +66,27 @@ export function buildBulkCloseConfirmationMessage(input: BulkClosableTabGroups):
return `This will archive ${agentTabs.length} agent(s).`; return `This will archive ${agentTabs.length} agent(s).`;
} }
function toSuccessfulAgentIds(payload: CloseItemsPayload | null): Set<string> { export async function closeBulkWorkspaceTabs(input: CloseBulkWorkspaceTabsInput): Promise<void> {
return new Set(payload?.agents.map((agent) => agent.agentId) ?? []);
}
function toSuccessfulTerminalIds(payload: CloseItemsPayload | null): Set<string> {
return new Set(
payload?.terminals.filter((terminal) => terminal.success).map((terminal) => terminal.terminalId) ??
[],
);
}
export async function closeBulkWorkspaceTabs(
input: CloseBulkWorkspaceTabsInput,
): Promise<CloseItemsPayload | null> {
const { client, groups, closeTab, closeWorkspaceTabWithCleanup, logLabel, warn } = input; const { client, groups, closeTab, closeWorkspaceTabWithCleanup, logLabel, warn } = input;
const hasDestructiveTabs = groups.agentTabs.length > 0 || groups.terminalTabs.length > 0; const hasDestructiveTabs = groups.agentTabs.length > 0 || groups.terminalTabs.length > 0;
let payload: CloseItemsPayload | null = null;
if (hasDestructiveTabs && client) { if (hasDestructiveTabs && client) {
try { void client
payload = await client.closeItems({ .closeItems({
agentIds: groups.agentTabs.map((tab) => tab.agentId), agentIds: groups.agentTabs.map((tab) => tab.agentId),
terminalIds: groups.terminalTabs.map((tab) => tab.terminalId), terminalIds: groups.terminalTabs.map((tab) => tab.terminalId),
})
.catch((error) => {
warn?.(`[WorkspaceScreen] Failed to bulk close tabs ${logLabel}`, { error });
}); });
} catch (error) {
warn?.(`[WorkspaceScreen] Failed to bulk close tabs ${logLabel}`, { error });
}
} else if (hasDestructiveTabs) { } else if (hasDestructiveTabs) {
warn?.(`[WorkspaceScreen] Failed to bulk close tabs ${logLabel}`, { warn?.(`[WorkspaceScreen] Failed to bulk close tabs ${logLabel}`, {
error: new Error("Daemon client not available"), error: new Error("Daemon client not available"),
}); });
} }
const successfulAgentIds = toSuccessfulAgentIds(payload);
const successfulTerminalIds = toSuccessfulTerminalIds(payload);
for (const { tabId, agentId } of groups.agentTabs) { for (const { tabId, agentId } of groups.agentTabs) {
if (!successfulAgentIds.has(agentId)) { void closeTab(tabId, async () => {
continue;
}
await closeTab(tabId, async () => {
closeWorkspaceTabWithCleanup({ closeWorkspaceTabWithCleanup({
tabId, tabId,
target: { kind: "agent", agentId }, target: { kind: "agent", agentId },
@@ -117,10 +95,7 @@ export async function closeBulkWorkspaceTabs(
} }
for (const { tabId, terminalId } of groups.terminalTabs) { for (const { tabId, terminalId } of groups.terminalTabs) {
if (!successfulTerminalIds.has(terminalId)) { void closeTab(tabId, async () => {
continue;
}
await closeTab(tabId, async () => {
closeWorkspaceTabWithCleanup({ closeWorkspaceTabWithCleanup({
tabId, tabId,
target: { kind: "terminal", terminalId }, target: { kind: "terminal", terminalId },
@@ -129,10 +104,8 @@ export async function closeBulkWorkspaceTabs(
} }
for (const { tabId } of groups.otherTabs) { for (const { tabId } of groups.otherTabs) {
await closeTab(tabId, async () => { void closeTab(tabId, async () => {
closeWorkspaceTabWithCleanup({ tabId }); closeWorkspaceTabWithCleanup({ tabId });
}); });
} }
return payload;
} }
@@ -14,8 +14,8 @@ import {
ArrowRightToLine, ArrowRightToLine,
Columns2, Columns2,
Copy, Copy,
Plus,
Rows2, Rows2,
SquarePen,
SquareTerminal, SquareTerminal,
X, X,
} from "lucide-react-native"; } from "lucide-react-native";
@@ -473,42 +473,36 @@ export function WorkspaceDesktopTabsRow({
); );
}} }}
/> />
</ScrollView>
<View style={styles.tabsActions} onLayout={handleTabsActionsLayout}>
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}> <Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger asChild> <TooltipTrigger
<Pressable testID="workspace-new-agent-tab"
testID="workspace-new-tab" onPress={() => onCreateDraftTab({ paneId })}
onPress={() => onCreateDraftTab({ paneId })} accessibilityRole="button"
accessibilityRole="button" accessibilityLabel="New agent tab"
accessibilityLabel="New tab" style={({ hovered, pressed }) => [
style={styles.inlineNewTabButton} styles.newTabActionButton,
> (hovered || pressed) && styles.newTabActionButtonHovered,
{({ hovered, pressed }) => ( ]}
<Plus >
size={theme.iconSize.md} <SquarePen size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
color={
hovered || pressed ? theme.colors.foreground : theme.colors.foregroundMuted
}
/>
)}
</Pressable>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="bottom" align="center" offset={8}> <TooltipContent side="bottom" align="center" offset={8}>
<View style={styles.newTabTooltipRow}> <View style={styles.newTabTooltipRow}>
<Text style={styles.newTabTooltipText}>New tab</Text> <Text style={styles.newTabTooltipText}>New agent tab</Text>
{newTabKeys ? ( {newTabKeys ? (
<Shortcut chord={newTabKeys} style={styles.newTabTooltipShortcut} /> <Shortcut chord={newTabKeys} style={styles.newTabTooltipShortcut} />
) : null} ) : null}
</View> </View>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</ScrollView>
<View style={styles.tabsActions} onLayout={handleTabsActionsLayout}>
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}> <Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger <TooltipTrigger
testID="workspace-new-terminal" testID="workspace-new-terminal"
onPress={() => onCreateTerminalTab({ paneId })} onPress={() => onCreateTerminalTab({ paneId })}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel="New terminal" accessibilityLabel="New terminal tab"
style={({ hovered, pressed }) => [ style={({ hovered, pressed }) => [
styles.newTabActionButton, styles.newTabActionButton,
(hovered || pressed) && styles.newTabActionButtonHovered, (hovered || pressed) && styles.newTabActionButtonHovered,
@@ -518,7 +512,7 @@ export function WorkspaceDesktopTabsRow({
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="bottom" align="center" offset={8}> <TooltipContent side="bottom" align="center" offset={8}>
<View style={styles.newTabTooltipRow}> <View style={styles.newTabTooltipRow}>
<Text style={styles.newTabTooltipText}>New terminal</Text> <Text style={styles.newTabTooltipText}>New terminal tab</Text>
{newTerminalKeys ? ( {newTerminalKeys ? (
<Shortcut chord={newTerminalKeys} style={styles.newTabTooltipShortcut} /> <Shortcut chord={newTerminalKeys} style={styles.newTabTooltipShortcut} />
) : null} ) : null}
@@ -819,13 +813,6 @@ const styles = StyleSheet.create((theme) => ({
tabCloseButtonActive: { tabCloseButtonActive: {
backgroundColor: theme.colors.surface3, backgroundColor: theme.colors.surface3,
}, },
inlineNewTabButton: {
paddingHorizontal: theme.spacing[3],
height: "100%",
alignItems: "center",
justifyContent: "center",
borderRadius: 0,
},
newTabActionButton: { newTabActionButton: {
width: 22, width: 22,
height: 22, height: 22,
@@ -63,7 +63,6 @@ import type { WorkspaceTab, WorkspaceTabTarget } from "@/stores/workspace-tabs-s
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler"; import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher"; import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher";
import { useCreateFlowStore } from "@/stores/create-flow-store"; import { useCreateFlowStore } from "@/stores/create-flow-store";
import { decodeWorkspaceIdFromPathSegment } from "@/utils/host-routes";
import { import {
normalizeWorkspaceTabTarget, normalizeWorkspaceTabTarget,
workspaceTabTargetsEqual, workspaceTabTargetsEqual,
@@ -79,7 +78,7 @@ import {
import type { ListTerminalsResponse } from "@server/shared/messages"; import type { ListTerminalsResponse } from "@server/shared/messages";
import { upsertTerminalListEntry } from "@/utils/terminal-list"; import { upsertTerminalListEntry } from "@/utils/terminal-list";
import { confirmDialog } from "@/utils/confirm-dialog"; import { confirmDialog } from "@/utils/confirm-dialog";
import { applyArchivedAgentCloseResults, useArchiveAgent } from "@/hooks/use-archive-agent"; import { useArchiveAgent } from "@/hooks/use-archive-agent";
import { useStableEvent } from "@/hooks/use-stable-event"; import { useStableEvent } from "@/hooks/use-stable-event";
import { buildProviderCommand } from "@/utils/provider-command-templates"; import { buildProviderCommand } from "@/utils/provider-command-templates";
import { generateDraftId } from "@/stores/draft-keys"; import { generateDraftId } from "@/stores/draft-keys";
@@ -607,7 +606,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
const normalizedWorkspaceId = const normalizedWorkspaceId =
resolveWorkspaceRouteId({ resolveWorkspaceRouteId({
routeWorkspaceId: decodeWorkspaceIdFromPathSegment(workspaceId), routeWorkspaceId: workspaceId,
}) ?? ""; }) ?? "";
const sessionWorkspaces = useSessionStore( const sessionWorkspaces = useSessionStore(
(state) => state.sessions[normalizedServerId]?.workspaces, (state) => state.sessions[normalizedServerId]?.workspaces,
@@ -1198,6 +1197,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
if (!workspaceDirectory) { if (!workspaceDirectory) {
return; return;
} }
createTerminalMutation.mutate(input); createTerminalMutation.mutate(input);
}, },
[createTerminalMutation, workspaceDirectory], [createTerminalMutation, workspaceDirectory],
@@ -1243,10 +1243,6 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
return; return;
} }
await killTerminalAsync(terminalId);
setHoveredTabKey((current) => (current === tabId ? null : current));
setHoveredCloseTabKey((current) => (current === tabId ? null : current));
queryClient.setQueryData<ListTerminalsPayload>(terminalsQueryKey, (current) => { queryClient.setQueryData<ListTerminalsPayload>(terminalsQueryKey, (current) => {
if (!current) { if (!current) {
return current; return current;
@@ -1256,13 +1252,18 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
terminals: current.terminals.filter((terminal) => terminal.id !== terminalId), terminals: current.terminals.filter((terminal) => terminal.id !== terminalId),
}; };
}); });
setHoveredTabKey((current) => (current === tabId ? null : current));
setHoveredCloseTabKey((current) => (current === tabId ? null : current));
if (persistenceKey) { if (persistenceKey) {
closeWorkspaceTabWithCleanup({ closeWorkspaceTabWithCleanup({
tabId, tabId,
target: { kind: "terminal", terminalId }, target: { kind: "terminal", terminalId },
}); });
} }
void killTerminalAsync(terminalId).catch(() => {
void queryClient.invalidateQueries({ queryKey: terminalsQueryKey });
});
}); });
}, },
[ [
@@ -1294,7 +1295,6 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
return; return;
} }
await archiveAgent({ serverId: normalizedServerId, agentId });
setHoveredTabKey((current) => (current === tabId ? null : current)); setHoveredTabKey((current) => (current === tabId ? null : current));
setHoveredCloseTabKey((current) => (current === tabId ? null : current)); setHoveredCloseTabKey((current) => (current === tabId ? null : current));
if (persistenceKey) { if (persistenceKey) {
@@ -1303,6 +1303,8 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
target: { kind: "agent", agentId }, target: { kind: "agent", agentId },
}); });
} }
void archiveAgent({ serverId: normalizedServerId, agentId });
}); });
}, },
[archiveAgent, closeTab, closeWorkspaceTabWithCleanup, normalizedServerId, persistenceKey], [archiveAgent, closeTab, closeWorkspaceTabWithCleanup, normalizedServerId, persistenceKey],
@@ -1464,7 +1466,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
return; return;
} }
const closeItemsPayload = await closeBulkWorkspaceTabs({ await closeBulkWorkspaceTabs({
client, client,
groups, groups,
closeTab, closeTab,
@@ -1480,31 +1482,6 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
}, },
}); });
if (closeItemsPayload) {
for (const terminal of closeItemsPayload.terminals) {
if (!terminal.success) {
continue;
}
queryClient.setQueryData<ListTerminalsPayload>(terminalsQueryKey, (current) => {
if (!current) {
return current;
}
return {
...current,
terminals: current.terminals.filter((entry) => entry.id !== terminal.terminalId),
};
});
}
if (normalizedServerId) {
applyArchivedAgentCloseResults({
queryClient,
serverId: normalizedServerId,
results: closeItemsPayload.agents,
});
}
}
const closedKeys = new Set(tabsToClose.map((tab) => tab.key)); const closedKeys = new Set(tabsToClose.map((tab) => tab.key));
setHoveredTabKey((current) => (current && closedKeys.has(current) ? null : current)); setHoveredTabKey((current) => (current && closedKeys.has(current) ? null : current));
setHoveredCloseTabKey((current) => (current && closedKeys.has(current) ? null : current)); setHoveredCloseTabKey((current) => (current && closedKeys.has(current) ? null : current));
@@ -1513,10 +1490,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
client, client,
closeTab, closeTab,
closeWorkspaceTabWithCleanup, closeWorkspaceTabWithCleanup,
normalizedServerId,
persistenceKey, persistenceKey,
queryClient,
terminalsQueryKey,
], ],
); );
@@ -42,8 +42,8 @@ describe("normalizeWorkspaceDescriptor", () => {
}, },
]; ];
const workspace = normalizeWorkspaceDescriptor({ const workspace = normalizeWorkspaceDescriptor({
id: 1, id: "1",
projectId: 1, projectId: "1",
projectDisplayName: "Project 1", projectDisplayName: "Project 1",
projectRootPath: "/repo", projectRootPath: "/repo",
workspaceDirectory: "/repo", workspaceDirectory: "/repo",
@@ -71,8 +71,8 @@ describe("normalizeWorkspaceDescriptor", () => {
it("defaults missing services to an empty array", () => { it("defaults missing services to an empty array", () => {
const payload = { const payload = {
id: 1, id: "1",
projectId: 1, projectId: "1",
projectDisplayName: "Project 1", projectDisplayName: "Project 1",
projectRootPath: "/repo", projectRootPath: "/repo",
workspaceDirectory: "/repo", workspaceDirectory: "/repo",
@@ -82,6 +82,7 @@ describe("normalizeWorkspaceDescriptor", () => {
status: "done", status: "done",
activityAt: null, activityAt: null,
diffStat: null, diffStat: null,
services: [],
} as WorkspaceDescriptorPayload; } as WorkspaceDescriptorPayload;
const workspace = normalizeWorkspaceDescriptor(payload); const workspace = normalizeWorkspaceDescriptor(payload);