fix(release): preserve 0.1.60 schema compatibility

This commit is contained in:
Mohamed Boudra
2026-04-22 20:37:19 +07:00
parent 484e453401
commit c6680b3001
4 changed files with 219 additions and 113 deletions
+7 -1
View File
@@ -1,6 +1,6 @@
# Changelog # Changelog
## 0.1.60-beta.2 - 2026-04-22 ## 0.1.60 - 2026-04-22
### Added ### 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). - 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. - 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. - 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. - `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 ### Fixed
- Composer textarea shrinks back down after sending on web. - Composer textarea shrinks back down after sending on web.
@@ -42,6 +43,11 @@
- iOS image picker no longer leaves the screen unresponsive after cancelling. - iOS image picker no longer leaves the screen unresponsive after cancelling.
- Archiving a worktree recovers cleanly if a previous attempt was interrupted. - Archiving a worktree recovers cleanly if a previous attempt was interrupted.
- Images in agent messages with `~`-prefixed paths load instead of spinning forever. - 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 ## 0.1.59 - 2026-04-16
@@ -46,6 +46,69 @@ describe("shared messages stream parsing", () => {
expect(parsed.payload.entries[0]?.item.type).toBe("assistant_message"); 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", () => { it("parses explicit shutdown and restart lifecycle request payloads as distinct message types", () => {
const shutdownParsed = SessionInboundMessageSchema.safeParse({ const shutdownParsed = SessionInboundMessageSchema.safeParse({
type: "shutdown_server_request", type: "shutdown_server_request",
+122 -112
View File
@@ -251,7 +251,11 @@ export const AgentPermissionResponseSchema: z.ZodType<AgentPermissionResponse> =
}), }),
]); ]);
export const AgentPermissionRequestPayloadSchema: z.ZodType<AgentPermissionRequest> = z.object({ export const AgentPermissionRequestPayloadSchema: z.ZodType<
AgentPermissionRequest,
z.ZodTypeDef,
unknown
> = z.object({
id: z.string(), id: z.string(),
provider: AgentProviderSchema, provider: AgentProviderSchema,
name: z.string(), name: z.string(),
@@ -286,7 +290,7 @@ const WorktreeSetupCommandSnapshotSchema = z.object({
index: z.number().int().positive(), index: z.number().int().positive(),
command: z.string(), command: z.string(),
cwd: z.string(), cwd: z.string(),
log: z.string(), log: z.string().optional().default(""),
status: z.enum(["running", "completed", "failed"]), status: z.enum(["running", "completed", "failed"]),
exitCode: z.number().nullable(), exitCode: z.number().nullable(),
durationMs: z.number().nonnegative().optional(), durationMs: z.number().nonnegative().optional(),
@@ -301,95 +305,96 @@ const WorktreeSetupDetailPayloadSchema = z.object({
truncated: z.boolean().optional(), truncated: z.boolean().optional(),
}); });
const ToolCallDetailPayloadSchema: z.ZodType<ToolCallDetail> = z.discriminatedUnion("type", [ const ToolCallDetailPayloadSchema: z.ZodType<ToolCallDetail, z.ZodTypeDef, unknown> =
WorktreeSetupDetailPayloadSchema, z.discriminatedUnion("type", [
z.object({ WorktreeSetupDetailPayloadSchema,
type: z.literal("shell"), z.object({
command: z.string(), type: z.literal("shell"),
cwd: z.string().optional(), command: z.string(),
output: z.string().optional(), cwd: z.string().optional(),
exitCode: z.number().nullable().optional(), output: z.string().optional(),
}), exitCode: z.number().nullable().optional(),
z.object({ }),
type: z.literal("read"), z.object({
filePath: z.string(), type: z.literal("read"),
content: z.string().optional(), filePath: z.string(),
offset: z.number().optional(), content: z.string().optional(),
limit: z.number().optional(), offset: z.number().optional(),
}), limit: z.number().optional(),
z.object({ }),
type: z.literal("edit"), z.object({
filePath: z.string(), type: z.literal("edit"),
oldString: z.string().optional(), filePath: z.string(),
newString: z.string().optional(), oldString: z.string().optional(),
unifiedDiff: z.string().optional(), newString: z.string().optional(),
}), unifiedDiff: z.string().optional(),
z.object({ }),
type: z.literal("write"), z.object({
filePath: z.string(), type: z.literal("write"),
content: z.string().optional(), filePath: z.string(),
}), content: z.string().optional(),
z.object({ }),
type: z.literal("search"), z.object({
query: z.string(), type: z.literal("search"),
toolName: z.enum(["search", "grep", "glob", "web_search"]).optional(), query: z.string(),
content: z.string().optional(), toolName: z.enum(["search", "grep", "glob", "web_search"]).optional(),
filePaths: z.array(z.string()).optional(), content: z.string().optional(),
webResults: z filePaths: z.array(z.string()).optional(),
.array( 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({ z.object({
title: z.string(), index: z.number().int().positive(),
url: z.string(), toolName: z.string(),
summary: z.string().optional(),
}), }),
) ),
.optional(), }),
annotations: z.array(z.string()).optional(), z.object({
numFiles: z.number().optional(), type: z.literal("plain_text"),
numMatches: z.number().optional(), label: z.string().optional(),
durationMs: z.number().optional(), text: z.string().optional(),
durationSeconds: z.number().optional(), icon: z.enum(TOOL_CALL_ICON_NAMES).optional(),
truncated: z.boolean().optional(), }),
mode: z.enum(["content", "files_with_matches", "count"]).optional(), z.object({
}), type: z.literal("plan"),
z.object({ text: z.string(),
type: z.literal("fetch"), }),
url: z.string(), z.object({
prompt: z.string().optional(), type: z.literal("unknown"),
result: z.string().optional(), input: UnknownValueSchema,
code: z.number().optional(), output: UnknownValueSchema,
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,
}),
]);
const ToolCallBasePayloadSchema = z const ToolCallBasePayloadSchema = z
.object({ .object({
@@ -2060,29 +2065,34 @@ const WorkspaceGitHubRuntimePayloadSchema = z
.optional() .optional()
.nullable(); .nullable();
export const WorkspaceDescriptorPayloadSchema = z.object({ export const WorkspaceDescriptorPayloadSchema = z
id: z.string(), .object({
projectId: z.string(), id: z.string(),
projectDisplayName: z.string(), projectId: z.string(),
projectRootPath: z.string(), projectDisplayName: z.string(),
workspaceDirectory: z.string(), projectRootPath: z.string(),
projectKind: z.enum(["git", "non_git", "directory"]), workspaceDirectory: z.string().optional(),
// COMPAT(workspaces): keep legacy directory workspace kind parseable. projectKind: z.enum(["git", "non_git", "directory"]),
workspaceKind: z.enum(["directory", "local_checkout", "checkout", "worktree"]), // COMPAT(workspaces): keep legacy directory workspace kind parseable.
name: z.string(), workspaceKind: z.enum(["directory", "local_checkout", "checkout", "worktree"]),
status: WorkspaceStateBucketSchema, name: z.string(),
activityAt: z.string().nullable(), status: WorkspaceStateBucketSchema,
diffStat: z activityAt: z.string().nullable(),
.object({ diffStat: z
additions: z.number(), .object({
deletions: z.number(), additions: z.number(),
}) deletions: z.number(),
.nullable() })
.optional(), .nullable()
scripts: z.array(WorkspaceScriptPayloadSchema).default([]), .optional(),
gitRuntime: WorkspaceGitRuntimePayloadSchema, scripts: z.array(WorkspaceScriptPayloadSchema).default([]),
githubRuntime: WorkspaceGitHubRuntimePayloadSchema, gitRuntime: WorkspaceGitRuntimePayloadSchema,
}); githubRuntime: WorkspaceGitHubRuntimePayloadSchema,
})
.transform((workspace) => ({
...workspace,
workspaceDirectory: workspace.workspaceDirectory ?? workspace.projectRootPath,
}));
export const AgentUpdateMessageSchema = z.object({ export const AgentUpdateMessageSchema = z.object({
type: z.literal("agent_update"), type: z.literal("agent_update"),
@@ -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", () => { test("parses legacy workspace descriptor enum values", () => {
const parsed = SessionOutboundMessageSchema.parse({ const parsed = SessionOutboundMessageSchema.parse({
type: "workspace_update", type: "workspace_update",