feat: move session sharing and collab rooms into a plugin (B12)

This commit is contained in:
LukeGus committed 2026-09-23 23:42:46 -05:00
1 parent 791d8656a1
commit a276f6898c
207 files changed
+35771 -11072

No files matched your search

+2 -1
View File
@@ -27,7 +27,8 @@
{
"service": "sessions.live",
"version": "1.0.0",
"permission": "ssh-terminal.sessions"
"permission": "ssh-terminal.sessions",
"names": ["ssh"]
},
{
"service": "terminal.history",
+2 -1
View File
@@ -153,7 +153,8 @@ export async function activate(ctx: PluginContext) {
},
idleTimeoutMinutes: () => timeoutMinutes,
};
ctx.services.provide("sessions.live", liveSessions);
// sessions.live is keyed by session type; remote desktop provides the others.
ctx.services.provide("sessions.live", liveSessions, { name: "ssh" });
const terminalHistory: TerminalHistoryV1 = {
list: async (hostId, limit = 200) => {
+7 -5
View File
@@ -1,11 +1,13 @@
/**
* The services this plugin provides and consumes.
*
* Provided: "sessions.live" (live SSH sessions, for session sharing, collab
* and recording) and "terminal.history" (command history, for the AI
* assistant). Consumed, all optional: "tmux.sessions" (B10),
* "sessions.sharing" (B12) and "recordings.writer" (B13). The terminal keeps
* working without any of them: no tmux attach, no shared joins, no recording.
* Provided: "sessions.live" as its "ssh" provider (live SSH sessions, for
* session sharing, collab rooms and recording; remote desktop provides the
* other session types) and "terminal.history" (command history, for the AI
* assistant). Consumed, all optional: "tmux.sessions" (tmux-monitor),
* "sessions.sharing" (session-sharing) and "recordings.writer" (B13). The
* terminal keeps working without any of them: no tmux attach, no shared
* joins, no recording.
*/
export interface LiveSessionInfo {
@@ -784,8 +784,30 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
openSidePanel: (panelId, props) =>
setDock({ id: panelId, props: props ?? {} }),
runCommand: handleRunCommandInTerminal,
getShareTarget: () =>
isConnected &&
!isQuickConnect &&
!hostConfig.joinShareId &&
typeof hostConfig.id === "number" &&
sessionIdRef.current
? {
hostId: hostConfig.id,
sessionId: sessionIdRef.current,
protocol: "ssh" as const,
tabInstanceId: hostConfig.instanceId,
}
: null,
}),
[host, terminal, handleRunCommandInTerminal],
[
host,
terminal,
handleRunCommandInTerminal,
isConnected,
isQuickConnect,
hostConfig.joinShareId,
hostConfig.id,
hostConfig.instanceId,
],
);
const overlayProps = useMemo<TerminalOverlayProps>(
@@ -1423,27 +1445,8 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
onOpenFileManager?.("/");
}
},
getShareTarget: () =>
isConnected &&
!isQuickConnect &&
!hostConfig.joinShareId &&
typeof hostConfig.id === "number" &&
sessionIdRef.current
? {
hostId: hostConfig.id,
sessionId: sessionIdRef.current,
protocol: "ssh" as const,
tabInstanceId: hostConfig.instanceId,
}
: null,
}),
[
isConnected,
terminal,
isQuickConnect,
hostConfig.joinShareId,
hostConfig.id,
],
[isConnected, terminal],
);
function getCopyOnSelect() {
@@ -29,6 +29,16 @@ export interface TerminalSlotApi {
openSidePanel: (panelId: string, props?: Record<string, unknown>) => void;
/** Types a command into the session and runs it. */
runCommand: (command: string) => void;
/**
* What sharing this session needs, or null while there is nothing to share
* (not connected yet, a quick connect, or a session joined from a share).
*/
getShareTarget: () => {
hostId: number;
sessionId: string;
protocol: "ssh";
tabInstanceId?: string;
} | null;
}
export interface TerminalToolbarStatusProps {
@@ -33,11 +33,4 @@ export interface TerminalHandle {
notifyResize: () => void;
refresh: () => void;
getApplicationCursorKeysMode: () => boolean;
/** What the shell's share action needs, or null while there is nothing to share. */
getShareTarget: () => {
hostId: number;
sessionId: string;
protocol: "ssh";
tabInstanceId?: string;
} | null;
}
@@ -30,9 +30,11 @@ describe("ssh-terminal activate", () => {
expect(server.mock.wsRoutes).toEqual([{ path: "/terminal", raw: false }]);
});
it("provides sessions.live and terminal.history", async () => {
it("provides sessions.live as ssh and terminal.history", async () => {
server = await startServer();
const live = server.mock.services.get("sessions.live") as LiveSessionsV1;
const live = server.mock.services.get(
"sessions.live#ssh",
) as LiveSessionsV1;
expect(live.getSession("missing")).toBeNull();
expect(live.listForUser("user-1")).toEqual([]);
expect(live.write("missing", "ls\r")).toBe(false);
@@ -52,7 +54,9 @@ describe("ssh-terminal activate", () => {
it("keeps a plugin caller to its own actor's sessions", async () => {
server = await startServer();
const live = server.mock.services.get("sessions.live") as LiveSessionsV1;
const live = server.mock.services.get(
"sessions.live#ssh",
) as LiveSessionsV1;
const other = await server.mock.ctx.asUser("user-2", async () =>
live.listForUser("user-1"),
);
@@ -61,7 +65,9 @@ describe("ssh-terminal activate", () => {
it("uses the admin session timeout for detached sessions", async () => {
server = await startServer({ settings: { sessionTimeoutMinutes: 90 } });
const live = server.mock.services.get("sessions.live") as LiveSessionsV1;
const live = server.mock.services.get(
"sessions.live#ssh",
) as LiveSessionsV1;
expect(live.idleTimeoutMinutes()).toBe(90);
});
@@ -901,6 +901,7 @@ describe("TerminalToolbar Phase 1", () => {
sessionId: () => null,
openSidePanel: vi.fn(),
runCommand: vi.fn(),
getShareTarget: () => null,
};
renderToolbar({ slotApi });
await userEvent.click(