Edit workspace files directly on web (#2270)
* feat(files): edit workspace files on web Keep source buffers synchronized with host file changes and require an explicit overwrite or reload when revisions diverge. * feat(panels): surface and protect modified tabs Expose tooltip and modification state through the generic panel boundary so tabs can show stable metadata and guard every close route consistently. * fix(tests): use portable fake timeout handle * fix(files): harden editor conflict handling Preserve modified panel state across tab eviction, use precise revisions for optimistic writes, coalesce concurrent file watchers, and localize the editor interface. * fix(files): close editor concurrency gaps Coalesce clean reloads, preserve subscriber identities and file permissions, suspend pending saves during close confirmation, and carry precise revisions through file reads. * test(files): expect read revision metadata
This commit is contained in:
@@ -2132,6 +2132,7 @@ const FileExplorerFileSchema = z.object({
|
||||
mimeType: z.string().optional(),
|
||||
size: z.number(),
|
||||
modifiedAt: z.string(),
|
||||
revision: z.string().optional(),
|
||||
});
|
||||
|
||||
const FileExplorerDirectorySchema = z.object({
|
||||
@@ -2148,6 +2149,52 @@ export const FileExplorerRequestSchema = z.object({
|
||||
acceptBinary: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const FileVersionSchema = z.discriminatedUnion("status", [
|
||||
z.object({
|
||||
status: z.literal("ready"),
|
||||
cwd: z.string(),
|
||||
path: z.string(),
|
||||
size: z.number().int().nonnegative(),
|
||||
modifiedAt: z.string(),
|
||||
revision: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
status: z.literal("missing"),
|
||||
cwd: z.string(),
|
||||
path: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
status: z.literal("error"),
|
||||
cwd: z.string(),
|
||||
path: z.string(),
|
||||
error: z.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const FileSubscribeRequestSchema = z.object({
|
||||
type: z.literal("fs.file.subscribe.request"),
|
||||
cwd: z.string(),
|
||||
path: z.string(),
|
||||
subscriptionId: z.string(),
|
||||
requestId: z.string(),
|
||||
});
|
||||
|
||||
export const FileUnsubscribeRequestSchema = z.object({
|
||||
type: z.literal("fs.file.unsubscribe.request"),
|
||||
subscriptionId: z.string(),
|
||||
requestId: z.string(),
|
||||
});
|
||||
|
||||
export const FileWriteRequestSchema = z.object({
|
||||
type: z.literal("fs.file.write.request"),
|
||||
cwd: z.string(),
|
||||
path: z.string(),
|
||||
content: z.string(),
|
||||
expectedModifiedAt: z.string(),
|
||||
expectedRevision: z.string().optional(),
|
||||
requestId: z.string(),
|
||||
});
|
||||
|
||||
export const ProjectIconRequestSchema = z.object({
|
||||
type: z.literal("project_icon_request"),
|
||||
cwd: z.string(),
|
||||
@@ -2463,6 +2510,9 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
|
||||
WorkspaceCreateRequestSchema,
|
||||
WorkspaceClearAttentionRequestSchema,
|
||||
FileExplorerRequestSchema,
|
||||
FileSubscribeRequestSchema,
|
||||
FileUnsubscribeRequestSchema,
|
||||
FileWriteRequestSchema,
|
||||
ProjectIconRequestSchema,
|
||||
FileDownloadTokenRequestSchema,
|
||||
FileUploadRequestSchema,
|
||||
@@ -2698,6 +2748,8 @@ export const ServerInfoStatusPayloadSchema = z
|
||||
worktreeRestore: z.boolean().optional(),
|
||||
// COMPAT(workspaceRecovery): added in v0.1.105, remove after 2027-01-11 once daemon floor >= v0.1.105.
|
||||
workspaceRecovery: z.boolean().optional(),
|
||||
// COMPAT(workspaceFileEditing): added in v0.2.0, remove after 2027-01-18 once daemon floor >= v0.2.0.
|
||||
workspaceFileEditing: z.boolean().optional(),
|
||||
// COMPAT(providerUsageList): added in v0.1.98, drop the gate when daemon floor >= v0.1.98.
|
||||
providerUsageList: z.boolean().optional(),
|
||||
// COMPAT(agentDetach): added in v0.1.98, remove gate after 2026-12-19 once daemon floor >= v0.1.98.
|
||||
@@ -4552,6 +4604,50 @@ export const FileExplorerResponseSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
export const FileSubscribeResponseSchema = z.object({
|
||||
type: z.literal("fs.file.subscribe.response"),
|
||||
payload: z.object({
|
||||
subscriptionId: z.string(),
|
||||
initial: FileVersionSchema,
|
||||
requestId: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const FileUnsubscribeResponseSchema = z.object({
|
||||
type: z.literal("fs.file.unsubscribe.response"),
|
||||
payload: z.object({
|
||||
subscriptionId: z.string(),
|
||||
requestId: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const FileWriteResultSchema = z.discriminatedUnion("status", [
|
||||
z.object({
|
||||
status: z.literal("written"),
|
||||
modifiedAt: z.string(),
|
||||
size: z.number(),
|
||||
revision: z.string().optional(),
|
||||
}),
|
||||
z.object({ status: z.literal("conflict"), version: FileVersionSchema }),
|
||||
z.object({ status: z.literal("error"), error: z.string() }),
|
||||
]);
|
||||
|
||||
export const FileWriteResponseSchema = z.object({
|
||||
type: z.literal("fs.file.write.response"),
|
||||
payload: z.object({
|
||||
result: FileWriteResultSchema,
|
||||
requestId: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const FileUpdateSchema = z.object({
|
||||
type: z.literal("fs.file.update"),
|
||||
payload: z.object({
|
||||
subscriptionId: z.string(),
|
||||
version: FileVersionSchema,
|
||||
}),
|
||||
});
|
||||
|
||||
const ProjectIconSchema = z.object({
|
||||
data: z.string(),
|
||||
mimeType: z.string(),
|
||||
@@ -5099,6 +5195,10 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
|
||||
PaseoWorktreeArchiveResponseSchema,
|
||||
CreatePaseoWorktreeResponseSchema,
|
||||
FileExplorerResponseSchema,
|
||||
FileSubscribeResponseSchema,
|
||||
FileUnsubscribeResponseSchema,
|
||||
FileWriteResponseSchema,
|
||||
FileUpdateSchema,
|
||||
ProjectIconResponseSchema,
|
||||
FileDownloadTokenResponseSchema,
|
||||
FileUploadResponseSchema,
|
||||
@@ -5493,6 +5593,15 @@ export type ArchiveWorkspaceRequest = z.infer<typeof ArchiveWorkspaceRequestSche
|
||||
export type WorkspaceClearAttentionRequest = z.infer<typeof WorkspaceClearAttentionRequestSchema>;
|
||||
export type FileExplorerRequest = z.infer<typeof FileExplorerRequestSchema>;
|
||||
export type FileExplorerResponse = z.infer<typeof FileExplorerResponseSchema>;
|
||||
export type FileVersion = z.infer<typeof FileVersionSchema>;
|
||||
export type FileSubscribeRequest = z.infer<typeof FileSubscribeRequestSchema>;
|
||||
export type FileSubscribeResponse = z.infer<typeof FileSubscribeResponseSchema>;
|
||||
export type FileUnsubscribeRequest = z.infer<typeof FileUnsubscribeRequestSchema>;
|
||||
export type FileUnsubscribeResponse = z.infer<typeof FileUnsubscribeResponseSchema>;
|
||||
export type FileWriteRequest = z.infer<typeof FileWriteRequestSchema>;
|
||||
export type FileWriteResponse = z.infer<typeof FileWriteResponseSchema>;
|
||||
export type FileWriteResult = z.infer<typeof FileWriteResultSchema>;
|
||||
export type FileUpdate = z.infer<typeof FileUpdateSchema>;
|
||||
export type ProjectIconRequest = z.infer<typeof ProjectIconRequestSchema>;
|
||||
export type ProjectIconResponse = z.infer<typeof ProjectIconResponseSchema>;
|
||||
export type ProjectIcon = z.infer<typeof ProjectIconSchema>;
|
||||
|
||||
Reference in New Issue
Block a user