Key workspace status and ownership by ID, not directory
Multiple workspaces can share one checkout directory. Agent and terminal status, sidebar tabs, and ownership now attribute strictly by workspace ID, so a running or blocked agent in one workspace no longer shows up on its same-directory siblings. A one-time startup migration backfills workspace IDs onto legacy agents, and every agent and terminal is created with one thereafter. Directory lookups remain only for genuinely folder-derived git facts (branch, diff) and the archive-by-path adapter.
This commit is contained in:
@@ -64,7 +64,7 @@ it("returns empty list for new cwd", async () => {
|
||||
it("returns existing terminals on subsequent calls", async () => {
|
||||
manager = createTerminalManager();
|
||||
const cwd = realpathSync(tmpdir());
|
||||
const created = await manager.createTerminal({ cwd });
|
||||
const created = await manager.createTerminal({ cwd, workspaceId: "ws-test" });
|
||||
const first = await manager.getTerminals(cwd);
|
||||
const second = await manager.getTerminals(cwd);
|
||||
|
||||
@@ -89,8 +89,8 @@ it("creates separate terminals for different cwds", async () => {
|
||||
const firstCwd = mkdtempSync(join(tmpdir(), "terminal-manager-first-"));
|
||||
const secondCwd = mkdtempSync(join(tmpdir(), "terminal-manager-second-"));
|
||||
temporaryDirs.push(firstCwd, secondCwd);
|
||||
const tmpTerminals = [await manager.createTerminal({ cwd: firstCwd })];
|
||||
const homeTerminals = [await manager.createTerminal({ cwd: secondCwd })];
|
||||
const tmpTerminals = [await manager.createTerminal({ cwd: firstCwd, workspaceId: "ws-test" })];
|
||||
const homeTerminals = [await manager.createTerminal({ cwd: secondCwd, workspaceId: "ws-test" })];
|
||||
|
||||
expect(tmpTerminals.length).toBe(1);
|
||||
expect(homeTerminals.length).toBe(1);
|
||||
@@ -104,7 +104,11 @@ it("lists subdirectory terminals when querying the workspace root", async () =>
|
||||
mkdirSync(subdirCwd, { recursive: true });
|
||||
temporaryDirs.push(rootCwd);
|
||||
|
||||
const created = await manager.createTerminal({ cwd: subdirCwd, name: "Mobile" });
|
||||
const created = await manager.createTerminal({
|
||||
cwd: subdirCwd,
|
||||
name: "Mobile",
|
||||
workspaceId: "ws-test",
|
||||
});
|
||||
|
||||
const rootTerminals = await manager.getTerminals(rootCwd);
|
||||
expect(rootTerminals.map((terminal) => terminal.id)).toEqual([created.id]);
|
||||
@@ -113,7 +117,7 @@ it("lists subdirectory terminals when querying the workspace root", async () =>
|
||||
it("includes only stamped terminals in workspace-scoped queries", async () => {
|
||||
manager = createTerminalManager();
|
||||
const cwd = realpathSync(tmpdir());
|
||||
const legacy = await manager.createTerminal({ cwd });
|
||||
const legacy = await manager.createTerminal({ cwd, workspaceId: "ws-test" });
|
||||
const owned = await manager.createTerminal({ cwd, workspaceId: "ws-owned" });
|
||||
const sibling = await manager.createTerminal({ cwd, workspaceId: "ws-sibling" });
|
||||
|
||||
@@ -127,8 +131,8 @@ it("includes only stamped terminals in workspace-scoped queries", async () => {
|
||||
it("creates additional terminal with auto-incrementing name", async () => {
|
||||
manager = createTerminalManager();
|
||||
const cwd = realpathSync(tmpdir());
|
||||
await manager.createTerminal({ cwd });
|
||||
const second = await manager.createTerminal({ cwd });
|
||||
await manager.createTerminal({ cwd, workspaceId: "ws-test" });
|
||||
const second = await manager.createTerminal({ cwd, workspaceId: "ws-test" });
|
||||
|
||||
expect(second.name).toBe("Terminal 2");
|
||||
|
||||
@@ -138,7 +142,11 @@ it("creates additional terminal with auto-incrementing name", async () => {
|
||||
|
||||
it("uses custom name when provided", async () => {
|
||||
manager = createTerminalManager();
|
||||
const session = await manager.createTerminal({ cwd: realpathSync(tmpdir()), name: "Dev Server" });
|
||||
const session = await manager.createTerminal({
|
||||
cwd: realpathSync(tmpdir()),
|
||||
name: "Dev Server",
|
||||
workspaceId: "ws-test",
|
||||
});
|
||||
|
||||
expect(session.name).toBe("Dev Server");
|
||||
});
|
||||
@@ -146,7 +154,7 @@ it("uses custom name when provided", async () => {
|
||||
it("creates first terminal if none exist", async () => {
|
||||
manager = createTerminalManager();
|
||||
const cwd = realpathSync(tmpdir());
|
||||
const session = await manager.createTerminal({ cwd });
|
||||
const session = await manager.createTerminal({ cwd, workspaceId: "ws-test" });
|
||||
|
||||
expect(session.name).toBe("Terminal 1");
|
||||
|
||||
@@ -157,7 +165,9 @@ it("creates first terminal if none exist", async () => {
|
||||
|
||||
it("throws for relative paths", async () => {
|
||||
manager = createTerminalManager();
|
||||
await expect(manager.createTerminal({ cwd: "tmp" })).rejects.toThrow("cwd must be absolute path");
|
||||
await expect(manager.createTerminal({ cwd: "tmp", workspaceId: "ws-test" })).rejects.toThrow(
|
||||
"cwd must be absolute path",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not reject Windows absolute paths as relative", async () => {
|
||||
@@ -166,7 +176,7 @@ it("does not reject Windows absolute paths as relative", async () => {
|
||||
temporaryDirs.push(cwd);
|
||||
|
||||
try {
|
||||
await manager.createTerminal({ cwd });
|
||||
await manager.createTerminal({ cwd, workspaceId: "ws-test" });
|
||||
} catch (error) {
|
||||
expect((error as Error).message).not.toBe("cwd must be absolute path");
|
||||
}
|
||||
@@ -183,6 +193,7 @@ it("inherits registered env for the worktree root cwd", async () => {
|
||||
env: { PASEO_WORKTREE_PORT: "45678" },
|
||||
});
|
||||
await manager.createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd,
|
||||
command: process.execPath,
|
||||
args: [
|
||||
@@ -208,6 +219,7 @@ it("inherits registered env for subdirectories within the worktree", async () =>
|
||||
env: { PASEO_WORKTREE_PORT: "45679" },
|
||||
});
|
||||
await manager.createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: subdirCwd,
|
||||
command: process.execPath,
|
||||
args: [
|
||||
@@ -222,7 +234,10 @@ it("inherits registered env for subdirectories within the worktree", async () =>
|
||||
|
||||
it("returns terminal by id", async () => {
|
||||
manager = createTerminalManager();
|
||||
const session = await manager.createTerminal({ cwd: realpathSync(tmpdir()) });
|
||||
const session = await manager.createTerminal({
|
||||
cwd: realpathSync(tmpdir()),
|
||||
workspaceId: "ws-test",
|
||||
});
|
||||
const found = manager.getTerminal(session.id);
|
||||
|
||||
expect(found).toBe(session);
|
||||
@@ -237,7 +252,10 @@ it("returns undefined for unknown id", () => {
|
||||
|
||||
it("removes terminal from manager", async () => {
|
||||
manager = createTerminalManager();
|
||||
const session = await manager.createTerminal({ cwd: realpathSync(tmpdir()) });
|
||||
const session = await manager.createTerminal({
|
||||
cwd: realpathSync(tmpdir()),
|
||||
workspaceId: "ws-test",
|
||||
});
|
||||
const id = session.id;
|
||||
|
||||
manager.killTerminal(id);
|
||||
@@ -248,7 +266,7 @@ it("removes terminal from manager", async () => {
|
||||
it("removes cwd entry when last terminal is killed", async () => {
|
||||
manager = createTerminalManager();
|
||||
const cwd = realpathSync(tmpdir());
|
||||
const created = await manager.createTerminal({ cwd });
|
||||
const created = await manager.createTerminal({ cwd, workspaceId: "ws-test" });
|
||||
manager.killTerminal(created.id);
|
||||
|
||||
const remaining = await manager.getTerminals(cwd);
|
||||
@@ -259,8 +277,8 @@ it("removes cwd entry when last terminal is killed", async () => {
|
||||
it("keeps cwd entry when other terminals remain", async () => {
|
||||
manager = createTerminalManager();
|
||||
const cwd = realpathSync(tmpdir());
|
||||
await manager.createTerminal({ cwd });
|
||||
const second = await manager.createTerminal({ cwd });
|
||||
await manager.createTerminal({ cwd, workspaceId: "ws-test" });
|
||||
const second = await manager.createTerminal({ cwd, workspaceId: "ws-test" });
|
||||
|
||||
const terminals = await manager.getTerminals(cwd);
|
||||
manager.killTerminal(terminals[0].id);
|
||||
@@ -279,7 +297,7 @@ it("is no-op for unknown id", () => {
|
||||
it("auto-removes terminal when shell exits", async () => {
|
||||
manager = createTerminalManager();
|
||||
const cwd = realpathSync(tmpdir());
|
||||
const session = await manager.createTerminal({ cwd });
|
||||
const session = await manager.createTerminal({ cwd, workspaceId: "ws-test" });
|
||||
const exitedId = session.id;
|
||||
session.kill();
|
||||
|
||||
@@ -301,8 +319,8 @@ it("returns all cwds with active terminals", async () => {
|
||||
const firstCwd = mkdtempSync(join(tmpdir(), "terminal-manager-list-first-"));
|
||||
const secondCwd = mkdtempSync(join(tmpdir(), "terminal-manager-list-second-"));
|
||||
temporaryDirs.push(firstCwd, secondCwd);
|
||||
await manager.createTerminal({ cwd: firstCwd });
|
||||
await manager.createTerminal({ cwd: secondCwd });
|
||||
await manager.createTerminal({ cwd: firstCwd, workspaceId: "ws-test" });
|
||||
await manager.createTerminal({ cwd: secondCwd, workspaceId: "ws-test" });
|
||||
|
||||
const dirs = manager.listDirectories();
|
||||
expect(dirs).toContain(firstCwd);
|
||||
@@ -315,8 +333,8 @@ it("kills all terminals and clears state", async () => {
|
||||
const firstCwd = mkdtempSync(join(tmpdir(), "terminal-manager-kill-first-"));
|
||||
const secondCwd = mkdtempSync(join(tmpdir(), "terminal-manager-kill-second-"));
|
||||
temporaryDirs.push(firstCwd, secondCwd);
|
||||
const tmpSession = await manager.createTerminal({ cwd: firstCwd });
|
||||
const homeSession = await manager.createTerminal({ cwd: secondCwd });
|
||||
const tmpSession = await manager.createTerminal({ cwd: firstCwd, workspaceId: "ws-test" });
|
||||
const homeSession = await manager.createTerminal({ cwd: secondCwd, workspaceId: "ws-test" });
|
||||
const tmpId = tmpSession.id;
|
||||
const homeId = homeSession.id;
|
||||
|
||||
@@ -340,8 +358,8 @@ it("emits cwd snapshots when terminals are created", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
await manager.createTerminal({ cwd });
|
||||
await manager.createTerminal({ cwd, name: "Dev Server" });
|
||||
await manager.createTerminal({ cwd, workspaceId: "ws-test" });
|
||||
await manager.createTerminal({ cwd, name: "Dev Server", workspaceId: "ws-test" });
|
||||
|
||||
expect(snapshots).toContainEqual({
|
||||
cwd,
|
||||
@@ -381,6 +399,7 @@ it("emits updated terminal titles after debounced title changes", async () => {
|
||||
});
|
||||
|
||||
const session = await manager.createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: realpathSync(tmpdir()),
|
||||
command: process.execPath,
|
||||
args: ["-e", "process.stdout.write('\\x1b]0;Logs\\x07'); setTimeout(() => {}, 10000);"],
|
||||
@@ -402,7 +421,7 @@ it("emits empty snapshot when last terminal is removed", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
const session = await manager.createTerminal({ cwd });
|
||||
const session = await manager.createTerminal({ cwd, workspaceId: "ws-test" });
|
||||
manager.killTerminal(session.id);
|
||||
|
||||
expect(snapshots).toContainEqual({
|
||||
@@ -416,6 +435,7 @@ it("emits empty snapshot when last terminal is removed", async () => {
|
||||
it("setTerminalTitle returns false for unknown terminal ids without changing existing terminals", async () => {
|
||||
manager = createTerminalManager();
|
||||
const session = await manager.createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: realpathSync(tmpdir()),
|
||||
title: "Existing title",
|
||||
});
|
||||
@@ -439,7 +459,10 @@ it("setTerminalTitle returns false for unknown terminal ids without changing exi
|
||||
|
||||
it("setTerminalTitle returns true and updates the terminal title for existing terminals", async () => {
|
||||
manager = createTerminalManager();
|
||||
const session = await manager.createTerminal({ cwd: realpathSync(tmpdir()) });
|
||||
const session = await manager.createTerminal({
|
||||
cwd: realpathSync(tmpdir()),
|
||||
workspaceId: "ws-test",
|
||||
});
|
||||
|
||||
expect(manager.setTerminalTitle(session.id, "x")).toBe(true);
|
||||
expect(session.getTitle()).toBe("x");
|
||||
@@ -447,7 +470,10 @@ it("setTerminalTitle returns true and updates the terminal title for existing te
|
||||
|
||||
it("new terminal starts with unknown activity", async () => {
|
||||
manager = createTerminalManager();
|
||||
const session = await manager.createTerminal({ cwd: realpathSync(tmpdir()) });
|
||||
const session = await manager.createTerminal({
|
||||
cwd: realpathSync(tmpdir()),
|
||||
workspaceId: "ws-test",
|
||||
});
|
||||
|
||||
expect(session.getActivity()).toBeNull();
|
||||
});
|
||||
@@ -465,7 +491,7 @@ it("includes nullable activity in terminals-changed snapshot items", async () =>
|
||||
);
|
||||
});
|
||||
|
||||
const session = await manager.createTerminal({ cwd });
|
||||
const session = await manager.createTerminal({ cwd, workspaceId: "ws-test" });
|
||||
|
||||
expect(snapshots.length).toBeGreaterThan(0);
|
||||
const lastSnapshot = snapshots[snapshots.length - 1];
|
||||
@@ -491,7 +517,7 @@ it("setTerminalActivity returns true and emits terminals-changed", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
const session = await manager.createTerminal({ cwd });
|
||||
const session = await manager.createTerminal({ cwd, workspaceId: "ws-test" });
|
||||
await expect(manager.setTerminalActivity(session.id, "working")).resolves.toBe(true);
|
||||
|
||||
const hasWorkingActivity = (entries: ActivityEntry[]) =>
|
||||
@@ -519,7 +545,7 @@ it("clearTerminalAttention returns attention terminals to idle", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
const session = await manager.createTerminal({ cwd });
|
||||
const session = await manager.createTerminal({ cwd, workspaceId: "ws-test" });
|
||||
await manager.setTerminalActivity(session.id, "attention");
|
||||
|
||||
await expect(manager.clearTerminalAttention(session.id)).resolves.toBe(true);
|
||||
@@ -536,7 +562,10 @@ it("clearTerminalAttention returns attention terminals to idle", async () => {
|
||||
|
||||
it("clearTerminalAttention leaves non-attention terminals unchanged", async () => {
|
||||
manager = createTerminalManager();
|
||||
const session = await manager.createTerminal({ cwd: realpathSync(tmpdir()) });
|
||||
const session = await manager.createTerminal({
|
||||
cwd: realpathSync(tmpdir()),
|
||||
workspaceId: "ws-test",
|
||||
});
|
||||
await manager.setTerminalActivity(session.id, "working");
|
||||
|
||||
await expect(manager.clearTerminalAttention(session.id)).resolves.toBe(false);
|
||||
@@ -574,7 +603,7 @@ it("does not emit workspace contribution event for title-only changes", async ()
|
||||
events.push(event);
|
||||
});
|
||||
|
||||
const session = await manager.createTerminal({ cwd });
|
||||
const session = await manager.createTerminal({ cwd, workspaceId: "ws-test" });
|
||||
manager.setTerminalTitle(session.id, "New title");
|
||||
|
||||
expect(events).toEqual([]);
|
||||
@@ -614,7 +643,7 @@ it("does not emit workspace contribution event when an idle terminal is removed"
|
||||
events.push(event);
|
||||
});
|
||||
|
||||
const session = await manager.createTerminal({ cwd });
|
||||
const session = await manager.createTerminal({ cwd, workspaceId: "ws-test" });
|
||||
manager.killTerminal(session.id);
|
||||
|
||||
expect(events).toEqual([]);
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface TerminalListItem {
|
||||
id: string;
|
||||
name: string;
|
||||
cwd: string;
|
||||
workspaceId?: string;
|
||||
workspaceId: string;
|
||||
title?: string;
|
||||
activity: TerminalActivity | null;
|
||||
}
|
||||
@@ -32,6 +32,7 @@ export interface TerminalActivityTransitionEvent {
|
||||
terminalId: string;
|
||||
name: string;
|
||||
cwd: string;
|
||||
workspaceId: string;
|
||||
activity: TerminalActivity | null;
|
||||
previous: TerminalActivity | null;
|
||||
}
|
||||
@@ -41,7 +42,7 @@ export type TerminalActivityListener = (event: TerminalActivityTransitionEvent)
|
||||
export interface TerminalWorkspaceContributionChangedEvent {
|
||||
terminalId: string;
|
||||
cwd: string;
|
||||
workspaceId?: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
export type TerminalWorkspaceContributionChangedListener = (
|
||||
@@ -53,7 +54,7 @@ export interface TerminalManager {
|
||||
createTerminal(options: {
|
||||
id?: string;
|
||||
cwd: string;
|
||||
workspaceId?: string;
|
||||
workspaceId: string;
|
||||
name?: string;
|
||||
title?: string;
|
||||
env?: Record<string, string>;
|
||||
@@ -159,7 +160,7 @@ export function createTerminalManager(
|
||||
emitTerminalWorkspaceContributionChanged({
|
||||
terminalId: session.id,
|
||||
cwd: session.cwd,
|
||||
...(session.workspaceId ? { workspaceId: session.workspaceId } : {}),
|
||||
workspaceId: session.workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -200,7 +201,7 @@ export function createTerminalManager(
|
||||
emitTerminalWorkspaceContributionChanged({
|
||||
terminalId: session.id,
|
||||
cwd: session.cwd,
|
||||
...(session.workspaceId ? { workspaceId: session.workspaceId } : {}),
|
||||
workspaceId: session.workspaceId,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -215,7 +216,7 @@ export function createTerminalManager(
|
||||
id: input.session.id,
|
||||
name: input.session.name,
|
||||
cwd: input.session.cwd,
|
||||
...(input.session.workspaceId ? { workspaceId: input.session.workspaceId } : {}),
|
||||
workspaceId: input.session.workspaceId,
|
||||
title: input.session.getTitle(),
|
||||
activity: input.session.getActivity(),
|
||||
};
|
||||
@@ -254,6 +255,7 @@ export function createTerminalManager(
|
||||
terminalId: input.session.id,
|
||||
name: input.session.name,
|
||||
cwd: input.session.cwd,
|
||||
workspaceId: input.session.workspaceId,
|
||||
activity: input.transition.activity,
|
||||
previous: input.transition.previous,
|
||||
};
|
||||
@@ -307,7 +309,7 @@ export function createTerminalManager(
|
||||
async createTerminal(options: {
|
||||
id?: string;
|
||||
cwd: string;
|
||||
workspaceId?: string;
|
||||
workspaceId: string;
|
||||
name?: string;
|
||||
title?: string;
|
||||
env?: Record<string, string>;
|
||||
@@ -341,7 +343,7 @@ export function createTerminalManager(
|
||||
await createTerminal({
|
||||
id: terminalId,
|
||||
cwd: options.cwd,
|
||||
...(options.workspaceId ? { workspaceId: options.workspaceId } : {}),
|
||||
workspaceId: options.workspaceId,
|
||||
name: options.name ?? defaultName,
|
||||
...(options.title ? { title: options.title } : {}),
|
||||
...(options.command ? { command: options.command } : {}),
|
||||
|
||||
@@ -60,6 +60,7 @@ describe("terminal-session-controller restore", () => {
|
||||
id: "term-1",
|
||||
name: "Terminal",
|
||||
cwd: "/tmp",
|
||||
workspaceId: "ws-test",
|
||||
send: vi.fn(),
|
||||
subscribe: (listener) => {
|
||||
terminalListener = listener;
|
||||
@@ -154,6 +155,7 @@ function listSession(input: { id: string; name: string; cwd: string }): Terminal
|
||||
id: input.id,
|
||||
name: input.name,
|
||||
cwd: input.cwd,
|
||||
workspaceId: "ws-test",
|
||||
send: vi.fn(),
|
||||
subscribe: () => vi.fn(),
|
||||
onExit: () => vi.fn(),
|
||||
@@ -193,6 +195,7 @@ describe("terminal-session-controller wrap-flag gating", () => {
|
||||
id: "term-1",
|
||||
name: "Terminal",
|
||||
cwd: "/tmp",
|
||||
workspaceId: "ws-test",
|
||||
send: vi.fn(),
|
||||
subscribe: (listener) => {
|
||||
queueMicrotask(() => listener({ type: "snapshotReady", revision: 1 }));
|
||||
@@ -329,7 +332,7 @@ describe("terminal-session-controller subdirectory aggregation", () => {
|
||||
|
||||
changedListener?.({
|
||||
cwd: subdirCwd,
|
||||
terminals: [{ id: "subdir-term", name: "Mobile", cwd: subdirCwd }],
|
||||
terminals: [{ id: "subdir-term", name: "Mobile", cwd: subdirCwd, workspaceId: "ws-test" }],
|
||||
});
|
||||
await flushMicrotasks();
|
||||
|
||||
@@ -339,8 +342,8 @@ describe("terminal-session-controller subdirectory aggregation", () => {
|
||||
payload: {
|
||||
cwd: rootCwd,
|
||||
terminals: [
|
||||
{ id: "root-term", name: "Terminal 1", activity: null },
|
||||
{ id: "subdir-term", name: "Mobile", activity: null },
|
||||
{ id: "root-term", name: "Terminal 1", workspaceId: "ws-test", activity: null },
|
||||
{ id: "subdir-term", name: "Mobile", workspaceId: "ws-test", activity: null },
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -403,7 +406,9 @@ describe("terminal-session-controller subdirectory aggregation", () => {
|
||||
type: "list_terminals_response",
|
||||
payload: {
|
||||
cwd: rootCwd,
|
||||
terminals: [{ id: "root-term", name: "Terminal 1", activity: null }],
|
||||
terminals: [
|
||||
{ id: "root-term", name: "Terminal 1", workspaceId: "ws-test", activity: null },
|
||||
],
|
||||
requestId: "req-root",
|
||||
},
|
||||
},
|
||||
@@ -411,7 +416,9 @@ describe("terminal-session-controller subdirectory aggregation", () => {
|
||||
type: "list_terminals_response",
|
||||
payload: {
|
||||
cwd: worktreeCwd,
|
||||
terminals: [{ id: "worktree-term", name: "Feature", activity: null }],
|
||||
terminals: [
|
||||
{ id: "worktree-term", name: "Feature", workspaceId: "ws-test", activity: null },
|
||||
],
|
||||
requestId: "req-worktree",
|
||||
},
|
||||
},
|
||||
@@ -500,6 +507,7 @@ describe("terminal-session-controller backpressure snapshot fallback", () => {
|
||||
id: "term-1",
|
||||
name: "Terminal",
|
||||
cwd: "/tmp",
|
||||
workspaceId: "ws-test",
|
||||
send: vi.fn(),
|
||||
subscribe: (listener) => {
|
||||
terminalListener = listener;
|
||||
|
||||
@@ -298,7 +298,7 @@ export class TerminalSessionController {
|
||||
terminals: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
workspaceId?: string;
|
||||
workspaceId: string;
|
||||
title?: string;
|
||||
activity: TerminalActivity | null;
|
||||
}>;
|
||||
@@ -317,7 +317,7 @@ export class TerminalSessionController {
|
||||
): {
|
||||
id: string;
|
||||
name: string;
|
||||
workspaceId?: string;
|
||||
workspaceId: string;
|
||||
title?: string;
|
||||
activity: TerminalActivity | null;
|
||||
} {
|
||||
@@ -326,7 +326,7 @@ export class TerminalSessionController {
|
||||
return {
|
||||
id: terminal.id,
|
||||
name: terminal.name,
|
||||
...(terminal.workspaceId ? { workspaceId: terminal.workspaceId } : {}),
|
||||
workspaceId: terminal.workspaceId,
|
||||
...(title ? { title } : {}),
|
||||
activity,
|
||||
};
|
||||
@@ -520,9 +520,21 @@ export class TerminalSessionController {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!msg.workspaceId) {
|
||||
this.emit({
|
||||
type: "create_terminal_response",
|
||||
payload: {
|
||||
terminal: null,
|
||||
error: "workspaceId is required",
|
||||
requestId: msg.requestId,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const session = await this.terminalManager.createTerminal({
|
||||
cwd: msg.cwd,
|
||||
...(msg.workspaceId ? { workspaceId: msg.workspaceId } : {}),
|
||||
workspaceId: msg.workspaceId,
|
||||
name: msg.name,
|
||||
command: msg.command,
|
||||
args: msg.args,
|
||||
@@ -535,7 +547,7 @@ export class TerminalSessionController {
|
||||
id: session.id,
|
||||
name: session.name,
|
||||
cwd: session.cwd,
|
||||
...(session.workspaceId ? { workspaceId: session.workspaceId } : {}),
|
||||
workspaceId: session.workspaceId,
|
||||
...(session.getTitle() ? { title: session.getTitle() } : {}),
|
||||
activity: session.getActivity(),
|
||||
},
|
||||
|
||||
@@ -66,7 +66,7 @@ function toTerminalInfo(session: TerminalSession): WorkerTerminalInfo {
|
||||
id: session.id,
|
||||
name: session.name,
|
||||
cwd: session.cwd,
|
||||
...(session.workspaceId ? { workspaceId: session.workspaceId } : {}),
|
||||
workspaceId: session.workspaceId,
|
||||
...(session.getTitle() ? { title: session.getTitle() } : {}),
|
||||
activity: session.getActivity(),
|
||||
};
|
||||
@@ -201,7 +201,11 @@ async function handleCreateTerminalRequest(message: TerminalCreateRequest): Prom
|
||||
};
|
||||
inFlightTerminalCreateRequest = request;
|
||||
try {
|
||||
const session = await manager.createTerminal(message.options);
|
||||
const { workspaceId } = message.options;
|
||||
if (!workspaceId) {
|
||||
throw new Error("workspaceId is required");
|
||||
}
|
||||
const session = await manager.createTerminal({ ...message.options, workspaceId });
|
||||
if (request.errorReported) {
|
||||
session.kill();
|
||||
return;
|
||||
|
||||
@@ -302,6 +302,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
it("executes a simple echo command", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -328,6 +329,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
it("executes multiple commands sequentially", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -353,6 +355,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
it("captures output from pwd in specified cwd", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -382,6 +385,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: homeDir,
|
||||
command: "/bin/zsh",
|
||||
args: ["-c", 'printf \'%s\\n%s\\n\' "${ZDOTDIR-}" "${PASEO_TEST_REAL_ZDOTDIR-}"'],
|
||||
@@ -409,6 +413,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: packageRoot,
|
||||
command: process.execPath,
|
||||
args: [scriptPath, "run", "dev"],
|
||||
@@ -432,6 +437,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
it("emits OSC title updates to title listeners", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -457,6 +463,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
it("keeps preset titles instead of applying OSC title updates", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -481,6 +488,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: packageRoot,
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -514,6 +522,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: packageRoot,
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -538,6 +547,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
it("debounces rapid title changes and emits only the final title", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -581,6 +591,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: workingDir,
|
||||
shell: "/bin/zsh",
|
||||
env: {
|
||||
@@ -663,6 +674,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: workingDir,
|
||||
shell: "/bin/zsh",
|
||||
env: {
|
||||
@@ -692,6 +704,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
it("captures ANSI 16 color codes (mode 1)", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ ", TERM: "xterm-256color" },
|
||||
@@ -721,6 +734,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
it("captures 256 color codes (mode 2)", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ ", TERM: "xterm-256color" },
|
||||
@@ -746,6 +760,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
it("captures true color RGB (mode 3)", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ ", TERM: "xterm-256color" },
|
||||
@@ -774,6 +789,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
it("captures background colors", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ ", TERM: "xterm-256color" },
|
||||
@@ -800,6 +816,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
it("receives a snapshot on initial subscription", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: realpathSync(tmpdir()),
|
||||
}),
|
||||
);
|
||||
@@ -820,6 +837,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
it("receives output messages on updates without replay snapshots", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -849,6 +867,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
it("does not emit snapshot messages for resize-only updates", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -877,6 +896,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
it("emits output only after getState reflects the new data", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -902,6 +922,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
it("unsubscribe stops receiving messages", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -931,6 +952,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -950,6 +972,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -967,6 +990,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -986,6 +1010,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -1005,6 +1030,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -1024,6 +1050,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -1043,6 +1070,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
it("streams raw output messages without replay metadata", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -1070,6 +1098,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
it("sends the current snapshot to a new subscriber instead of replaying raw output", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -1112,6 +1141,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
it("returns current terminal state with grid", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -1135,6 +1165,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
it("captures cursor presentation modes emitted by terminal apps", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -1164,6 +1195,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
it("grid cells have char and color attributes", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -1184,6 +1216,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
it("preserves scrollback buffer", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -1222,6 +1255,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
it("terminates the shell process", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -1240,6 +1274,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
||||
it("send after kill is a no-op", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
|
||||
@@ -251,6 +251,7 @@ describe("createTerminal", () => {
|
||||
it("creates a terminal session with an id, name, and cwd", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: realpathSync(tmpdir()),
|
||||
}),
|
||||
);
|
||||
@@ -268,6 +269,7 @@ describe("createTerminal", () => {
|
||||
: "/bin/sh";
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: realpathSync(tmpdir()),
|
||||
shell,
|
||||
env: { PS1: "$ " },
|
||||
@@ -281,6 +283,7 @@ describe("createTerminal", () => {
|
||||
it("uses default shell if not specified", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: realpathSync(tmpdir()),
|
||||
}),
|
||||
);
|
||||
@@ -294,6 +297,7 @@ describe("createTerminal", () => {
|
||||
: "/bin/sh";
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: realpathSync(tmpdir()),
|
||||
shell,
|
||||
env: { PS1: "$ " },
|
||||
@@ -311,6 +315,7 @@ describe("createTerminal", () => {
|
||||
: "/bin/sh";
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: realpathSync(tmpdir()),
|
||||
shell,
|
||||
env: { PS1: "$ " },
|
||||
@@ -327,6 +332,7 @@ describe("createTerminal", () => {
|
||||
it("reports per-row soft-wrap flags only when wrap flags are requested", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: realpathSync(tmpdir()),
|
||||
cols: 40,
|
||||
rows: 10,
|
||||
@@ -358,6 +364,7 @@ describe("createTerminal", () => {
|
||||
it("captures exit diagnostics from the terminal buffer", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: realpathSync(tmpdir()),
|
||||
command: process.execPath,
|
||||
args: [
|
||||
@@ -389,6 +396,7 @@ describe("createTerminal", () => {
|
||||
// summary races the async xterm parse and reads a stale buffer.
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: realpathSync(tmpdir()),
|
||||
command: process.execPath,
|
||||
args: [
|
||||
@@ -429,6 +437,7 @@ describe.skipIf(isPlatform("win32"))("send input", () => {
|
||||
it("executes a simple echo command", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -455,6 +464,7 @@ describe.skipIf(isPlatform("win32"))("send input", () => {
|
||||
it("captures output from pwd in specified cwd", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -484,6 +494,7 @@ describe.skipIf(isPlatform("win32"))("terminal title", () => {
|
||||
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: homeDir,
|
||||
command: "/bin/zsh",
|
||||
args: ["-c", 'printf \'%s\\n%s\\n\' "${ZDOTDIR-}" "${PASEO_TEST_REAL_ZDOTDIR-}"'],
|
||||
@@ -511,6 +522,7 @@ describe.skipIf(isPlatform("win32"))("terminal title", () => {
|
||||
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: packageRoot,
|
||||
command: process.execPath,
|
||||
args: [scriptPath, "run", "dev"],
|
||||
@@ -534,6 +546,7 @@ describe.skipIf(isPlatform("win32"))("terminal title", () => {
|
||||
it("emits OSC title updates to title listeners", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -559,6 +572,7 @@ describe.skipIf(isPlatform("win32"))("terminal title", () => {
|
||||
it("keeps preset titles instead of applying OSC title updates", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -583,6 +597,7 @@ describe.skipIf(isPlatform("win32"))("terminal title", () => {
|
||||
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: packageRoot,
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -616,6 +631,7 @@ describe.skipIf(isPlatform("win32"))("terminal title", () => {
|
||||
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: packageRoot,
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -640,6 +656,7 @@ describe.skipIf(isPlatform("win32"))("terminal title", () => {
|
||||
it("debounces rapid title changes and emits only the final title", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -683,6 +700,7 @@ describe.skipIf(isPlatform("win32"))("terminal title", () => {
|
||||
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: workingDir,
|
||||
shell: "/bin/zsh",
|
||||
env: {
|
||||
@@ -765,6 +783,7 @@ describe.skipIf(isPlatform("win32"))("terminal title", () => {
|
||||
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: workingDir,
|
||||
shell: "/bin/zsh",
|
||||
env: {
|
||||
@@ -792,6 +811,7 @@ describe.skipIf(isPlatform("win32"))("terminal title", () => {
|
||||
it("clears already scheduled OSC title debounce timers when setting a user title", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -825,6 +845,7 @@ describe.skipIf(isPlatform("win32"))("terminal title", () => {
|
||||
it("ignores later OSC title updates after setting a user title", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -854,6 +875,7 @@ describe.skipIf(isPlatform("win32"))("terminal title", () => {
|
||||
it("trims user-set titles and treats empty titles as no-ops", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
@@ -884,6 +906,7 @@ describe.skipIf(isPlatform("win32"))("colors", () => {
|
||||
it("captures ANSI 16 color codes (mode 1)", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ ", TERM: "xterm-256color" },
|
||||
@@ -913,6 +936,7 @@ describe.skipIf(isPlatform("win32"))("colors", () => {
|
||||
it("captures true color RGB (mode 3)", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ ", TERM: "xterm-256color" },
|
||||
@@ -941,6 +965,7 @@ describe.skipIf(isPlatform("win32"))("colors", () => {
|
||||
it("captures background colors", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ ", TERM: "xterm-256color" },
|
||||
@@ -967,6 +992,7 @@ describe("resize", () => {
|
||||
it("updates terminal dimensions on resize", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: realpathSync(tmpdir()),
|
||||
rows: 24,
|
||||
cols: 80,
|
||||
@@ -985,6 +1011,7 @@ describe("resize", () => {
|
||||
it("grid reflects new dimensions after resize", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: realpathSync(tmpdir()),
|
||||
rows: 24,
|
||||
cols: 80,
|
||||
@@ -1002,6 +1029,7 @@ describe("resize", () => {
|
||||
it("exposes the current size without extracting full state", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: realpathSync(tmpdir()),
|
||||
rows: 24,
|
||||
cols: 80,
|
||||
@@ -1021,6 +1049,7 @@ describe("mouse events", () => {
|
||||
it("accepts mouse events without throwing", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: realpathSync(tmpdir()),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -77,7 +77,7 @@ export interface TerminalSession {
|
||||
id: string;
|
||||
name: string;
|
||||
cwd: string;
|
||||
workspaceId?: string;
|
||||
workspaceId: string;
|
||||
send(msg: ClientMessage): void;
|
||||
subscribe(listener: (msg: ServerMessage) => void, options?: TerminalSubscribeOptions): () => void;
|
||||
onExit(listener: (info: TerminalExitInfo) => void): () => void;
|
||||
@@ -118,7 +118,7 @@ function parseCommandFinishedOsc(data: string): TerminalCommandFinishedInfo | nu
|
||||
export interface CreateTerminalOptions {
|
||||
id?: string;
|
||||
cwd: string;
|
||||
workspaceId?: string;
|
||||
workspaceId: string;
|
||||
shell?: string;
|
||||
env?: Record<string, string>;
|
||||
activityEnv?: Record<string, string>;
|
||||
@@ -865,7 +865,7 @@ export async function createTerminal(options: CreateTerminalOptions): Promise<Te
|
||||
env: {
|
||||
...env,
|
||||
...activityEnv,
|
||||
...(workspaceId ? { PASEO_WORKSPACE_ID: workspaceId } : {}),
|
||||
PASEO_WORKSPACE_ID: workspaceId,
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -1448,7 +1448,7 @@ export async function createTerminal(options: CreateTerminalOptions): Promise<Te
|
||||
id,
|
||||
name,
|
||||
cwd,
|
||||
...(workspaceId ? { workspaceId } : {}),
|
||||
workspaceId,
|
||||
send,
|
||||
subscribe,
|
||||
onExit,
|
||||
|
||||
@@ -166,6 +166,7 @@ it("creates a terminal through the worker and streams output", async () => {
|
||||
manager = createWorkerTerminalManager();
|
||||
const session = trackTerminal(
|
||||
await manager.createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd,
|
||||
...nodeTerminalCommand(`
|
||||
process.stdin.on("data", (chunk) => {
|
||||
@@ -212,6 +213,7 @@ it("delivers rapid small writes complete and in order through worker coalescing"
|
||||
// on platform-specific PTY input echo/canonical-mode behavior.
|
||||
const session = trackTerminal(
|
||||
await manager.createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd,
|
||||
env: { PASEO_TERMINAL_BURST_GATE: burstGatePath },
|
||||
...nodeTerminalCommand(`
|
||||
@@ -272,6 +274,7 @@ it("pulls fresh terminal state from the worker authority", async () => {
|
||||
manager = createWorkerTerminalManager();
|
||||
const session = trackTerminal(
|
||||
await manager.createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd,
|
||||
...nodeTerminalCommand(`
|
||||
process.stdout.write("worker-state-ready\\n");
|
||||
@@ -304,6 +307,7 @@ it.skipIf(isPlatform("win32"))(
|
||||
// tracker records and reflects in its replay preamble (\x1b[=1;1u).
|
||||
const session = trackTerminal(
|
||||
await manager.createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd,
|
||||
...nodeTerminalCommand(`
|
||||
process.stdout.write("\\u001b[>1u");
|
||||
@@ -327,6 +331,7 @@ it("refreshes cached terminal title after worker title changes", async () => {
|
||||
manager = createWorkerTerminalManager();
|
||||
const session = trackTerminal(
|
||||
await manager.createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd,
|
||||
...nodeTerminalCommand(`
|
||||
process.stdout.write("\\u001b]0;Build Output\\u0007");
|
||||
@@ -344,7 +349,7 @@ it("refreshes cached terminal size after worker resize", async () => {
|
||||
const cwd = mkdtempSync(join(tmpdir(), "worker-terminal-manager-resize-"));
|
||||
temporaryDirs.push(cwd);
|
||||
manager = createWorkerTerminalManager();
|
||||
const session = trackTerminal(await manager.createTerminal({ cwd }));
|
||||
const session = trackTerminal(await manager.createTerminal({ cwd, workspaceId: "ws-test" }));
|
||||
|
||||
session.send({ type: "resize", rows: 10, cols: 40 });
|
||||
|
||||
@@ -361,7 +366,7 @@ it("captures terminal output from the worker authority", async () => {
|
||||
const cwd = mkdtempSync(join(tmpdir(), "worker-terminal-manager-capture-"));
|
||||
temporaryDirs.push(cwd);
|
||||
manager = createWorkerTerminalManager();
|
||||
const session = trackTerminal(await manager.createTerminal({ cwd }));
|
||||
const session = trackTerminal(await manager.createTerminal({ cwd, workspaceId: "ws-test" }));
|
||||
|
||||
session.send({ type: "input", data: "echo hello world\r" });
|
||||
|
||||
@@ -388,6 +393,7 @@ it("does not surface fire-and-forget send timeouts as unhandled rejections", asy
|
||||
id: "terminal-1",
|
||||
name: "Terminal",
|
||||
cwd: "/tmp",
|
||||
workspaceId: "ws-test",
|
||||
activity: { state: "idle", changedAt: 0 },
|
||||
},
|
||||
state: createTerminalState(),
|
||||
@@ -423,6 +429,7 @@ it("keeps registered cwd env inheritance behind the worker manager interface", a
|
||||
});
|
||||
trackTerminal(
|
||||
await manager.createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd,
|
||||
...nodeTerminalCommand(`
|
||||
require("node:fs").writeFileSync(
|
||||
@@ -450,6 +457,7 @@ it("injects parent-minted terminal activity env through the worker", async () =>
|
||||
|
||||
const session = trackTerminal(
|
||||
await manager.createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd,
|
||||
...nodeTerminalCommand(`
|
||||
require("node:fs").writeFileSync(
|
||||
@@ -495,7 +503,7 @@ it("starts the default shell through the worker and accepts quoted commands", as
|
||||
const cwd = mkdtempSync(join(tmpdir(), "worker-terminal-manager-shell-"));
|
||||
temporaryDirs.push(cwd);
|
||||
const markerPath = join(cwd, "shell quoted marker.txt");
|
||||
const session = trackTerminal(await manager.createTerminal({ cwd }));
|
||||
const session = trackTerminal(await manager.createTerminal({ cwd, workspaceId: "ws-test" }));
|
||||
const command = [
|
||||
"node",
|
||||
"-e",
|
||||
@@ -517,6 +525,7 @@ it("lists subdirectory terminals when querying the workspace root", async () =>
|
||||
manager = createWorkerTerminalManager();
|
||||
const created = trackTerminal(
|
||||
await manager.createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd: subdirCwd,
|
||||
...nodeTerminalCommand("setInterval(() => {}, 1000);"),
|
||||
}),
|
||||
@@ -540,6 +549,7 @@ it("lists terminals locally without waiting on the worker", async () => {
|
||||
id: "terminal-root",
|
||||
name: "Shell",
|
||||
cwd: "/workspace",
|
||||
workspaceId: "ws-test",
|
||||
activity: { state: "idle", changedAt: 0 },
|
||||
},
|
||||
state: createTerminalState(),
|
||||
@@ -550,6 +560,7 @@ it("lists terminals locally without waiting on the worker", async () => {
|
||||
id: "terminal-subdir",
|
||||
name: "Shell",
|
||||
cwd: "/workspace/apps/mobile",
|
||||
workspaceId: "ws-test",
|
||||
activity: { state: "idle", changedAt: 0 },
|
||||
},
|
||||
state: createTerminalState(),
|
||||
@@ -579,6 +590,7 @@ it("includes only stamped terminals in workspace-scoped local reads", async () =
|
||||
id: "terminal-legacy",
|
||||
name: "Legacy",
|
||||
cwd: "/workspace",
|
||||
workspaceId: "ws-test",
|
||||
activity: null,
|
||||
},
|
||||
state: createTerminalState(),
|
||||
@@ -640,6 +652,7 @@ it("surfaces worker activity changes via getActivity, onActivityChange, and term
|
||||
id: "terminal-a",
|
||||
name: "Shell",
|
||||
cwd: "/workspace",
|
||||
workspaceId: "ws-test",
|
||||
activity: { state: "idle", changedAt: 0 },
|
||||
},
|
||||
state: createTerminalState(),
|
||||
@@ -681,6 +694,7 @@ it("surfaces worker activity changes via getActivity, onActivityChange, and term
|
||||
terminalId: "terminal-a",
|
||||
name: "Shell",
|
||||
cwd: "/workspace",
|
||||
workspaceId: "ws-test",
|
||||
activity: workingActivity,
|
||||
previous: idleActivity,
|
||||
},
|
||||
@@ -700,6 +714,7 @@ it("sets terminal activity through a worker request", async () => {
|
||||
id: "terminal-a",
|
||||
name: "Shell",
|
||||
cwd: "/workspace",
|
||||
workspaceId: "ws-test",
|
||||
activity: null,
|
||||
},
|
||||
state: createTerminalState(),
|
||||
@@ -732,6 +747,7 @@ it("clears terminal attention through a worker request", async () => {
|
||||
id: "terminal-a",
|
||||
name: "Shell",
|
||||
cwd: "/workspace",
|
||||
workspaceId: "ws-test",
|
||||
activity: { state: "idle", attentionReason: "finished", changedAt: 1000 },
|
||||
},
|
||||
state: createTerminalState(),
|
||||
@@ -757,6 +773,7 @@ it("clears finished attention on a real terminal", async () => {
|
||||
manager = createWorkerTerminalManager();
|
||||
const session = trackTerminal(
|
||||
await manager.createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd,
|
||||
...nodeTerminalCommand("setInterval(() => {}, 1000);"),
|
||||
}),
|
||||
@@ -792,6 +809,7 @@ it("removes worker terminals after killAndWait", async () => {
|
||||
manager = createWorkerTerminalManager();
|
||||
const session = trackTerminal(
|
||||
await manager.createTerminal({
|
||||
workspaceId: "ws-test",
|
||||
cwd,
|
||||
...nodeTerminalCommand("setInterval(() => {}, 1000);"),
|
||||
}),
|
||||
@@ -822,6 +840,7 @@ it("produces one terminals-changed snapshot per title change", async () => {
|
||||
id: "terminal-a",
|
||||
name: "Shell",
|
||||
cwd: "/workspace",
|
||||
workspaceId: "ws-test",
|
||||
activity: null,
|
||||
},
|
||||
state: createTerminalState(),
|
||||
|
||||
@@ -36,6 +36,20 @@ import type {
|
||||
|
||||
const REQUEST_TIMEOUT_MS = 10000;
|
||||
|
||||
type RequiredWorkerTerminalInfo = WorkerTerminalInfo & { workspaceId: string };
|
||||
|
||||
function requiredWorkspaceId(workspaceId: string | undefined): string {
|
||||
if (workspaceId === undefined) {
|
||||
throw new Error("workspaceId is required");
|
||||
}
|
||||
return workspaceId;
|
||||
}
|
||||
|
||||
function asRequiredWorkerTerminalInfo(info: WorkerTerminalInfo): RequiredWorkerTerminalInfo {
|
||||
requiredWorkspaceId(info.workspaceId);
|
||||
return info as RequiredWorkerTerminalInfo;
|
||||
}
|
||||
|
||||
type TerminalWorkerRequestInput = TerminalWorkerRequest extends infer Request
|
||||
? Request extends TerminalWorkerRequest
|
||||
? Omit<Request, "requestId">
|
||||
@@ -49,7 +63,7 @@ interface PendingRequest {
|
||||
}
|
||||
|
||||
interface WorkerTerminalRecord {
|
||||
info: WorkerTerminalInfo;
|
||||
info: RequiredWorkerTerminalInfo;
|
||||
state: TerminalState;
|
||||
activity: TerminalActivity | null;
|
||||
// Cached input-mode preamble from the worker (the authoritative tracker lives
|
||||
@@ -114,12 +128,12 @@ function isResponse(message: TerminalWorkerToParentMessage): message is Terminal
|
||||
return message.type === "response";
|
||||
}
|
||||
|
||||
function cloneTerminalInfo(info: WorkerTerminalInfo): WorkerTerminalInfo {
|
||||
function cloneTerminalInfo(info: RequiredWorkerTerminalInfo): RequiredWorkerTerminalInfo {
|
||||
return {
|
||||
id: info.id,
|
||||
name: info.name,
|
||||
cwd: info.cwd,
|
||||
...(info.workspaceId ? { workspaceId: info.workspaceId } : {}),
|
||||
workspaceId: info.workspaceId,
|
||||
...(info.title ? { title: info.title } : {}),
|
||||
activity: info.activity,
|
||||
};
|
||||
@@ -196,7 +210,7 @@ export function createWorkerTerminalManager(
|
||||
id: record.info.id,
|
||||
name: record.info.name,
|
||||
cwd: record.info.cwd,
|
||||
...(record.info.workspaceId ? { workspaceId: record.info.workspaceId } : {}),
|
||||
workspaceId: record.info.workspaceId,
|
||||
...(record.info.title ? { title: record.info.title } : {}),
|
||||
activity: record.activity,
|
||||
});
|
||||
@@ -205,7 +219,7 @@ export function createWorkerTerminalManager(
|
||||
}
|
||||
|
||||
function registerRecord(input: {
|
||||
info: WorkerTerminalInfo;
|
||||
info: RequiredWorkerTerminalInfo;
|
||||
state: TerminalState;
|
||||
}): TerminalSession {
|
||||
const existing = recordsById.get(input.info.id);
|
||||
@@ -438,7 +452,7 @@ export function createWorkerTerminalManager(
|
||||
emitTerminalWorkspaceContributionChanged({
|
||||
terminalId: removedRecord.info.id,
|
||||
cwd: removedRecord.info.cwd,
|
||||
...(removedRecord.info.workspaceId ? { workspaceId: removedRecord.info.workspaceId } : {}),
|
||||
workspaceId: removedRecord.info.workspaceId,
|
||||
});
|
||||
}
|
||||
emitTerminalsChanged({
|
||||
@@ -506,6 +520,7 @@ export function createWorkerTerminalManager(
|
||||
terminalId: record.info.id,
|
||||
name: record.info.name,
|
||||
cwd: record.info.cwd,
|
||||
workspaceId: record.info.workspaceId,
|
||||
activity: message.activity,
|
||||
previous: message.previous,
|
||||
});
|
||||
@@ -515,7 +530,7 @@ export function createWorkerTerminalManager(
|
||||
emitTerminalWorkspaceContributionChanged({
|
||||
terminalId: record.info.id,
|
||||
cwd: record.info.cwd,
|
||||
...(record.info.workspaceId ? { workspaceId: record.info.workspaceId } : {}),
|
||||
workspaceId: record.info.workspaceId,
|
||||
});
|
||||
}
|
||||
emitTerminalsChanged({
|
||||
@@ -527,7 +542,10 @@ export function createWorkerTerminalManager(
|
||||
function handleWorkerEvent(message: TerminalWorkerToParentMessage): void {
|
||||
switch (message.type) {
|
||||
case "terminalCreated": {
|
||||
registerRecord({ info: message.terminal, state: message.state });
|
||||
registerRecord({
|
||||
info: asRequiredWorkerTerminalInfo(message.terminal),
|
||||
state: message.state,
|
||||
});
|
||||
emitTerminalsChanged({
|
||||
cwd: message.terminal.cwd,
|
||||
terminals: listTerminalItemsForCwd(message.terminal.cwd),
|
||||
@@ -661,13 +679,15 @@ export function createWorkerTerminalManager(
|
||||
return sessions;
|
||||
},
|
||||
|
||||
async createTerminal(options: WorkerCreateTerminalOptions): Promise<TerminalSession> {
|
||||
async createTerminal(
|
||||
options: WorkerCreateTerminalOptions & { workspaceId: string },
|
||||
): Promise<TerminalSession> {
|
||||
const terminalId = options.id ?? randomUUID();
|
||||
const activityToken = createActivityToken();
|
||||
const terminalActivityUrl = managerOptions.getTerminalActivityUrl?.() ?? null;
|
||||
terminalActivityTokenById.set(terminalId, activityToken);
|
||||
let result: {
|
||||
terminal: WorkerTerminalInfo;
|
||||
terminal: RequiredWorkerTerminalInfo;
|
||||
state: TerminalState;
|
||||
};
|
||||
try {
|
||||
@@ -680,7 +700,7 @@ export function createWorkerTerminalManager(
|
||||
activityUrl: terminalActivityUrl,
|
||||
},
|
||||
})) as {
|
||||
terminal: WorkerTerminalInfo;
|
||||
terminal: RequiredWorkerTerminalInfo;
|
||||
state: TerminalState;
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user