Merge branch 'main' into dev

Surgical merge of 74 commits from main into dev. Key features ported:

- Provider visibility gating (appVersion filtering for Pi/Copilot)
- Pi agent provider + Copilot re-enabled
- Provider-declared features system (Codex fast mode)
- Workspace dedup by worktree root (adapted to integer IDs)
- Bulk close archiving fix for stored agents
- Agent creation timeout increase to 60s
- Audio/voice crash fixes (external buffer copies)
- Multi-host setup fix
- Reload agent tab action

Dev architecture preserved: SQLite storage, service proxy,
workspace setup dialog, hover cards, removed launcher tabs.
This commit is contained in:
Mohamed Boudra
2026-04-05 12:59:07 +07:00
173 changed files with 8873 additions and 2316 deletions
@@ -0,0 +1,146 @@
import { describe, expect, it } from "vitest";
import {
AgentFeatureSchema,
AgentSnapshotPayloadSchema,
SetAgentFeatureRequestMessageSchema,
SetAgentFeatureResponseMessageSchema,
} from "./messages.js";
describe("agent feature schemas", () => {
it("parses valid toggle features", () => {
const parsed = AgentFeatureSchema.parse({
type: "toggle",
id: "fast_mode",
label: "Fast mode",
description: "Uses lower latency service tier",
tooltip: "Toggle fast mode",
icon: "bolt",
value: true,
});
expect(parsed.type).toBe("toggle");
if (parsed.type !== "toggle") {
throw new Error("Expected toggle feature");
}
expect(parsed.value).toBe(true);
});
it("parses valid select features", () => {
const parsed = AgentFeatureSchema.parse({
type: "select",
id: "service_tier",
label: "Service tier",
description: "Choose a processing tier",
tooltip: "Select service tier",
icon: "gauge",
value: "flex",
options: [
{ id: "default", label: "Default", isDefault: true },
{ id: "flex", label: "Flex" },
],
});
expect(parsed.type).toBe("select");
if (parsed.type !== "select") {
throw new Error("Expected select feature");
}
expect(parsed.options).toHaveLength(2);
expect(parsed.value).toBe("flex");
});
it("rejects invalid features", () => {
const invalidDiscriminator = AgentFeatureSchema.safeParse({
type: "slider",
id: "fast_mode",
label: "Fast mode",
value: true,
});
const missingToggleValue = AgentFeatureSchema.safeParse({
type: "toggle",
id: "fast_mode",
label: "Fast mode",
});
const missingSelectOptions = AgentFeatureSchema.safeParse({
type: "select",
id: "service_tier",
label: "Service tier",
value: null,
});
expect(invalidDiscriminator.success).toBe(false);
expect(missingToggleValue.success).toBe(false);
expect(missingSelectOptions.success).toBe(false);
});
it("parses valid requests", () => {
const parsed = SetAgentFeatureRequestMessageSchema.parse({
type: "set_agent_feature_request",
agentId: "agent-123",
featureId: "fast_mode",
value: true,
requestId: "req-123",
});
expect(parsed.featureId).toBe("fast_mode");
expect(parsed.value).toBe(true);
});
it("parses valid responses", () => {
const parsed = SetAgentFeatureResponseMessageSchema.parse({
type: "set_agent_feature_response",
payload: {
requestId: "req-123",
agentId: "agent-123",
accepted: true,
error: null,
},
});
expect(parsed.payload.accepted).toBe(true);
expect(parsed.payload.error).toBeNull();
});
it("accepts features on agent snapshot payloads", () => {
const parsed = AgentSnapshotPayloadSchema.parse({
id: "agent-123",
provider: "codex",
cwd: "/tmp/project",
model: "gpt-5",
features: [
{
type: "toggle",
id: "fast_mode",
label: "Fast mode",
tooltip: "Toggle fast mode",
value: false,
},
],
thinkingOptionId: null,
effectiveThinkingOptionId: null,
createdAt: "2026-04-03T12:00:00.000Z",
updatedAt: "2026-04-03T12: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,
title: null,
labels: {},
});
expect(parsed.features).toHaveLength(1);
expect(parsed.features?.[0]?.id).toBe("fast_mode");
});
});
@@ -27,6 +27,9 @@ describe("list_commands_request schema", () => {
modeId: "bypassPermissions",
model: "gpt-5",
thinkingOptionId: "off",
featureValues: {
plan_mode: true,
},
},
requestId: "req-456",
});
@@ -41,6 +44,9 @@ describe("list_commands_request schema", () => {
modeId: "bypassPermissions",
model: "gpt-5",
thinkingOptionId: "off",
featureValues: {
plan_mode: true,
},
});
});
});
+152 -24
View File
@@ -77,6 +77,32 @@ const AgentSelectOptionSchema = z.object({
metadata: z.record(z.unknown()).optional(),
});
export const AgentFeatureToggleSchema = z.object({
type: z.literal("toggle"),
id: z.string(),
label: z.string(),
description: z.string().optional(),
tooltip: z.string().optional(),
icon: z.string().optional(),
value: z.boolean(),
});
export const AgentFeatureSelectSchema = z.object({
type: z.literal("select"),
id: z.string(),
label: z.string(),
description: z.string().optional(),
tooltip: z.string().optional(),
icon: z.string().optional(),
value: z.string().nullable(),
options: z.array(AgentSelectOptionSchema),
});
export const AgentFeatureSchema = z.discriminatedUnion("type", [
AgentFeatureToggleSchema,
AgentFeatureSelectSchema,
]);
const AgentModelDefinitionSchema: z.ZodType<AgentModelDefinition> = z.object({
provider: AgentProviderSchema,
id: z.string(),
@@ -135,6 +161,7 @@ const AgentSessionConfigSchema = z.object({
modeId: z.string().optional(),
model: z.string().optional(),
thinkingOptionId: z.string().optional(),
featureValues: z.record(z.unknown()).optional(),
title: z.string().trim().min(1).max(MAX_EXPLICIT_AGENT_TITLE_CHARS).optional().nullable(),
approvalPolicy: z.string().optional(),
sandboxMode: z.string().optional(),
@@ -293,6 +320,10 @@ const ToolCallDetailPayloadSchema: z.ZodType<ToolCallDetail> = z.discriminatedUn
text: z.string().optional(),
icon: z.enum(TOOL_CALL_ICON_NAMES).optional(),
}),
z.object({
type: z.literal("plan"),
text: z.string(),
}),
z.object({
type: z.literal("unknown"),
input: UnknownValueSchema,
@@ -461,6 +492,7 @@ export const AgentSnapshotPayloadSchema = z.object({
provider: AgentProviderSchema,
cwd: z.string(),
model: z.string().nullable(),
features: z.array(AgentFeatureSchema).optional(),
thinkingOptionId: z.string().nullable().optional(),
effectiveThinkingOptionId: z.string().nullable().optional(),
createdAt: z.string(),
@@ -732,6 +764,13 @@ export const ListProviderModelsRequestMessageSchema = z.object({
requestId: z.string(),
});
export const ListProviderModesRequestMessageSchema = z.object({
type: z.literal("list_provider_modes_request"),
provider: AgentProviderSchema,
cwd: z.string().optional(),
requestId: z.string(),
});
export const ListAvailableProvidersRequestMessageSchema = z.object({
type: z.literal("list_available_providers_request"),
requestId: z.string(),
@@ -835,6 +874,24 @@ export const SetAgentThinkingResponseMessageSchema = z.object({
}),
});
export const SetAgentFeatureRequestMessageSchema = z.object({
type: z.literal("set_agent_feature_request"),
agentId: z.string(),
featureId: z.string(),
value: z.unknown(),
requestId: z.string(),
});
export const SetAgentFeatureResponseMessageSchema = z.object({
type: z.literal("set_agent_feature_response"),
payload: z.object({
requestId: z.string(),
agentId: z.string(),
accepted: z.boolean(),
error: z.string().nullable(),
}),
});
export const UpdateAgentResponseMessageSchema = z.object({
type: z.literal("update_agent_response"),
payload: z.object({
@@ -1114,6 +1171,13 @@ const ListCommandsDraftConfigSchema = z.object({
modeId: z.string().optional(),
model: z.string().optional(),
thinkingOptionId: z.string().optional(),
featureValues: z.record(z.unknown()).optional(),
});
export const ListProviderFeaturesRequestMessageSchema = z.object({
type: z.literal("list_provider_features_request"),
draftConfig: ListCommandsDraftConfigSchema,
requestId: z.string(),
});
export const ListCommandsRequestSchema = z.object({
@@ -1222,6 +1286,8 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
DictationStreamCancelMessageSchema,
CreateAgentRequestMessageSchema,
ListProviderModelsRequestMessageSchema,
ListProviderModesRequestMessageSchema,
ListProviderFeaturesRequestMessageSchema,
ListAvailableProvidersRequestMessageSchema,
ResumeAgentRequestMessageSchema,
RefreshAgentRequestMessageSchema,
@@ -1232,6 +1298,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
SetAgentModeRequestMessageSchema,
SetAgentModelRequestMessageSchema,
SetAgentThinkingRequestMessageSchema,
SetAgentFeatureRequestMessageSchema,
AgentPermissionResponseMessageSchema,
CheckoutStatusRequestSchema,
SubscribeCheckoutDiffRequestSchema,
@@ -1555,32 +1622,50 @@ export const ArtifactMessageSchema = z.object({
}),
});
export const ProjectCheckoutLiteNotGitPayloadSchema = z.object({
cwd: z.string(),
isGit: z.literal(false),
currentBranch: z.null(),
remoteUrl: z.null(),
isPaseoOwnedWorktree: z.literal(false),
mainRepoRoot: z.null(),
});
export const ProjectCheckoutLiteNotGitPayloadSchema = z
.object({
cwd: z.string(),
isGit: z.literal(false),
currentBranch: z.null(),
remoteUrl: z.null(),
worktreeRoot: z.null().optional(),
isPaseoOwnedWorktree: z.literal(false),
mainRepoRoot: z.null(),
})
.transform((value) => ({
...value,
worktreeRoot: null,
}));
export const ProjectCheckoutLiteGitNonPaseoPayloadSchema = z.object({
cwd: z.string(),
isGit: z.literal(true),
currentBranch: z.string().nullable(),
remoteUrl: z.string().nullable(),
isPaseoOwnedWorktree: z.literal(false),
mainRepoRoot: z.null(),
});
export const ProjectCheckoutLiteGitNonPaseoPayloadSchema = z
.object({
cwd: z.string(),
isGit: z.literal(true),
currentBranch: z.string().nullable(),
remoteUrl: z.string().nullable(),
worktreeRoot: z.string().optional(),
isPaseoOwnedWorktree: z.literal(false),
mainRepoRoot: z.null(),
})
.transform((value) => ({
...value,
worktreeRoot: value.worktreeRoot ?? value.cwd,
}));
export const ProjectCheckoutLiteGitPaseoPayloadSchema = z.object({
cwd: z.string(),
isGit: z.literal(true),
currentBranch: z.string().nullable(),
remoteUrl: z.string().nullable(),
isPaseoOwnedWorktree: z.literal(true),
mainRepoRoot: z.string(),
});
export const ProjectCheckoutLiteGitPaseoPayloadSchema = z
.object({
cwd: z.string(),
isGit: z.literal(true),
currentBranch: z.string().nullable(),
remoteUrl: z.string().nullable(),
worktreeRoot: z.string().optional(),
isPaseoOwnedWorktree: z.literal(true),
mainRepoRoot: z.string(),
})
.transform((value) => ({
...value,
worktreeRoot: value.worktreeRoot ?? value.cwd,
}));
export const ProjectCheckoutLitePayloadSchema = z.union([
ProjectCheckoutLiteNotGitPayloadSchema,
@@ -2161,6 +2246,28 @@ export const ListProviderModelsResponseMessageSchema = z.object({
}),
});
export const ListProviderModesResponseMessageSchema = z.object({
type: z.literal("list_provider_modes_response"),
payload: z.object({
provider: AgentProviderSchema,
modes: z.array(AgentModeSchema).optional(),
error: z.string().nullable().optional(),
fetchedAt: z.string(),
requestId: z.string(),
}),
});
export const ListProviderFeaturesResponseMessageSchema = z.object({
type: z.literal("list_provider_features_response"),
payload: z.object({
provider: AgentProviderSchema,
features: z.array(AgentFeatureSchema).optional(),
error: z.string().nullable().optional(),
fetchedAt: z.string(),
requestId: z.string(),
}),
});
const ProviderAvailabilitySchema = z.object({
provider: AgentProviderSchema,
available: z.boolean(),
@@ -2345,6 +2452,7 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
SetAgentModeResponseMessageSchema,
SetAgentModelResponseMessageSchema,
SetAgentThinkingResponseMessageSchema,
SetAgentFeatureResponseMessageSchema,
UpdateAgentResponseMessageSchema,
WaitForFinishResponseMessageSchema,
AgentPermissionRequestMessageSchema,
@@ -2371,6 +2479,8 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
ProjectIconResponseSchema,
FileDownloadTokenResponseSchema,
ListProviderModelsResponseMessageSchema,
ListProviderModesResponseMessageSchema,
ListProviderFeaturesResponseMessageSchema,
ListAvailableProvidersResponseSchema,
ListCommandsResponseSchema,
ListTerminalsResponseSchema,
@@ -2439,6 +2549,10 @@ export type FetchAgentTimelineResponseMessage = z.infer<
>;
export type SendAgentMessageResponseMessage = z.infer<typeof SendAgentMessageResponseMessageSchema>;
export type SetVoiceModeResponseMessage = z.infer<typeof SetVoiceModeResponseMessageSchema>;
export type SetAgentModeResponseMessage = z.infer<typeof SetAgentModeResponseMessageSchema>;
export type SetAgentModelResponseMessage = z.infer<typeof SetAgentModelResponseMessageSchema>;
export type SetAgentThinkingResponseMessage = z.infer<typeof SetAgentThinkingResponseMessageSchema>;
export type SetAgentFeatureResponseMessage = z.infer<typeof SetAgentFeatureResponseMessageSchema>;
export type UpdateAgentResponseMessage = z.infer<typeof UpdateAgentResponseMessageSchema>;
export type WaitForFinishResponseMessage = z.infer<typeof WaitForFinishResponseMessageSchema>;
export type AgentPermissionRequestMessage = z.infer<typeof AgentPermissionRequestMessageSchema>;
@@ -2447,6 +2561,12 @@ export type AgentDeletedMessage = z.infer<typeof AgentDeletedMessageSchema>;
export type ListProviderModelsResponseMessage = z.infer<
typeof ListProviderModelsResponseMessageSchema
>;
export type ListProviderModesResponseMessage = z.infer<
typeof ListProviderModesResponseMessageSchema
>;
export type ListProviderFeaturesResponseMessage = z.infer<
typeof ListProviderFeaturesResponseMessageSchema
>;
export type ListAvailableProvidersResponse = z.infer<typeof ListAvailableProvidersResponseSchema>;
export type ChatCreateResponse = z.infer<typeof ChatCreateResponseSchema>;
export type ChatListResponse = z.infer<typeof ChatListResponseSchema>;
@@ -2486,6 +2606,12 @@ export type CreateAgentRequestMessage = z.infer<typeof CreateAgentRequestMessage
export type ListProviderModelsRequestMessage = z.infer<
typeof ListProviderModelsRequestMessageSchema
>;
export type ListProviderModesRequestMessage = z.infer<
typeof ListProviderModesRequestMessageSchema
>;
export type ListProviderFeaturesRequestMessage = z.infer<
typeof ListProviderFeaturesRequestMessageSchema
>;
export type ListAvailableProvidersRequestMessage = z.infer<
typeof ListAvailableProvidersRequestMessageSchema
>;
@@ -2514,6 +2640,7 @@ export type UpdateAgentRequestMessage = z.infer<typeof UpdateAgentRequestMessage
export type SetAgentModeRequestMessage = z.infer<typeof SetAgentModeRequestMessageSchema>;
export type SetAgentModelRequestMessage = z.infer<typeof SetAgentModelRequestMessageSchema>;
export type SetAgentThinkingRequestMessage = z.infer<typeof SetAgentThinkingRequestMessageSchema>;
export type SetAgentFeatureRequestMessage = z.infer<typeof SetAgentFeatureRequestMessageSchema>;
export type AgentPermissionResponseMessage = z.infer<typeof AgentPermissionResponseMessageSchema>;
export type CheckoutStatusRequest = z.infer<typeof CheckoutStatusRequestSchema>;
export type CheckoutStatusResponse = z.infer<typeof CheckoutStatusResponseSchema>;
@@ -2603,6 +2730,7 @@ export const WSHelloMessageSchema = z.object({
clientId: z.string().min(1),
clientType: z.enum(["mobile", "browser", "cli", "mcp"]),
protocolVersion: z.number().int(),
appVersion: z.string().optional(),
capabilities: z
.object({
voice: z.boolean().optional(),
@@ -179,4 +179,71 @@ describe("workspace message schemas", () => {
expect(parsed.type).toBe("workspace_setup_status_response");
});
test("parses legacy fetch_agents_response checkout payloads without worktreeRoot", () => {
const result = SessionOutboundMessageSchema.safeParse({
type: "fetch_agents_response",
payload: {
requestId: "req-1",
entries: [
{
agent: {
id: "agent-1",
provider: "codex",
cwd: "C:\\repo",
model: null,
features: [],
thinkingOptionId: null,
effectiveThinkingOptionId: null,
createdAt: "2026-04-04T00:00:00.000Z",
updatedAt: "2026-04-04T00:00:00.000Z",
lastUserMessageAt: null,
status: "running",
capabilities: {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: true,
supportsMcpServers: true,
supportsReasoningStream: true,
supportsToolInvocations: true,
},
currentModeId: null,
availableModes: [],
pendingPermissions: [],
persistence: null,
title: "Agent 1",
labels: {},
requiresAttention: false,
attentionReason: null,
},
project: {
projectKey: "remote:github.com/acme/repo",
projectName: "acme/repo",
checkout: {
cwd: "C:\\repo",
isGit: true,
currentBranch: "main",
remoteUrl: "https://github.com/acme/repo.git",
isPaseoOwnedWorktree: false,
mainRepoRoot: null,
},
},
},
],
pageInfo: {
nextCursor: null,
prevCursor: null,
hasMore: false,
},
},
});
expect(result.success).toBe(true);
if (!result.success) {
return;
}
const checkout = result.data.payload.entries[0]?.project.checkout;
expect(checkout?.worktreeRoot).toBe("C:\\repo");
});
});
@@ -141,4 +141,20 @@ describe("shared tool-call display mapping", () => {
summary: "npm run test",
});
});
it("labels plan detail rows as Plan", () => {
const display = buildToolCallDisplayModel({
name: "plan",
status: "completed",
error: null,
detail: {
type: "plan",
text: "### Login Screen\n- Build layout",
},
});
expect(display).toEqual({
displayName: "Plan",
});
});
});
@@ -109,6 +109,10 @@ function buildCanonicalDetailDisplay(input: ToolCallDisplayInput): DetailDisplay
return {
summary: input.detail.label,
};
case "plan":
return {
displayName: "Plan",
};
case "unknown":
return {};
}