feat: stream worktree setup progress and terminal subscriptions
This commit is contained in:
@@ -201,4 +201,52 @@ describe("TerminalManager", () => {
|
||||
expect(manager.getTerminal(homeId)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("subscribeTerminalsChanged", () => {
|
||||
it("emits cwd snapshots when terminals are created", async () => {
|
||||
manager = createTerminalManager();
|
||||
const snapshots: Array<{ cwd: string; terminalNames: string[] }> = [];
|
||||
const unsubscribe = manager.subscribeTerminalsChanged((input) => {
|
||||
snapshots.push({
|
||||
cwd: input.cwd,
|
||||
terminalNames: input.terminals.map((terminal) => terminal.name),
|
||||
});
|
||||
});
|
||||
|
||||
await manager.getTerminals("/tmp");
|
||||
await manager.createTerminal({ cwd: "/tmp", name: "Dev Server" });
|
||||
|
||||
expect(snapshots).toContainEqual({
|
||||
cwd: "/tmp",
|
||||
terminalNames: ["Terminal 1"],
|
||||
});
|
||||
expect(snapshots).toContainEqual({
|
||||
cwd: "/tmp",
|
||||
terminalNames: ["Terminal 1", "Dev Server"],
|
||||
});
|
||||
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
it("emits empty snapshot when last terminal is removed", async () => {
|
||||
manager = createTerminalManager();
|
||||
const snapshots: Array<{ cwd: string; terminalCount: number }> = [];
|
||||
const unsubscribe = manager.subscribeTerminalsChanged((input) => {
|
||||
snapshots.push({
|
||||
cwd: input.cwd,
|
||||
terminalCount: input.terminals.length,
|
||||
});
|
||||
});
|
||||
|
||||
const terminals = await manager.getTerminals("/tmp");
|
||||
manager.killTerminal(terminals[0].id);
|
||||
|
||||
expect(snapshots).toContainEqual({
|
||||
cwd: "/tmp",
|
||||
terminalCount: 0,
|
||||
});
|
||||
|
||||
unsubscribe();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import { createTerminal, type TerminalSession } from "./terminal.js";
|
||||
|
||||
export interface TerminalListItem {
|
||||
id: string;
|
||||
name: string;
|
||||
cwd: string;
|
||||
}
|
||||
|
||||
export interface TerminalsChangedEvent {
|
||||
cwd: string;
|
||||
terminals: TerminalListItem[];
|
||||
}
|
||||
|
||||
export type TerminalsChangedListener = (input: TerminalsChangedEvent) => void;
|
||||
|
||||
export interface TerminalManager {
|
||||
getTerminals(cwd: string): Promise<TerminalSession[]>;
|
||||
createTerminal(options: { cwd: string; name?: string }): Promise<TerminalSession>;
|
||||
@@ -7,12 +20,14 @@ export interface TerminalManager {
|
||||
killTerminal(id: string): void;
|
||||
listDirectories(): string[];
|
||||
killAll(): void;
|
||||
subscribeTerminalsChanged(listener: TerminalsChangedListener): () => void;
|
||||
}
|
||||
|
||||
export function createTerminalManager(): TerminalManager {
|
||||
const terminalsByCwd = new Map<string, TerminalSession[]>();
|
||||
const terminalsById = new Map<string, TerminalSession>();
|
||||
const terminalExitUnsubscribeById = new Map<string, () => void>();
|
||||
const terminalsChangedListeners = new Set<TerminalsChangedListener>();
|
||||
|
||||
function assertAbsolutePath(cwd: string): void {
|
||||
if (!cwd.startsWith("/")) {
|
||||
@@ -48,6 +63,8 @@ export function createTerminalManager(): TerminalManager {
|
||||
if (options.kill) {
|
||||
session.kill();
|
||||
}
|
||||
|
||||
emitTerminalsChanged({ cwd: session.cwd });
|
||||
}
|
||||
|
||||
function registerSession(session: TerminalSession): TerminalSession {
|
||||
@@ -59,6 +76,36 @@ export function createTerminalManager(): TerminalManager {
|
||||
return session;
|
||||
}
|
||||
|
||||
function toTerminalListItem(input: { session: TerminalSession }): TerminalListItem {
|
||||
return {
|
||||
id: input.session.id,
|
||||
name: input.session.name,
|
||||
cwd: input.session.cwd,
|
||||
};
|
||||
}
|
||||
|
||||
function emitTerminalsChanged(input: { cwd: string }): void {
|
||||
if (terminalsChangedListeners.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const terminals = (terminalsByCwd.get(input.cwd) ?? []).map((session) =>
|
||||
toTerminalListItem({ session })
|
||||
);
|
||||
const event: TerminalsChangedEvent = {
|
||||
cwd: input.cwd,
|
||||
terminals,
|
||||
};
|
||||
|
||||
for (const listener of terminalsChangedListeners) {
|
||||
try {
|
||||
listener(event);
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
async getTerminals(cwd: string): Promise<TerminalSession[]> {
|
||||
assertAbsolutePath(cwd);
|
||||
@@ -70,6 +117,7 @@ export function createTerminalManager(): TerminalManager {
|
||||
);
|
||||
terminals = [session];
|
||||
terminalsByCwd.set(cwd, terminals);
|
||||
emitTerminalsChanged({ cwd });
|
||||
}
|
||||
return terminals;
|
||||
},
|
||||
@@ -88,6 +136,7 @@ export function createTerminalManager(): TerminalManager {
|
||||
|
||||
terminals.push(session);
|
||||
terminalsByCwd.set(options.cwd, terminals);
|
||||
emitTerminalsChanged({ cwd: options.cwd });
|
||||
|
||||
return session;
|
||||
},
|
||||
@@ -109,5 +158,12 @@ export function createTerminalManager(): TerminalManager {
|
||||
removeSessionById(id, { kill: true });
|
||||
}
|
||||
},
|
||||
|
||||
subscribeTerminalsChanged(listener: TerminalsChangedListener): () => void {
|
||||
terminalsChangedListeners.add(listener);
|
||||
return () => {
|
||||
terminalsChangedListeners.delete(listener);
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user