[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
@@ -1927,6 +1927,59 @@ describe("DaemonClient", () => {
]);
});
test("sends close_items_request and resolves close_items_response", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const responsePromise = client.closeItems(
{
agentIds: ["agent-1"],
terminalIds: ["term-1"],
},
"req-close-items",
);
expect(JSON.parse(String(mock.sent[0]))).toEqual({
type: "session",
message: {
type: "close_items_request",
agentIds: ["agent-1"],
terminalIds: ["term-1"],
requestId: "req-close-items",
},
});
mock.triggerMessage(
wrapSessionMessage({
type: "close_items_response",
payload: {
agents: [{ agentId: "agent-1", archivedAt: "2026-04-01T00:00:00.000Z" }],
terminals: [{ terminalId: "term-1", success: true }],
requestId: "req-close-items",
},
}),
);
await expect(responsePromise).resolves.toEqual({
agents: [{ agentId: "agent-1", archivedAt: "2026-04-01T00:00:00.000Z" }],
terminals: [{ terminalId: "term-1", success: true }],
requestId: "req-close-items",
});
});
test("waitForFinish with timeout=0 omits timeoutMs and has no client deadline", async () => {
vi.useFakeTimers();
try {
@@ -43,6 +43,7 @@ import type {
CreateTerminalResponse,
SubscribeTerminalResponse,
TerminalState,
CloseItemsResponse,
KillTerminalResponse,
CaptureTerminalResponse,
TerminalInput,
@@ -237,6 +238,7 @@ type AgentPermissionResolvedPayload = AgentPermissionResolvedMessage["payload"];
type ListTerminalsPayload = ListTerminalsResponse["payload"];
type CreateTerminalPayload = CreateTerminalResponse["payload"];
type SubscribeTerminalPayload = SubscribeTerminalResponse["payload"];
type CloseItemsPayload = CloseItemsResponse["payload"];
type KillTerminalPayload = KillTerminalResponse["payload"];
type CaptureTerminalPayload = CaptureTerminalResponse["payload"];
type ChatCreatePayload = Extract<
@@ -2829,6 +2831,26 @@ export class DaemonClient {
});
}
async closeItems(
input: { agentIds?: string[]; terminalIds?: string[] },
requestId?: string,
): Promise<CloseItemsPayload> {
const resolvedRequestId = this.createRequestId(requestId);
const message = SessionInboundMessageSchema.parse({
type: "close_items_request",
agentIds: input.agentIds ?? [],
terminalIds: input.terminalIds ?? [],
requestId: resolvedRequestId,
});
return this.sendCorrelatedRequest({
requestId: resolvedRequestId,
message,
responseType: "close_items_response",
timeout: 10000,
options: { skipQueue: true },
});
}
async captureTerminal(
terminalId: string,
options?: { start?: number; end?: number; stripAnsi?: boolean },
@@ -792,6 +792,52 @@ export function translateOpenCodeEvent(
break;
}
case "message.part.delta": {
const deltaSessionId = props.sessionID as string | undefined;
if (deltaSessionId !== state.sessionId) {
break;
}
const deltaMessageId = props.messageID as string | undefined;
const deltaMessageRole = deltaMessageId
? state.messageRoles.get(deltaMessageId)
: undefined;
const deltaField = props.field as string | undefined;
const deltaText = props.delta as string | undefined;
if (!deltaText || !deltaField) {
break;
}
if (deltaField === "text") {
if (deltaMessageRole === "user") {
break;
}
const partId = props.partID as string | undefined;
const partKey = partId ? `text:${partId}` : null;
if (partKey) {
state.streamedPartKeys.add(partKey);
}
events.push({
type: "timeline",
provider: "opencode",
item: { type: "assistant_message", text: deltaText },
});
} else if (deltaField === "reasoning") {
const partId = props.partID as string | undefined;
const partKey = partId ? `reasoning:${partId}` : null;
if (partKey) {
state.streamedPartKeys.add(partKey);
}
events.push({
type: "timeline",
provider: "opencode",
item: { type: "reasoning", text: deltaText },
});
}
break;
}
case "permission.asked": {
const sessionId = props.sessionID as string | undefined;
if (sessionId !== state.sessionId) {
@@ -1095,7 +1141,6 @@ class OpenCodeAgentSession implements AgentSession {
const turnId = this.createTurnId();
this.activeForegroundTurnId = turnId;
void this.consumeEventStream();
return { turnId };
@@ -0,0 +1,73 @@
import { describe, test, expect } from "vitest";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import pino from "pino";
import { OpenCodeAgentClient } from "./opencode-agent.js";
import { isProviderAvailable } from "../../daemon-e2e/agent-configs.js";
import type { AgentStreamEvent } from "../agent-sdk-types.js";
describe("OpenCode assistant message", () => {
test.runIf(isProviderAvailable("opencode"))(
"assistant_message appears in live stream with opencode/big-pickle",
async () => {
const cwd = mkdtempSync(path.join(tmpdir(), "opencode-msg-"));
const logger = pino({ level: "silent" });
const client = new OpenCodeAgentClient(logger);
try {
const session = await client.createSession({
provider: "opencode",
cwd,
model: "opencode/big-pickle",
modeId: "build",
});
const result = await session.run("Say hello back in one sentence.");
const assistantItems = result.timeline.filter(
(item) => item.type === "assistant_message",
);
expect(assistantItems.length).toBeGreaterThan(0);
expect(result.finalText.length).toBeGreaterThan(0);
} finally {
rmSync(cwd, { recursive: true, force: true });
}
},
60_000,
);
test.runIf(isProviderAvailable("opencode"))(
"streamHistory returns assistant_message after a completed turn",
async () => {
const cwd = mkdtempSync(path.join(tmpdir(), "opencode-history-"));
const logger = pino({ level: "silent" });
const client = new OpenCodeAgentClient(logger);
try {
const session = await client.createSession({
provider: "opencode",
cwd,
});
const result = await session.run("Say hello back in one sentence.");
expect(result.timeline.some((item) => item.type === "assistant_message")).toBe(true);
const historyEvents: AgentStreamEvent[] = [];
for await (const event of session.streamHistory()) {
historyEvents.push(event);
}
const historyAssistant = historyEvents.filter(
(e) => e.type === "timeline" && e.item.type === "assistant_message",
);
expect(historyAssistant.length).toBeGreaterThan(0);
} finally {
rmSync(cwd, { recursive: true, force: true });
}
},
60_000,
);
});
@@ -170,6 +170,199 @@ describe("translateOpenCodeEvent", () => {
]);
});
it("emits assistant text from message.part.delta events", () => {
const state = createState();
// Register message role
translateOpenCodeEvent(
{
type: "message.updated",
properties: {
info: { id: "msg-d1", sessionID: "session-1", role: "assistant" },
},
},
state,
);
// OpenCode v2 can send streaming text as message.part.delta
const delta1 = translateOpenCodeEvent(
{
type: "message.part.delta",
properties: {
sessionID: "session-1",
messageID: "msg-d1",
partID: "part-d1",
field: "text",
delta: "hey! ",
},
},
state,
);
const delta2 = translateOpenCodeEvent(
{
type: "message.part.delta",
properties: {
sessionID: "session-1",
messageID: "msg-d1",
partID: "part-d1",
field: "text",
delta: "what's up?",
},
},
state,
);
expect([...delta1, ...delta2]).toEqual([
{
type: "timeline",
provider: "opencode",
item: { type: "assistant_message", text: "hey! " },
},
{
type: "timeline",
provider: "opencode",
item: { type: "assistant_message", text: "what's up?" },
},
]);
});
it("emits reasoning from message.part.delta events", () => {
const state = createState();
const delta = translateOpenCodeEvent(
{
type: "message.part.delta",
properties: {
sessionID: "session-1",
messageID: "msg-r1",
partID: "rp-1",
field: "reasoning",
delta: "The user said hello.",
},
},
state,
);
expect(delta).toEqual([
{
type: "timeline",
provider: "opencode",
item: { type: "reasoning", text: "The user said hello." },
},
]);
});
it("deduplicates when message.part.delta is followed by completed message.part.updated", () => {
const state = createState();
translateOpenCodeEvent(
{
type: "message.updated",
properties: {
info: { id: "msg-dd1", sessionID: "session-1", role: "assistant" },
},
},
state,
);
// Stream via delta event
const streamed = translateOpenCodeEvent(
{
type: "message.part.delta",
properties: {
sessionID: "session-1",
messageID: "msg-dd1",
partID: "part-dd1",
field: "text",
delta: "hello there",
},
},
state,
);
// Completed part echoes the same text
const completed = translateOpenCodeEvent(
{
type: "message.part.updated",
properties: {
part: {
id: "part-dd1",
sessionID: "session-1",
messageID: "msg-dd1",
type: "text",
text: "hello there",
time: { start: 1, end: 2 },
},
},
},
state,
);
const all = [...streamed, ...completed].filter(
(e) => e.type === "timeline" && e.item.type === "assistant_message",
);
// Only the delta, not the completed echo
expect(all).toEqual([
{
type: "timeline",
provider: "opencode",
item: { type: "assistant_message", text: "hello there" },
},
]);
});
it("ignores message.part.delta for wrong session", () => {
const state = createState();
const result = translateOpenCodeEvent(
{
type: "message.part.delta",
properties: {
sessionID: "other-session",
messageID: "msg-1",
partID: "part-1",
field: "text",
delta: "should not appear",
},
},
state,
);
expect(result).toEqual([]);
});
it("ignores message.part.delta for user messages", () => {
const state = createState();
// Register as user message
translateOpenCodeEvent(
{
type: "message.updated",
properties: {
info: { id: "msg-u1", sessionID: "session-1", role: "user" },
},
},
state,
);
const result = translateOpenCodeEvent(
{
type: "message.part.delta",
properties: {
sessionID: "session-1",
messageID: "msg-u1",
partID: "part-u1",
field: "text",
delta: "user typing",
},
},
state,
);
expect(result).toEqual([]);
});
it("emits structured assistant output when schema mode completes without text parts", () => {
const state = createState();
+73 -19
View File
@@ -22,6 +22,7 @@ import {
type SubscribeTerminalRequest,
type UnsubscribeTerminalRequest,
type TerminalInput,
type CloseItemsRequest,
type KillTerminalRequest,
type CaptureTerminalRequest,
type SubscribeCheckoutDiffRequest,
@@ -1474,6 +1475,10 @@ export class Session {
await this.handleArchiveAgentRequest(msg.agentId, msg.requestId);
break;
case "close_items_request":
await this.handleCloseItemsRequest(msg);
break;
case "update_agent_request":
await this.handleUpdateAgentRequest(msg.agentId, msg.name, msg.labels, msg.requestId);
break;
@@ -1986,6 +1991,20 @@ export class Session {
}
private async handleArchiveAgentRequest(agentId: string, requestId: string): Promise<void> {
const result = await this.archiveAgentForClose(agentId);
this.emit({
type: "agent_archived",
payload: {
agentId: result.agentId,
archivedAt: result.archivedAt,
requestId,
},
});
}
private async archiveAgentForClose(
agentId: string,
): Promise<{ agentId: string; archivedAt: string }> {
this.sessionLogger.info({ agentId }, `Archiving agent ${agentId}`);
if (this.agentManager.getAgent(agentId)) {
@@ -2023,12 +2042,44 @@ export class Session {
await this.emitWorkspaceUpdateForCwd(payload.cwd);
}
return { agentId, archivedAt };
}
private async handleCloseItemsRequest(msg: CloseItemsRequest): Promise<void> {
const agents = [];
for (const agentId of msg.agentIds) {
try {
agents.push(await this.archiveAgentForClose(agentId));
} catch (error: any) {
this.sessionLogger.warn(
{ err: error, agentId, requestId: msg.requestId },
"Failed to archive agent during close_items batch",
);
}
}
const terminals = [];
for (const terminalId of msg.terminalIds) {
try {
terminals.push(this.killTerminalForClose(terminalId));
} catch (error: any) {
this.sessionLogger.warn(
{ err: error, terminalId, requestId: msg.requestId },
"Failed to kill terminal during close_items batch",
);
terminals.push({
terminalId,
success: false,
});
}
}
this.emit({
type: "agent_archived",
type: "close_items_response",
payload: {
agentId,
archivedAt,
requestId,
agents,
terminals,
requestId: msg.requestId,
},
});
}
@@ -7491,29 +7542,32 @@ export class Session {
}
private async handleKillTerminalRequest(msg: KillTerminalRequest): Promise<void> {
if (!this.terminalManager) {
this.emit({
type: "kill_terminal_response",
payload: {
terminalId: msg.terminalId,
success: false,
requestId: msg.requestId,
},
});
return;
}
this.killTrackedTerminal(msg.terminalId, { emitExit: true });
const result = this.killTerminalForClose(msg.terminalId);
this.emit({
type: "kill_terminal_response",
payload: {
terminalId: msg.terminalId,
success: true,
terminalId: result.terminalId,
success: result.success,
requestId: msg.requestId,
},
});
}
private killTerminalForClose(terminalId: string): { terminalId: string; success: boolean } {
if (!this.terminalManager) {
return {
terminalId,
success: false,
};
}
this.killTrackedTerminal(terminalId, { emitExit: true });
return {
terminalId,
success: true,
};
}
private async handleCaptureTerminalRequest(msg: CaptureTerminalRequest): Promise<void> {
if (!this.terminalManager) {
this.emit({
@@ -272,6 +272,304 @@ describe("workspace aggregation", () => {
});
});
test("close_items_request archives agents and kills terminals in one batch", async () => {
const emitted: Array<{ type: string; payload: any }> = [];
const archivedAt = "2026-04-01T00:00:00.000Z";
const sessionLogger = {
child: () => sessionLogger,
trace: vi.fn(),
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
const archivedRecord = {
id: "agent-1",
provider: "codex",
cwd: "/tmp/repo",
model: null,
thinkingOptionId: null,
effectiveThinkingOptionId: null,
createdAt: "2026-03-01T12:00:00.000Z",
updatedAt: "2026-03-01T12:00:00.000Z",
lastUserMessageAt: null,
status: "idle",
capabilities: {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: true,
supportsMcpServers: true,
supportsReasoningStream: true,
supportsToolInvocations: true,
},
currentModeId: null,
availableModes: [],
pendingPermissions: [],
persistence: null,
runtimeInfo: { provider: "codex", sessionId: null },
title: null,
labels: {},
requiresAttention: false,
attentionReason: null,
attentionTimestamp: null,
archivedAt: null,
};
const session = new Session({
clientId: "test-client",
onMessage: (message) => emitted.push(message as any),
logger: sessionLogger as any,
downloadTokenStore: {} as any,
pushTokenStore: {} as any,
paseoHome: "/tmp/paseo-test",
agentManager: {
subscribe: () => () => {},
listAgents: () => [],
getAgent: (agentId: string) => (agentId === "agent-1" ? { id: agentId } : null),
archiveAgent: async () => ({ archivedAt }),
clearAgentAttention: async () => {},
notifyAgentState: () => {},
} as any,
agentStorage: {
list: async () => [],
get: async (agentId: string) => {
if (agentId !== "agent-1") {
return null;
}
archivedRecord.archivedAt = archivedAt;
archivedRecord.updatedAt = archivedAt;
return archivedRecord;
},
} as any,
projectRegistry: {
initialize: async () => {},
existsOnDisk: async () => true,
list: async () => [],
get: async () => null,
upsert: async () => {},
archive: async () => {},
remove: async () => {},
} as any,
workspaceRegistry: {
initialize: async () => {},
existsOnDisk: async () => true,
list: async () => [],
get: async () => null,
upsert: async () => {},
archive: async () => {},
remove: async () => {},
} as any,
checkoutDiffManager: {
subscribe: async () => ({
initial: { cwd: "/tmp", files: [], error: null },
unsubscribe: () => {},
}),
scheduleRefreshForCwd: () => {},
getMetrics: () => ({
checkoutDiffTargetCount: 0,
checkoutDiffSubscriptionCount: 0,
checkoutDiffWatcherCount: 0,
checkoutDiffFallbackRefreshTargetCount: 0,
}),
dispose: () => {},
} as any,
createAgentMcpTransport: async () => {
throw new Error("not used");
},
stt: null,
tts: null,
terminalManager: {
killTerminal: vi.fn(),
subscribeTerminalsChanged: () => () => {},
} as any,
}) as any;
session.agentUpdatesSubscription = {
subscriptionId: "sub-agents",
filter: { includeArchived: true },
isBootstrapping: false,
pendingUpdatesByAgentId: new Map(),
};
session.buildProjectPlacement = async (cwd: string) => ({
projectKey: cwd,
projectName: "repo",
checkout: {
cwd,
isGit: false,
currentBranch: null,
remoteUrl: null,
isPaseoOwnedWorktree: false,
mainRepoRoot: null,
},
});
session.interruptAgentIfRunning = vi.fn();
await session.handleMessage({
type: "close_items_request",
agentIds: ["agent-1"],
terminalIds: ["term-1"],
requestId: "req-close-items",
});
expect(session.interruptAgentIfRunning).toHaveBeenCalledWith("agent-1");
expect(session.terminalManager.killTerminal).toHaveBeenCalledWith("term-1");
expect(
emitted.find((message) => message.type === "close_items_response")?.payload,
).toEqual({
agents: [{ agentId: "agent-1", archivedAt }],
terminals: [{ terminalId: "term-1", success: true }],
requestId: "req-close-items",
});
expect(emitted.find((message) => message.type === "agent_update")?.payload).toMatchObject({
kind: "upsert",
agent: {
id: "agent-1",
archivedAt,
},
});
});
test("close_items_request continues after an archive failure", async () => {
const emitted: Array<{ type: string; payload: any }> = [];
const sessionLogger = {
child: () => sessionLogger,
trace: vi.fn(),
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
const archivedAt = "2026-04-01T00:00:00.000Z";
const goodRecord = {
...makeAgent({
id: "agent-good",
cwd: "/tmp/repo",
status: "idle",
updatedAt: "2026-03-01T12:00:00.000Z",
}),
archivedAt: null as string | null,
};
const session = new Session({
clientId: "test-client",
onMessage: (message) => emitted.push(message as any),
logger: sessionLogger as any,
downloadTokenStore: {} as any,
pushTokenStore: {} as any,
paseoHome: "/tmp/paseo-test",
agentManager: {
subscribe: () => () => {},
listAgents: () => [],
getAgent: (agentId: string) =>
agentId === "agent-bad" || agentId === "agent-good" ? { id: agentId } : null,
archiveAgent: async (agentId: string) => {
if (agentId === "agent-bad") {
throw new Error("archive failed");
}
return { archivedAt };
},
clearAgentAttention: async () => {},
notifyAgentState: () => {},
} as any,
agentStorage: {
list: async () => [],
get: async (agentId: string) => {
if (agentId !== "agent-good") {
return null;
}
goodRecord.archivedAt = archivedAt;
goodRecord.updatedAt = archivedAt;
return goodRecord;
},
} as any,
projectRegistry: {
initialize: async () => {},
existsOnDisk: async () => true,
list: async () => [],
get: async () => null,
upsert: async () => {},
archive: async () => {},
remove: async () => {},
} as any,
workspaceRegistry: {
initialize: async () => {},
existsOnDisk: async () => true,
list: async () => [],
get: async () => null,
upsert: async () => {},
archive: async () => {},
remove: async () => {},
} as any,
checkoutDiffManager: {
subscribe: async () => ({
initial: { cwd: "/tmp", files: [], error: null },
unsubscribe: () => {},
}),
scheduleRefreshForCwd: () => {},
getMetrics: () => ({
checkoutDiffTargetCount: 0,
checkoutDiffSubscriptionCount: 0,
checkoutDiffWatcherCount: 0,
checkoutDiffFallbackRefreshTargetCount: 0,
}),
dispose: () => {},
} as any,
createAgentMcpTransport: async () => {
throw new Error("not used");
},
stt: null,
tts: null,
terminalManager: {
killTerminal: vi.fn(),
subscribeTerminalsChanged: () => () => {},
} as any,
}) as any;
session.agentUpdatesSubscription = {
subscriptionId: "sub-agents",
filter: { includeArchived: true },
isBootstrapping: false,
pendingUpdatesByAgentId: new Map(),
};
session.buildProjectPlacement = async (cwd: string) => ({
projectKey: cwd,
projectName: "repo",
checkout: {
cwd,
isGit: false,
currentBranch: null,
remoteUrl: null,
isPaseoOwnedWorktree: false,
mainRepoRoot: null,
},
});
session.interruptAgentIfRunning = vi.fn();
await session.handleMessage({
type: "close_items_request",
agentIds: ["agent-bad", "agent-good"],
terminalIds: ["term-1"],
requestId: "req-close-best-effort",
});
expect(session.interruptAgentIfRunning).toHaveBeenCalledWith("agent-bad");
expect(session.interruptAgentIfRunning).toHaveBeenCalledWith("agent-good");
expect(session.terminalManager.killTerminal).toHaveBeenCalledWith("term-1");
expect(
emitted.find((message) => message.type === "close_items_response")?.payload,
).toEqual({
agents: [{ agentId: "agent-good", archivedAt }],
terminals: [{ terminalId: "term-1", success: true }],
requestId: "req-close-best-effort",
});
expect(emitted.find((message) => message.type === "agent_update")?.payload).toMatchObject({
kind: "upsert",
agent: {
id: "agent-good",
archivedAt,
},
});
expect(sessionLogger.warn).toHaveBeenCalled();
});
test("non-git workspace uses deterministic directory name and no unknown branch fallback", async () => {
const session = createSessionForWorkspaceTests() as any;
session.workspaceRegistry.list = async () => [
+30
View File
@@ -525,6 +525,13 @@ export const ArchiveAgentRequestMessageSchema = z.object({
requestId: z.string(),
});
export const CloseItemsRequestMessageSchema = z.object({
type: z.literal("close_items_request"),
agentIds: z.array(z.string()).default([]),
terminalIds: z.array(z.string()).default([]),
requestId: z.string(),
});
export const UpdateAgentRequestMessageSchema = z.object({
type: z.literal("update_agent_request"),
agentId: z.string(),
@@ -1190,6 +1197,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
FetchAgentRequestMessageSchema,
DeleteAgentRequestMessageSchema,
ArchiveAgentRequestMessageSchema,
CloseItemsRequestMessageSchema,
UpdateAgentRequestMessageSchema,
SetVoiceModeMessageSchema,
SendAgentMessageRequestSchema,
@@ -1805,6 +1813,25 @@ export const AgentArchivedMessageSchema = z.object({
}),
});
const CloseItemsAgentResultSchema = z.object({
agentId: z.string(),
archivedAt: z.string(),
});
const CloseItemsTerminalResultSchema = z.object({
terminalId: z.string(),
success: z.boolean(),
});
export const CloseItemsResponseSchema = z.object({
type: z.literal("close_items_response"),
payload: z.object({
agents: z.array(CloseItemsAgentResultSchema),
terminals: z.array(CloseItemsTerminalResultSchema),
requestId: z.string(),
}),
});
const AheadBehindSchema = z.object({
ahead: z.number(),
behind: z.number(),
@@ -2270,6 +2297,7 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
AgentPermissionResolvedMessageSchema,
AgentDeletedMessageSchema,
AgentArchivedMessageSchema,
CloseItemsResponseSchema,
CheckoutStatusResponseSchema,
SubscribeCheckoutDiffResponseSchema,
CheckoutDiffUpdateSchema,
@@ -2487,6 +2515,8 @@ export type TerminalCell = z.infer<typeof TerminalCellSchema>;
export type TerminalCursorStyle = z.infer<typeof TerminalCursorStyleSchema>;
export type TerminalCursor = z.infer<typeof TerminalCursorSchema>;
export type TerminalState = z.infer<typeof TerminalStateSchema>;
export type CloseItemsRequest = z.infer<typeof CloseItemsRequestMessageSchema>;
export type CloseItemsResponse = z.infer<typeof CloseItemsResponseSchema>;
export type KillTerminalRequest = z.infer<typeof KillTerminalRequestSchema>;
export type KillTerminalResponse = z.infer<typeof KillTerminalResponseSchema>;
export type CaptureTerminalRequest = z.infer<typeof CaptureTerminalRequestSchema>;