feat: move the terminal fully into the ssh-terminal plugin (B9)

This commit is contained in:
LukeGus committed 2026-09-23 21:41:22 -05:00
1 parent 535126fd1b
commit 79d77678c9
309 files changed
+41789 -14958

No files matched your search

+14 -12
View File
@@ -6,9 +6,9 @@ import type {
TermixApp,
} from "@termix/plugin-sdk/frontend";
import type {
TerminalDockProps,
TerminalSidePanelProps,
TerminalSlotApi,
} from "@/features/terminal/terminal-slots";
} from "./terminal/terminal-slot-types";
import { AiPanel } from "./AiPanel";
import { TerminalAiPanel } from "./terminal/TerminalAiPanel";
import { AiAssistantStep } from "./AiAssistantStep";
@@ -18,7 +18,7 @@ import { getAiStatus } from "./ai-api";
import { AI_STATUS_CHANGED_EVENT } from "./use-ai-availability";
const ASSISTANT_ACTION = "ai.openWithContext";
const DOCK_ID = "ai.assistant";
const SIDE_PANEL_ID = "ai.assistant";
function AssistantPanel({ activeTabType }: PanelProps) {
return (
@@ -32,15 +32,15 @@ function AssistantTab() {
return <AiPanel />;
}
/** The assistant docked beside a terminal. */
function TerminalDock({
/** The assistant beside a terminal, in its "terminal.sidePanel" slot. */
function TerminalSidePanel({
host,
hostId,
hostLabel,
dockProps,
panelProps,
onClose,
onRunInTerminal,
}: TerminalDockProps) {
}: TerminalSidePanelProps) {
if (!hostId) return null;
return (
<TerminalAiPanel
@@ -48,7 +48,7 @@ function TerminalDock({
hostId={hostId}
activeTab={`terminal:${host?.name || host?.ip || ""}`}
initialContext={
typeof dockProps.context === "string" ? dockProps.context : ""
typeof panelProps.context === "string" ? panelProps.context : ""
}
onClose={onClose}
onRunInTerminal={onRunInTerminal}
@@ -114,12 +114,12 @@ export function activate(app: TermixApp): void {
}),
);
surface.push(
app.registerSlotContribution("terminal.dock", {
actionId: DOCK_ID,
app.registerSlotContribution("terminal.sidePanel", {
actionId: SIDE_PANEL_ID,
titleKey: "ai.assistant",
icon: Bot,
kind: "component",
component: TerminalDock as unknown as ComponentType<
component: TerminalSidePanel as unknown as ComponentType<
Record<string, unknown>
>,
when: offeredOnHost,
@@ -165,7 +165,9 @@ export function activate(app: TermixApp): void {
app.registerAction(
ASSISTANT_ACTION,
(terminal: TerminalSlotApi) =>
terminal?.openDock(DOCK_ID, { context: terminal.getBufferText() }),
terminal?.openSidePanel(SIDE_PANEL_ID, {
context: terminal.getBufferText(),
}),
{ permission: "services.use" },
);
@@ -18,7 +18,7 @@ import {
persistAiPanelPosition,
readStoredAiPanelPosition,
type ToolbarPosition,
} from "@/features/terminal/toolbar-geometry";
} from "./panel-geometry";
interface HistoryEntry {
role: "user" | "assistant";
@@ -0,0 +1,90 @@
/**
* Where the docked assistant panel sits over a terminal. Same clamping rules
* as the terminal's own toolbar, kept here so the panel owns its geometry.
*/
export interface ToolbarPosition {
x: number;
y: number;
}
export interface RectLike {
left: number;
right: number;
top: number;
bottom: number;
width: number;
height: number;
}
const AI_PANEL_POSITION_STORAGE_KEY = "termix-terminal-ai-panel-position";
const TOOLBAR_MARGIN = 8;
const RECOVERY_SIZE = 44;
function sanitizeToolbarPosition(value: unknown): ToolbarPosition {
if (!value || typeof value !== "object") return { x: 0, y: 0 };
const candidate = value as Partial<ToolbarPosition>;
return Number.isFinite(candidate.x) && Number.isFinite(candidate.y)
? { x: Number(candidate.x), y: Number(candidate.y) }
: { x: 0, y: 0 };
}
export function readStoredAiPanelPosition(): ToolbarPosition {
if (typeof window === "undefined") return { x: 0, y: 0 };
try {
return sanitizeToolbarPosition(
JSON.parse(
window.localStorage.getItem(AI_PANEL_POSITION_STORAGE_KEY) ?? "null",
),
);
} catch {
return { x: 0, y: 0 };
}
}
export function persistAiPanelPosition(position: ToolbarPosition): void {
try {
window.localStorage.setItem(
AI_PANEL_POSITION_STORAGE_KEY,
JSON.stringify(sanitizeToolbarPosition(position)),
);
} catch {
// Storage may be unavailable in hardened browser contexts.
}
}
export function clampToolbarPosition(
position: ToolbarPosition,
toolbarRect: RectLike,
hostRect: RectLike,
renderedPosition: ToolbarPosition = position,
): ToolbarPosition {
if (
!toolbarRect.width ||
!toolbarRect.height ||
!hostRect.width ||
!hostRect.height
)
return sanitizeToolbarPosition(position);
const left = hostRect.left + TOOLBAR_MARGIN;
const right = hostRect.right - TOOLBAR_MARGIN;
const top = hostRect.top + TOOLBAR_MARGIN;
const bottom = hostRect.bottom - TOOLBAR_MARGIN;
const baseLeft = toolbarRect.left - renderedPosition.x;
const baseRight = toolbarRect.right - renderedPosition.x;
const baseTop = toolbarRect.top - renderedPosition.y;
const baseBottom = toolbarRect.bottom - renderedPosition.y;
const clamp = (min: number, max: number, value: number) =>
Math.min(max, Math.max(min, value));
const xMin =
toolbarRect.width <= right - left
? left - baseLeft
: left + RECOVERY_SIZE - baseRight;
const xMax = right - baseRight;
const yMin =
toolbarRect.height <= bottom - top
? top - baseTop
: top + RECOVERY_SIZE - baseBottom;
const yMax = bottom - baseBottom;
return { x: clamp(xMin, xMax, position.x), y: clamp(yMin, yMax, position.y) };
}
@@ -0,0 +1,31 @@
/**
* What the ssh-terminal plugin hands its slots, as that plugin documents it.
* Kept as a local copy: a plugin never imports another plugin's source.
*/
interface TerminalHost {
id: number | string;
name?: string;
ip?: string;
enableAiAssistant?: boolean;
[key: string]: unknown;
}
/** What a "terminal.toolbar" action is invoked with. */
export interface TerminalSlotApi {
host: TerminalHost | undefined;
getBufferText: () => string;
sessionId: () => string | null;
openSidePanel: (panelId: string, props?: Record<string, unknown>) => void;
runCommand: (command: string) => void;
}
/** What a "terminal.sidePanel" component is rendered with. */
export interface TerminalSidePanelProps {
host: TerminalHost | undefined;
hostId: number | undefined;
hostLabel: string;
panelProps: Record<string, unknown>;
onClose: () => void;
onRunInTerminal: (command: string) => void;
}