feat: add file manager context actions (#3027)

* feat(app): add file manager context actions

* fix: reshape file context actions

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
nikuscs
2026-08-10 18:18:23 +02:00
committed by GitHub
co-authored by Mohamed Boudra
parent a98d154b6b
commit ca2b633286
36 changed files with 4159 additions and 227 deletions
@@ -0,0 +1,168 @@
import { describe, expect, test } from "vitest";
import {
CheckoutDiscardChangesRequestSchema,
CheckoutDiscardChangesResponseSchema,
FileEntryCreateRequestSchema,
FileEntryCreateResponseSchema,
FileEntryDeleteRequestSchema,
FileEntryDeleteResponseSchema,
FileEntryDuplicateRequestSchema,
FileEntryDuplicateResponseSchema,
FileEntryRenameRequestSchema,
FileEntryRenameResponseSchema,
ServerInfoStatusPayloadSchema,
} from "./messages.js";
describe("file context action messages", () => {
test("keeps context-action capabilities optional and preserves advertised support", () => {
const legacy = ServerInfoStatusPayloadSchema.parse({
status: "server_info",
serverId: "server-1",
features: {},
});
expect(legacy.features?.fsEntryOps).toBeUndefined();
expect(legacy.features?.fsEntryDuplicate).toBeUndefined();
expect(legacy.features?.checkoutDiscardChanges).toBeUndefined();
const current = ServerInfoStatusPayloadSchema.parse({
status: "server_info",
serverId: "server-1",
features: {
fsEntryOps: true,
fsEntryDuplicate: true,
checkoutDiscardChanges: true,
},
});
expect(current.features).toMatchObject({
fsEntryOps: true,
fsEntryDuplicate: true,
checkoutDiscardChanges: true,
});
});
test("rejects discard requests without a path", () => {
expect(() =>
CheckoutDiscardChangesRequestSchema.parse({
type: "checkout.discard_changes.request",
cwd: "/workspace",
paths: [],
requestId: "discard-empty",
}),
).toThrow();
});
test("round-trips file entry create requests and responses", () => {
const request = {
type: "fs.entry.create.request",
cwd: "/workspace",
parentPath: "src/components",
name: "button.tsx",
kind: "file",
requestId: "create-1",
};
expect(FileEntryCreateRequestSchema.parse(request)).toEqual(request);
const response = {
type: "fs.entry.create.response",
payload: {
cwd: "/workspace",
parentPath: "src/components",
path: "src/components/button.tsx",
success: true,
error: null,
requestId: "create-1",
},
};
expect(FileEntryCreateResponseSchema.parse(response)).toEqual(response);
});
test("round-trips file entry rename requests and responses", () => {
const request = {
type: "fs.entry.rename.request",
cwd: "/workspace",
path: "src/old.ts",
name: "new.ts",
requestId: "rename-1",
};
expect(FileEntryRenameRequestSchema.parse(request)).toEqual(request);
const response = {
type: "fs.entry.rename.response",
payload: {
cwd: "/workspace",
path: "src/old.ts",
renamedPath: "src/new.ts",
success: true,
error: null,
requestId: "rename-1",
},
};
expect(FileEntryRenameResponseSchema.parse(response)).toEqual(response);
});
test("round-trips file entry duplicate requests and responses", () => {
const request = {
type: "fs.entry.duplicate.request",
cwd: "/workspace",
path: "src/button.tsx",
requestId: "duplicate-1",
};
expect(FileEntryDuplicateRequestSchema.parse(request)).toEqual(request);
const response = {
type: "fs.entry.duplicate.response",
payload: {
cwd: "/workspace",
path: "src/button.tsx",
duplicatedPath: "src/button copy.tsx",
success: true,
error: null,
requestId: "duplicate-1",
},
};
expect(FileEntryDuplicateResponseSchema.parse(response)).toEqual(response);
});
test("round-trips file entry delete requests and responses", () => {
const request = {
type: "fs.entry.delete.request",
cwd: "/workspace",
path: "src/old.ts",
requestId: "delete-1",
};
expect(FileEntryDeleteRequestSchema.parse(request)).toEqual(request);
const response = {
type: "fs.entry.delete.response",
payload: {
cwd: "/workspace",
path: "src/old.ts",
success: false,
error: "File or folder no longer exists",
requestId: "delete-1",
},
};
expect(FileEntryDeleteResponseSchema.parse(response)).toEqual(response);
});
test("round-trips checkout discard requests and responses", () => {
const request = {
type: "checkout.discard_changes.request",
cwd: "/workspace",
paths: ["src", "README.md"],
requestId: "discard-1",
};
expect(CheckoutDiscardChangesRequestSchema.parse(request)).toEqual(request);
const response = {
type: "checkout.discard_changes.response",
payload: {
cwd: "/workspace",
success: false,
error: { code: "UNKNOWN", message: "discard failed" },
requestId: "discard-1",
},
};
expect(CheckoutDiscardChangesResponseSchema.parse(response)).toEqual(response);
});
});
+123
View File
@@ -1777,6 +1777,13 @@ export const CheckoutRefreshRequestSchema = z.object({
requestId: z.string(),
});
export const CheckoutDiscardChangesRequestSchema = z.object({
type: z.literal("checkout.discard_changes.request"),
cwd: z.string(),
paths: z.array(z.string()).min(1),
requestId: z.string(),
});
export const CheckoutPrCreateRequestSchema = z.object({
type: z.literal("checkout_pr_create_request"),
cwd: z.string(),
@@ -2204,6 +2211,8 @@ const DiffHunkSchema = z.object({
const ParsedDiffFileSchema = z.object({
path: z.string(),
// COMPAT(diffOldPath): added in v0.3.0, remove gate after 2027-02-09.
oldPath: z.string().optional(),
isNew: z.boolean(),
isDeleted: z.boolean(),
additions: z.number(),
@@ -2291,6 +2300,37 @@ export const FileWriteRequestSchema = z.object({
requestId: z.string(),
});
export const FileEntryCreateRequestSchema = z.object({
type: z.literal("fs.entry.create.request"),
cwd: z.string(),
parentPath: z.string(),
name: z.string(),
kind: z.enum(["file", "directory"]),
requestId: z.string(),
});
export const FileEntryRenameRequestSchema = z.object({
type: z.literal("fs.entry.rename.request"),
cwd: z.string(),
path: z.string(),
name: z.string(),
requestId: z.string(),
});
export const FileEntryDuplicateRequestSchema = z.object({
type: z.literal("fs.entry.duplicate.request"),
cwd: z.string(),
path: z.string(),
requestId: z.string(),
});
export const FileEntryDeleteRequestSchema = z.object({
type: z.literal("fs.entry.delete.request"),
cwd: z.string(),
path: z.string(),
requestId: z.string(),
});
export const ProjectIconRequestSchema = z.object({
type: z.literal("project_icon_request"),
cwd: z.string(),
@@ -2661,6 +2701,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
CheckoutPullRequestSchema,
CheckoutPushRequestSchema,
CheckoutRefreshRequestSchema,
CheckoutDiscardChangesRequestSchema,
CheckoutPrCreateRequestSchema,
CheckoutPrMergeRequestSchema,
CheckoutForgeSetAutoMergeRequestSchema,
@@ -2699,6 +2740,10 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
FileSubscribeRequestSchema,
FileUnsubscribeRequestSchema,
FileWriteRequestSchema,
FileEntryCreateRequestSchema,
FileEntryRenameRequestSchema,
FileEntryDuplicateRequestSchema,
FileEntryDeleteRequestSchema,
ProjectIconRequestSchema,
ProjectIconGetRequestSchema,
FileDownloadTokenRequestSchema,
@@ -3004,6 +3049,12 @@ export const ServerInfoStatusPayloadSchema = z
workspaceScriptManagement: z.boolean().optional(),
// COMPAT(projectCustomIcon): added in v0.2.0, remove after 2027-01-20.
projectCustomIcon: z.boolean().optional(),
// COMPAT(fsEntryOps): added in v0.3.0, remove gate after 2027-02-08.
fsEntryOps: z.boolean().optional(),
// COMPAT(fsEntryDuplicate): added in v0.3.0, remove gate after 2027-02-09.
fsEntryDuplicate: z.boolean().optional(),
// COMPAT(checkoutDiscardChanges): added in v0.3.0, remove gate after 2027-02-08.
checkoutDiscardChanges: z.boolean().optional(),
})
.optional(),
})
@@ -4493,6 +4544,16 @@ export const CheckoutGithubSetAutoMergeResponseSchema = z.object({
}),
});
export const CheckoutDiscardChangesResponseSchema = z.object({
type: z.literal("checkout.discard_changes.response"),
payload: z.object({
cwd: z.string(),
success: z.boolean(),
error: CheckoutErrorSchema.nullable(),
requestId: z.string(),
}),
});
export const CheckoutCommitsListResponseSchema = z.object({
type: z.literal("checkout.commits.list.response"),
payload: z.object({
@@ -4957,6 +5018,53 @@ export const FileWriteResponseSchema = z.object({
}),
});
export const FileEntryCreateResponseSchema = z.object({
type: z.literal("fs.entry.create.response"),
payload: z.object({
cwd: z.string(),
parentPath: z.string(),
path: z.string().nullable(),
success: z.boolean(),
error: z.string().nullable(),
requestId: z.string(),
}),
});
export const FileEntryRenameResponseSchema = z.object({
type: z.literal("fs.entry.rename.response"),
payload: z.object({
cwd: z.string(),
path: z.string(),
renamedPath: z.string().nullable(),
success: z.boolean(),
error: z.string().nullable(),
requestId: z.string(),
}),
});
export const FileEntryDuplicateResponseSchema = z.object({
type: z.literal("fs.entry.duplicate.response"),
payload: z.object({
cwd: z.string(),
path: z.string(),
duplicatedPath: z.string().nullable(),
success: z.boolean(),
error: z.string().nullable(),
requestId: z.string(),
}),
});
export const FileEntryDeleteResponseSchema = z.object({
type: z.literal("fs.entry.delete.response"),
payload: z.object({
cwd: z.string(),
path: z.string(),
success: z.boolean(),
error: z.string().nullable(),
requestId: z.string(),
}),
});
export const FileUpdateSchema = z.object({
type: z.literal("fs.file.update"),
payload: z.object({
@@ -5549,6 +5657,7 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
CheckoutPullResponseSchema,
CheckoutPushResponseSchema,
CheckoutRefreshResponseSchema,
CheckoutDiscardChangesResponseSchema,
CheckoutPrCreateResponseSchema,
CheckoutPrMergeResponseSchema,
CheckoutForgeSetAutoMergeResponseSchema,
@@ -5576,6 +5685,10 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
FileSubscribeResponseSchema,
FileUnsubscribeResponseSchema,
FileWriteResponseSchema,
FileEntryCreateResponseSchema,
FileEntryRenameResponseSchema,
FileEntryDuplicateResponseSchema,
FileEntryDeleteResponseSchema,
FileUpdateSchema,
ProjectIconResponseSchema,
ProjectIconGetResponseSchema,
@@ -5896,6 +6009,8 @@ export type CheckoutPushRequest = z.infer<typeof CheckoutPushRequestSchema>;
export type CheckoutPushResponse = z.infer<typeof CheckoutPushResponseSchema>;
export type CheckoutRefreshRequest = z.infer<typeof CheckoutRefreshRequestSchema>;
export type CheckoutRefreshResponse = z.infer<typeof CheckoutRefreshResponseSchema>;
export type CheckoutDiscardChangesRequest = z.infer<typeof CheckoutDiscardChangesRequestSchema>;
export type CheckoutDiscardChangesResponse = z.infer<typeof CheckoutDiscardChangesResponseSchema>;
export type CheckoutCommitFile = z.infer<typeof CheckoutCommitFileSchema>;
export type CheckoutCommit = z.infer<typeof CheckoutCommitSchema>;
export type CheckoutCommitsListRequest = z.infer<typeof CheckoutCommitsListRequestSchema>;
@@ -5999,6 +6114,14 @@ 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 FileEntryCreateRequest = z.infer<typeof FileEntryCreateRequestSchema>;
export type FileEntryCreateResponse = z.infer<typeof FileEntryCreateResponseSchema>;
export type FileEntryRenameRequest = z.infer<typeof FileEntryRenameRequestSchema>;
export type FileEntryRenameResponse = z.infer<typeof FileEntryRenameResponseSchema>;
export type FileEntryDuplicateRequest = z.infer<typeof FileEntryDuplicateRequestSchema>;
export type FileEntryDuplicateResponse = z.infer<typeof FileEntryDuplicateResponseSchema>;
export type FileEntryDeleteRequest = z.infer<typeof FileEntryDeleteRequestSchema>;
export type FileEntryDeleteResponse = z.infer<typeof FileEntryDeleteResponseSchema>;
export type FileWriteResult = z.infer<typeof FileWriteResultSchema>;
export type FileUpdate = z.infer<typeof FileUpdateSchema>;
export type ProjectIconRequest = z.infer<typeof ProjectIconRequestSchema>;