mirror of
https://github.com/harivansh-afk/clanker-agent.git
synced 2026-04-16 03:02:00 +00:00
move pi-mono into companion-cloud as apps/companion-os
- Copy all pi-mono source into apps/companion-os/ - Update Dockerfile to COPY pre-built binary instead of downloading from GitHub Releases - Update deploy-staging.yml to build pi from source (bun compile) before Docker build - Add apps/companion-os/** to path triggers - No more cross-repo dispatch needed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
0250f72976
579 changed files with 206942 additions and 0 deletions
207
packages/coding-agent/test/session-selector-path-delete.test.ts
Normal file
207
packages/coding-agent/test/session-selector-path-delete.test.ts
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
import {
|
||||
DEFAULT_EDITOR_KEYBINDINGS,
|
||||
EditorKeybindingsManager,
|
||||
setEditorKeybindings,
|
||||
} from "@mariozechner/pi-tui";
|
||||
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { KeybindingsManager } from "../src/core/keybindings.js";
|
||||
import type { SessionInfo } from "../src/core/session-manager.js";
|
||||
import { SessionSelectorComponent } from "../src/modes/interactive/components/session-selector.js";
|
||||
import { initTheme } from "../src/modes/interactive/theme/theme.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", () => {
|
||||
const keybindings = KeybindingsManager.inMemory();
|
||||
|
||||
beforeEach(() => {
|
||||
// Ensure test isolation: editor keybindings are a global singleton
|
||||
setEditorKeybindings(
|
||||
new EditorKeybindingsManager(DEFAULT_EDITOR_KEYBINDINGS),
|
||||
);
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
// session selector uses the global theme instance
|
||||
initTheme("dark");
|
||||
});
|
||||
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 () => [],
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
{ keybindings },
|
||||
);
|
||||
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 () => [],
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
{ keybindings },
|
||||
);
|
||||
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 () => [],
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
{ keybindings },
|
||||
);
|
||||
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;
|
||||
},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
{ keybindings },
|
||||
);
|
||||
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;
|
||||
},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
{ keybindings },
|
||||
);
|
||||
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