From c6680b30014b55c75c47598ecfb9d9c2ccbed385 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 22 Apr 2026 20:37:19 +0700 Subject: [PATCH] fix(release): preserve 0.1.60 schema compatibility --- CHANGELOG.md | 8 +- .../shared/messages.stream-parsing.test.ts | 63 +++++ packages/server/src/shared/messages.ts | 234 +++++++++--------- .../src/shared/messages.workspaces.test.ts | 27 ++ 4 files changed, 219 insertions(+), 113 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3248e9270..d30013385 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 0.1.60-beta.2 - 2026-04-22 +## 0.1.60 - 2026-04-22 ### Added - Scripts and services per worktree — define named commands in `paseo.json`, and long-running services get supervised with their own ports and nice proxy URLs like `http://web.my-app.localhost:6767`. See the [worktrees guide](https://paseo.sh/docs/worktrees). @@ -28,6 +28,7 @@ - Escape reliably interrupts the active agent. - Checking out a pull request from a fork lands on an owner-prefixed branch so multiple forks don't collide. - `paseo ls` defaults to active agents; pass `-a` to include archived. +- GitHub branch and PR picker loads faster — queries are deferred until the picker opens. ### Fixed - Composer textarea shrinks back down after sending on web. @@ -42,6 +43,11 @@ - iOS image picker no longer leaves the screen unresponsive after cancelling. - Archiving a worktree recovers cleanly if a previous attempt was interrupted. - Images in agent messages with `~`-prefixed paths load instead of spinning forever. +- Tool call blocks expand correctly on mobile while an agent is still streaming. +- Timeline no longer stutters when catch-up and projected ranges overlap. +- Codex no longer flashes idle when a replacement turn is in progress. +- Branch state recovers correctly when a rebase is in progress. +- Workspace hover card no longer clips near screen edges. ## 0.1.59 - 2026-04-16 diff --git a/packages/server/src/shared/messages.stream-parsing.test.ts b/packages/server/src/shared/messages.stream-parsing.test.ts index 4bb6692c7..cff9acb0e 100644 --- a/packages/server/src/shared/messages.stream-parsing.test.ts +++ b/packages/server/src/shared/messages.stream-parsing.test.ts @@ -46,6 +46,69 @@ describe("shared messages stream parsing", () => { expect(parsed.payload.entries[0]?.item.type).toBe("assistant_message"); }); + it("parses legacy worktree setup timeline entries without per-command log", () => { + const parsed = FetchAgentTimelineResponseMessageSchema.parse({ + type: "fetch_agent_timeline_response", + payload: { + requestId: "req-legacy-setup", + agentId: "agent_legacy_setup", + agent: null, + direction: "tail", + projection: "canonical", + epoch: "epoch-setup", + reset: false, + staleCursor: false, + gap: false, + window: { minSeq: 1, maxSeq: 1, nextSeq: 2 }, + startCursor: { epoch: "epoch-setup", seq: 1 }, + endCursor: { epoch: "epoch-setup", seq: 1 }, + hasOlder: false, + hasNewer: false, + entries: [ + { + provider: "codex", + item: { + type: "tool_call", + callId: "setup-1", + name: "paseo_worktree_setup", + status: "completed", + detail: { + type: "worktree_setup", + worktreePath: "/repo/.paseo/worktrees/feature", + branchName: "feature", + log: "setup complete", + commands: [ + { + index: 1, + command: "npm install", + cwd: "/repo/.paseo/worktrees/feature", + status: "completed", + exitCode: 0, + durationMs: 100, + }, + ], + }, + error: null, + }, + timestamp: "2026-04-22T00:00:00.000Z", + seqStart: 1, + seqEnd: 1, + sourceSeqRanges: [{ startSeq: 1, endSeq: 1 }], + collapsed: [], + }, + ], + error: null, + }, + }); + + const item = parsed.payload.entries[0]?.item; + expect(item?.type).toBe("tool_call"); + if (item?.type !== "tool_call" || item.detail.type !== "worktree_setup") { + throw new Error("Expected worktree setup tool call"); + } + expect(item.detail.commands[0]?.log).toBe(""); + }); + it("parses explicit shutdown and restart lifecycle request payloads as distinct message types", () => { const shutdownParsed = SessionInboundMessageSchema.safeParse({ type: "shutdown_server_request", diff --git a/packages/server/src/shared/messages.ts b/packages/server/src/shared/messages.ts index dbb78864c..88d4cf3c6 100644 --- a/packages/server/src/shared/messages.ts +++ b/packages/server/src/shared/messages.ts @@ -251,7 +251,11 @@ export const AgentPermissionResponseSchema: z.ZodType = }), ]); -export const AgentPermissionRequestPayloadSchema: z.ZodType = z.object({ +export const AgentPermissionRequestPayloadSchema: z.ZodType< + AgentPermissionRequest, + z.ZodTypeDef, + unknown +> = z.object({ id: z.string(), provider: AgentProviderSchema, name: z.string(), @@ -286,7 +290,7 @@ const WorktreeSetupCommandSnapshotSchema = z.object({ index: z.number().int().positive(), command: z.string(), cwd: z.string(), - log: z.string(), + log: z.string().optional().default(""), status: z.enum(["running", "completed", "failed"]), exitCode: z.number().nullable(), durationMs: z.number().nonnegative().optional(), @@ -301,95 +305,96 @@ const WorktreeSetupDetailPayloadSchema = z.object({ truncated: z.boolean().optional(), }); -const ToolCallDetailPayloadSchema: z.ZodType = z.discriminatedUnion("type", [ - WorktreeSetupDetailPayloadSchema, - z.object({ - type: z.literal("shell"), - command: z.string(), - cwd: z.string().optional(), - output: z.string().optional(), - exitCode: z.number().nullable().optional(), - }), - z.object({ - type: z.literal("read"), - filePath: z.string(), - content: z.string().optional(), - offset: z.number().optional(), - limit: z.number().optional(), - }), - z.object({ - type: z.literal("edit"), - filePath: z.string(), - oldString: z.string().optional(), - newString: z.string().optional(), - unifiedDiff: z.string().optional(), - }), - z.object({ - type: z.literal("write"), - filePath: z.string(), - content: z.string().optional(), - }), - z.object({ - type: z.literal("search"), - query: z.string(), - toolName: z.enum(["search", "grep", "glob", "web_search"]).optional(), - content: z.string().optional(), - filePaths: z.array(z.string()).optional(), - webResults: z - .array( +const ToolCallDetailPayloadSchema: z.ZodType = + z.discriminatedUnion("type", [ + WorktreeSetupDetailPayloadSchema, + z.object({ + type: z.literal("shell"), + command: z.string(), + cwd: z.string().optional(), + output: z.string().optional(), + exitCode: z.number().nullable().optional(), + }), + z.object({ + type: z.literal("read"), + filePath: z.string(), + content: z.string().optional(), + offset: z.number().optional(), + limit: z.number().optional(), + }), + z.object({ + type: z.literal("edit"), + filePath: z.string(), + oldString: z.string().optional(), + newString: z.string().optional(), + unifiedDiff: z.string().optional(), + }), + z.object({ + type: z.literal("write"), + filePath: z.string(), + content: z.string().optional(), + }), + z.object({ + type: z.literal("search"), + query: z.string(), + toolName: z.enum(["search", "grep", "glob", "web_search"]).optional(), + content: z.string().optional(), + filePaths: z.array(z.string()).optional(), + webResults: z + .array( + z.object({ + title: z.string(), + url: z.string(), + }), + ) + .optional(), + annotations: z.array(z.string()).optional(), + numFiles: z.number().optional(), + numMatches: z.number().optional(), + durationMs: z.number().optional(), + durationSeconds: z.number().optional(), + truncated: z.boolean().optional(), + mode: z.enum(["content", "files_with_matches", "count"]).optional(), + }), + z.object({ + type: z.literal("fetch"), + url: z.string(), + prompt: z.string().optional(), + result: z.string().optional(), + code: z.number().optional(), + codeText: z.string().optional(), + bytes: z.number().optional(), + durationMs: z.number().optional(), + }), + z.object({ + type: z.literal("sub_agent"), + subAgentType: z.string().optional(), + description: z.string().optional(), + log: z.string(), + actions: z.array( z.object({ - title: z.string(), - url: z.string(), + index: z.number().int().positive(), + toolName: z.string(), + summary: z.string().optional(), }), - ) - .optional(), - annotations: z.array(z.string()).optional(), - numFiles: z.number().optional(), - numMatches: z.number().optional(), - durationMs: z.number().optional(), - durationSeconds: z.number().optional(), - truncated: z.boolean().optional(), - mode: z.enum(["content", "files_with_matches", "count"]).optional(), - }), - z.object({ - type: z.literal("fetch"), - url: z.string(), - prompt: z.string().optional(), - result: z.string().optional(), - code: z.number().optional(), - codeText: z.string().optional(), - bytes: z.number().optional(), - durationMs: z.number().optional(), - }), - z.object({ - type: z.literal("sub_agent"), - subAgentType: z.string().optional(), - description: z.string().optional(), - log: z.string(), - actions: z.array( - z.object({ - index: z.number().int().positive(), - toolName: z.string(), - summary: z.string().optional(), - }), - ), - }), - z.object({ - type: z.literal("plain_text"), - label: z.string().optional(), - 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, - output: UnknownValueSchema, - }), -]); + ), + }), + z.object({ + type: z.literal("plain_text"), + label: z.string().optional(), + 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, + output: UnknownValueSchema, + }), + ]); const ToolCallBasePayloadSchema = z .object({ @@ -2060,29 +2065,34 @@ const WorkspaceGitHubRuntimePayloadSchema = z .optional() .nullable(); -export const WorkspaceDescriptorPayloadSchema = z.object({ - id: z.string(), - projectId: z.string(), - projectDisplayName: z.string(), - projectRootPath: z.string(), - workspaceDirectory: z.string(), - projectKind: z.enum(["git", "non_git", "directory"]), - // COMPAT(workspaces): keep legacy directory workspace kind parseable. - workspaceKind: z.enum(["directory", "local_checkout", "checkout", "worktree"]), - name: z.string(), - status: WorkspaceStateBucketSchema, - activityAt: z.string().nullable(), - diffStat: z - .object({ - additions: z.number(), - deletions: z.number(), - }) - .nullable() - .optional(), - scripts: z.array(WorkspaceScriptPayloadSchema).default([]), - gitRuntime: WorkspaceGitRuntimePayloadSchema, - githubRuntime: WorkspaceGitHubRuntimePayloadSchema, -}); +export const WorkspaceDescriptorPayloadSchema = z + .object({ + id: z.string(), + projectId: z.string(), + projectDisplayName: z.string(), + projectRootPath: z.string(), + workspaceDirectory: z.string().optional(), + projectKind: z.enum(["git", "non_git", "directory"]), + // COMPAT(workspaces): keep legacy directory workspace kind parseable. + workspaceKind: z.enum(["directory", "local_checkout", "checkout", "worktree"]), + name: z.string(), + status: WorkspaceStateBucketSchema, + activityAt: z.string().nullable(), + diffStat: z + .object({ + additions: z.number(), + deletions: z.number(), + }) + .nullable() + .optional(), + scripts: z.array(WorkspaceScriptPayloadSchema).default([]), + gitRuntime: WorkspaceGitRuntimePayloadSchema, + githubRuntime: WorkspaceGitHubRuntimePayloadSchema, + }) + .transform((workspace) => ({ + ...workspace, + workspaceDirectory: workspace.workspaceDirectory ?? workspace.projectRootPath, + })); export const AgentUpdateMessageSchema = z.object({ type: z.literal("agent_update"), diff --git a/packages/server/src/shared/messages.workspaces.test.ts b/packages/server/src/shared/messages.workspaces.test.ts index 93cdf3f45..08af11d48 100644 --- a/packages/server/src/shared/messages.workspaces.test.ts +++ b/packages/server/src/shared/messages.workspaces.test.ts @@ -254,6 +254,33 @@ describe("workspace message schemas", () => { ]); }); + test("parses legacy workspace descriptors without workspaceDirectory", () => { + const parsed = SessionOutboundMessageSchema.parse({ + type: "workspace_update", + payload: { + kind: "upsert", + workspace: { + id: "legacy-workspace", + projectId: "legacy-project", + projectDisplayName: "repo", + projectRootPath: "/repo", + projectKind: "git", + workspaceKind: "local_checkout", + name: "repo", + status: "done", + activityAt: null, + scripts: [], + }, + }, + }); + + expect(parsed.type).toBe("workspace_update"); + if (parsed.type !== "workspace_update" || parsed.payload.kind !== "upsert") { + throw new Error("Expected workspace_update upsert payload"); + } + expect(parsed.payload.workspace.workspaceDirectory).toBe("/repo"); + }); + test("parses legacy workspace descriptor enum values", () => { const parsed = SessionOutboundMessageSchema.parse({ type: "workspace_update",