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
+175
View File
@@ -1727,6 +1727,181 @@ test("a candidate measurement that times out under a heartbeat tick does not cou
expect(session.teardownCount()).toBe(0);
});
test("file context action RPCs correlate success and error responses", async () => {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_context_actions",
logger: createMockLogger(),
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const createPromise = client.createFileEntry({
cwd: "/tmp/project",
parentPath: "src",
name: "new.ts",
kind: "file",
});
const createRequest = parseSentFrame(mock.sent.at(-1));
expect(createRequest).toEqual({
type: "fs.entry.create.request",
cwd: "/tmp/project",
parentPath: "src",
name: "new.ts",
kind: "file",
requestId: expect.any(String),
});
mock.triggerMessage(
wrapSessionMessage({
type: "fs.entry.create.response",
payload: {
cwd: "/tmp/project",
parentPath: "src",
path: "src/new.ts",
success: true,
error: null,
requestId: createRequest.requestId,
},
}),
);
await expect(createPromise).resolves.toMatchObject({
path: "src/new.ts",
success: true,
error: null,
});
const renamePromise = client.renameFileEntry({
cwd: "/tmp/project",
path: "src/new.ts",
name: "existing.ts",
});
const renameRequest = parseSentFrame(mock.sent.at(-1));
expect(renameRequest).toMatchObject({
type: "fs.entry.rename.request",
cwd: "/tmp/project",
path: "src/new.ts",
name: "existing.ts",
});
mock.triggerMessage(
wrapSessionMessage({
type: "fs.entry.rename.response",
payload: {
cwd: "/tmp/project",
path: "src/new.ts",
renamedPath: null,
success: false,
error: '"existing.ts" already exists',
requestId: renameRequest.requestId,
},
}),
);
await expect(renamePromise).resolves.toMatchObject({
renamedPath: null,
success: false,
error: '"existing.ts" already exists',
});
const duplicatePromise = client.duplicateFileEntry({
cwd: "/tmp/project",
path: "src/new.ts",
});
const duplicateRequest = parseSentFrame(mock.sent.at(-1));
expect(duplicateRequest).toMatchObject({
type: "fs.entry.duplicate.request",
cwd: "/tmp/project",
path: "src/new.ts",
});
mock.triggerMessage(
wrapSessionMessage({
type: "fs.entry.duplicate.response",
payload: {
cwd: "/tmp/project",
path: "src/new.ts",
duplicatedPath: "src/new copy.ts",
success: true,
error: null,
requestId: duplicateRequest.requestId,
},
}),
);
await expect(duplicatePromise).resolves.toMatchObject({
duplicatedPath: "src/new copy.ts",
success: true,
error: null,
});
const deletePromise = client.deleteFileEntry({ cwd: "/tmp/project", path: "src/new.ts" });
const deleteRequest = parseSentFrame(mock.sent.at(-1));
expect(deleteRequest).toMatchObject({
type: "fs.entry.delete.request",
cwd: "/tmp/project",
path: "src/new.ts",
});
mock.triggerMessage(
wrapSessionMessage({
type: "fs.entry.delete.response",
payload: {
cwd: "/tmp/project",
path: "src/new.ts",
success: true,
error: null,
requestId: deleteRequest.requestId,
},
}),
);
await expect(deletePromise).resolves.toMatchObject({ success: true, error: null });
const discardPromise = client.checkoutDiscardChanges("/tmp/project", { paths: ["src"] });
const discardRequest = parseSentFrame(mock.sent.at(-1));
expect(discardRequest).toMatchObject({
type: "checkout.discard_changes.request",
cwd: "/tmp/project",
paths: ["src"],
});
mock.triggerMessage(
wrapSessionMessage({
type: "checkout.discard_changes.response",
payload: {
cwd: "/tmp/project",
success: false,
error: { code: "NOT_GIT_REPO", message: "Not a git repository" },
requestId: discardRequest.requestId,
},
}),
);
await expect(discardPromise).resolves.toMatchObject({
success: false,
error: { code: "NOT_GIT_REPO", message: "Not a git repository" },
});
});
test("a connection loss rejects an in-flight file context action", async () => {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_context_disconnect",
logger: createMockLogger(),
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const pending = client.deleteFileEntry({ cwd: "/tmp/project", path: "src/file.ts" });
mock.triggerClose({ code: 1006, reason: "network lost" });
await expect(pending).rejects.toThrow(/network lost|disconnected|closed/i);
});
test("listDirectory sends a list file explorer request and returns directory entries", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
+48
View File
@@ -4329,6 +4329,54 @@ export class DaemonClient {
return payload.result;
}
async createFileEntry(input: {
cwd: string;
parentPath: string;
name: string;
kind: "file" | "directory";
}): Promise<CorrelatedResponsePayload<"fs.entry.create.response">> {
return this.sendNamespacedCorrelatedSessionRequest<"fs.entry.create.response">({
message: { type: "fs.entry.create.request", ...input },
});
}
async renameFileEntry(input: {
cwd: string;
path: string;
name: string;
}): Promise<CorrelatedResponsePayload<"fs.entry.rename.response">> {
return this.sendNamespacedCorrelatedSessionRequest<"fs.entry.rename.response">({
message: { type: "fs.entry.rename.request", ...input },
});
}
async duplicateFileEntry(input: {
cwd: string;
path: string;
}): Promise<CorrelatedResponsePayload<"fs.entry.duplicate.response">> {
return this.sendNamespacedCorrelatedSessionRequest<"fs.entry.duplicate.response">({
message: { type: "fs.entry.duplicate.request", ...input },
});
}
async deleteFileEntry(input: {
cwd: string;
path: string;
}): Promise<CorrelatedResponsePayload<"fs.entry.delete.response">> {
return this.sendNamespacedCorrelatedSessionRequest<"fs.entry.delete.response">({
message: { type: "fs.entry.delete.request", ...input },
});
}
async checkoutDiscardChanges(
cwd: string,
input: { paths: string[] },
): Promise<CorrelatedResponsePayload<"checkout.discard_changes.response">> {
return this.sendNamespacedCorrelatedSessionRequest<"checkout.discard_changes.response">({
message: { type: "checkout.discard_changes.request", cwd, paths: input.paths },
});
}
async uploadFile(input: FileUploadInput): Promise<FileUploadResult> {
const bytes = asUint8Array(input.bytes);
if (!bytes) {