mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 17:01:02 +00:00
feat(coding-agent): add session path toggle and deletion to /resume
This commit is contained in:
parent
d43930c818
commit
26fe048314
6 changed files with 520 additions and 51 deletions
180
packages/coding-agent/test/session-selector-path-delete.test.ts
Normal file
180
packages/coding-agent/test/session-selector-path-delete.test.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import type { SessionInfo } from "../src/core/session-manager.js";
|
||||
import { SessionSelectorComponent } from "../src/modes/interactive/components/session-selector.js";
|
||||
|
||||
type Deferred<T> = {
|
||||
promise: Promise<T>;
|
||||
resolve: (value: T) => void;
|
||||
reject: (err: unknown) => void;
|
||||
};
|
||||
|
||||
function createDeferred<T>(): Deferred<T> {
|
||||
let resolve: (value: T) => void = () => {};
|
||||
let reject: (err: unknown) => void = () => {};
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
async function flushPromises(): Promise<void> {
|
||||
await new Promise<void>((resolve) => {
|
||||
setImmediate(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function makeSession(overrides: Partial<SessionInfo> & { id: string }): SessionInfo {
|
||||
return {
|
||||
path: overrides.path ?? `/tmp/${overrides.id}.jsonl`,
|
||||
id: overrides.id,
|
||||
cwd: overrides.cwd ?? "",
|
||||
name: overrides.name,
|
||||
created: overrides.created ?? new Date(0),
|
||||
modified: overrides.modified ?? new Date(0),
|
||||
messageCount: overrides.messageCount ?? 1,
|
||||
firstMessage: overrides.firstMessage ?? "hello",
|
||||
allMessagesText: overrides.allMessagesText ?? "hello",
|
||||
};
|
||||
}
|
||||
|
||||
const CTRL_D = "\x04";
|
||||
const CTRL_BACKSPACE = "\x1b[127;5u";
|
||||
|
||||
describe("session selector path/delete interactions", () => {
|
||||
it("does not treat Ctrl+Backspace as delete when search query is non-empty", async () => {
|
||||
const sessions = [makeSession({ id: "a" }), makeSession({ id: "b" })];
|
||||
|
||||
const selector = new SessionSelectorComponent(
|
||||
async () => sessions,
|
||||
async () => [],
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
const list = selector.getSessionList();
|
||||
const confirmationChanges: Array<string | null> = [];
|
||||
list.onDeleteConfirmationChange = (path) => confirmationChanges.push(path);
|
||||
|
||||
list.handleInput("a");
|
||||
list.handleInput(CTRL_BACKSPACE);
|
||||
|
||||
expect(confirmationChanges).toEqual([]);
|
||||
});
|
||||
|
||||
it("enters confirmation mode on Ctrl+D even with a non-empty search query", async () => {
|
||||
const sessions = [makeSession({ id: "a" }), makeSession({ id: "b" })];
|
||||
|
||||
const selector = new SessionSelectorComponent(
|
||||
async () => sessions,
|
||||
async () => [],
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
const list = selector.getSessionList();
|
||||
const confirmationChanges: Array<string | null> = [];
|
||||
list.onDeleteConfirmationChange = (path) => confirmationChanges.push(path);
|
||||
|
||||
list.handleInput("a");
|
||||
list.handleInput(CTRL_D);
|
||||
|
||||
expect(confirmationChanges).toEqual([sessions[0]!.path]);
|
||||
});
|
||||
|
||||
it("enters confirmation mode on Ctrl+Backspace when search query is empty", async () => {
|
||||
const sessions = [makeSession({ id: "a" }), makeSession({ id: "b" })];
|
||||
|
||||
const selector = new SessionSelectorComponent(
|
||||
async () => sessions,
|
||||
async () => [],
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
const list = selector.getSessionList();
|
||||
const confirmationChanges: Array<string | null> = [];
|
||||
list.onDeleteConfirmationChange = (path) => confirmationChanges.push(path);
|
||||
|
||||
let deletedPath: string | null = null;
|
||||
list.onDeleteSession = async (sessionPath) => {
|
||||
deletedPath = sessionPath;
|
||||
};
|
||||
|
||||
list.handleInput(CTRL_BACKSPACE);
|
||||
expect(confirmationChanges).toEqual([sessions[0]!.path]);
|
||||
|
||||
list.handleInput("\r");
|
||||
expect(confirmationChanges).toEqual([sessions[0]!.path, null]);
|
||||
expect(deletedPath).toBe(sessions[0]!.path);
|
||||
});
|
||||
|
||||
it("does not switch scope back to All when All load resolves after toggling back to Current", async () => {
|
||||
const currentSessions = [makeSession({ id: "current" })];
|
||||
const allDeferred = createDeferred<SessionInfo[]>();
|
||||
let allLoadCalls = 0;
|
||||
|
||||
const selector = new SessionSelectorComponent(
|
||||
async () => currentSessions,
|
||||
async () => {
|
||||
allLoadCalls++;
|
||||
return allDeferred.promise;
|
||||
},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
const list = selector.getSessionList();
|
||||
list.handleInput("\t"); // current -> all (starts async load)
|
||||
list.handleInput("\t"); // all -> current
|
||||
|
||||
allDeferred.resolve([makeSession({ id: "all" })]);
|
||||
await flushPromises();
|
||||
|
||||
expect(allLoadCalls).toBe(1);
|
||||
const output = selector.render(120).join("\n");
|
||||
expect(output).toContain("Resume Session (Current Folder)");
|
||||
expect(output).not.toContain("Resume Session (All)");
|
||||
});
|
||||
|
||||
it("does not start redundant All loads when toggling scopes while All is already loading", async () => {
|
||||
const currentSessions = [makeSession({ id: "current" })];
|
||||
const allDeferred = createDeferred<SessionInfo[]>();
|
||||
let allLoadCalls = 0;
|
||||
|
||||
const selector = new SessionSelectorComponent(
|
||||
async () => currentSessions,
|
||||
async () => {
|
||||
allLoadCalls++;
|
||||
return allDeferred.promise;
|
||||
},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
const list = selector.getSessionList();
|
||||
list.handleInput("\t"); // current -> all (starts async load)
|
||||
list.handleInput("\t"); // all -> current
|
||||
list.handleInput("\t"); // current -> all again while load pending
|
||||
|
||||
expect(allLoadCalls).toBe(1);
|
||||
|
||||
allDeferred.resolve([makeSession({ id: "all" })]);
|
||||
await flushPromises();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue