feat: wire ai, automations, fleets and ssh-terminal to snippets (B2)

This commit is contained in:
LukeGus committed 2026-09-23 08:59:24 -05:00
1 parent 3656caf168
commit c77301577f
16 files changed
+473 -52

No files matched your search

@@ -0,0 +1,68 @@
import { useEffect, useRef } from "react";
import type { TabProps } from "@termix/plugin-sdk/frontend";
import { TerminalTabContent } from "@/features/terminal/TerminalTabContent";
import type { TabRenderProps } from "@/shell/tab-registry";
import type { TerminalHandle } from "@/features/terminal/terminal-types";
import { registerSession, setActiveSession } from "./session-registry";
/**
* Wraps the core terminal tab component to register its ref in the session
* registry, so terminal.sendToActive/sendToSession (used by the snippets
* plugin, among others) can reach a live session without core exposing
* terminalRef through the public tab props.
*/
export function TerminalTabWithRegistry(props: TabProps) {
const tabRecord = props.tab as unknown as {
id: string;
terminalRef?: { current: TerminalHandle | null };
};
const host = props.host as
| {
id: string;
name?: string;
ip?: string;
username?: string;
port?: number;
}
| undefined;
const registeredRef = useRef<(() => void) | null>(null);
useEffect(() => {
let cancelled = false;
let attempts = 0;
const tryRegister = () => {
if (cancelled || registeredRef.current) return;
const ref = tabRecord.terminalRef?.current;
if (!ref) {
if (attempts++ < 40) setTimeout(tryRegister, 250);
return;
}
registeredRef.current = registerSession(
{
id: tabRecord.id,
hostId: host ? Number(host.id) || null : null,
hostName: host?.name,
ip: host?.ip,
username: host?.username,
port: host?.port,
},
ref,
);
};
tryRegister();
return () => {
cancelled = true;
registeredRef.current?.();
registeredRef.current = null;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tabRecord.id]);
useEffect(() => {
if (props.isFocusedPane) setActiveSession(tabRecord.id);
}, [props.isFocusedPane, tabRecord.id]);
return <TerminalTabContent {...(props as unknown as TabRenderProps)} />;
}
+17 -5
View File
@@ -5,16 +5,15 @@ import type {
TabProps,
TermixApp,
} from "@termix/plugin-sdk/frontend";
import {
TerminalTabContent,
loadTerminal,
} from "@/features/terminal/TerminalTabContent";
import { loadTerminal } from "@/features/terminal/TerminalTabContent";
import TerminalApp from "@/features/terminal/TerminalApp";
import {
TERMINAL_DOCK_SLOT,
TERMINAL_OVERLAY_SLOT,
TERMINAL_TOOLBAR_SLOT,
} from "@/features/terminal/terminal-slots";
import { TerminalTabWithRegistry } from "./TerminalTabWithRegistry";
import { listSessions, sendToActive, sendToSession } from "./session-registry";
/** `?view=terminal` full-screen links. */
function TerminalStandalone({ hostId, params }: StandaloneViewProps) {
@@ -33,7 +32,7 @@ function TerminalStandalone({ hostId, params }: StandaloneViewProps) {
export function activate(app: TermixApp): void {
app.registerTab(
"terminal",
TerminalTabContent as unknown as ComponentType<TabProps>,
TerminalTabWithRegistry as unknown as ComponentType<TabProps>,
{
icon: Terminal,
titleKey: "nav.terminal",
@@ -63,4 +62,17 @@ export function activate(app: TermixApp): void {
app.declareActionSlot({ id: TERMINAL_TOOLBAR_SLOT, accepts: ["button"] });
app.declareActionSlot({ id: TERMINAL_DOCK_SLOT, accepts: ["component"] });
app.declareActionSlot({ id: TERMINAL_OVERLAY_SLOT, accepts: ["component"] });
// Lets another plugin (snippets) push text into a live terminal session
// without reaching into core's tab state itself.
app.registerAction("terminal.listSessions", () => listSessions());
app.registerAction("terminal.sendToActive", ((
text: string,
opts?: { run?: boolean },
) => sendToActive(text, opts)) as never);
app.registerAction("terminal.sendToSession", ((
sessionId: string,
text: string,
opts?: { run?: boolean },
) => sendToSession(sessionId, text, opts)) as never);
}
@@ -0,0 +1,61 @@
import type { TerminalHandle } from "@/features/terminal/terminal-types";
export interface TerminalSessionInfo {
id: string;
hostId: number | null;
hostName?: string;
ip?: string;
username?: string;
port?: number;
}
interface Session extends TerminalSessionInfo {
ref: TerminalHandle;
}
const sessions = new Map<string, Session>();
let activeId: string | null = null;
/** Called by the terminal tab wrapper on mount/unmount and focus change. */
export function registerSession(
info: TerminalSessionInfo,
ref: TerminalHandle,
): () => void {
sessions.set(info.id, { ...info, ref });
return () => {
sessions.delete(info.id);
if (activeId === info.id) activeId = null;
};
}
export function setActiveSession(id: string | null): void {
activeId = id;
}
export function listSessions(): TerminalSessionInfo[] {
return [...sessions.values()].map(({ ref: _ref, ...info }) => info);
}
function send(session: Session, text: string, run?: boolean): void {
if (run) session.ref.sendInput(text + "\r");
else session.ref.paste(text);
}
export function sendToActive(text: string, opts?: { run?: boolean }): boolean {
if (!activeId) return false;
const session = sessions.get(activeId);
if (!session) return false;
send(session, text, opts?.run);
return true;
}
export function sendToSession(
sessionId: string,
text: string,
opts?: { run?: boolean },
): boolean {
const session = sessions.get(sessionId);
if (!session) return false;
send(session, text, opts?.run);
return true;
}