feat: add file tabs and improve terminal rendering with WebGL
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildHostWorkspaceRoute,
|
||||
buildHostWorkspaceFileRoute,
|
||||
decodeFilePathFromPathSegment,
|
||||
decodeWorkspaceIdFromPathSegment,
|
||||
encodeFilePathForPathSegment,
|
||||
encodeWorkspaceIdForPathSegment,
|
||||
parseHostAgentDraftRouteFromPathname,
|
||||
parseHostAgentRouteFromPathname,
|
||||
parseHostDraftRouteFromPathname,
|
||||
parseHostWorkspaceAgentRouteFromPathname,
|
||||
parseHostWorkspaceFileRouteFromPathname,
|
||||
parseHostWorkspaceTerminalRouteFromPathname,
|
||||
parseHostWorkspaceRouteFromPathname,
|
||||
} from "./host-routes";
|
||||
@@ -54,6 +58,12 @@ describe("workspace route parsing", () => {
|
||||
expect(decodeWorkspaceIdFromPathSegment("L3RtcC9yZXBv")).toBe("/tmp/repo");
|
||||
});
|
||||
|
||||
it("encodes file paths as base64url (no padding)", () => {
|
||||
const encoded = encodeFilePathForPathSegment("src/index.ts");
|
||||
expect(encoded).toMatch(/^[A-Za-z0-9_-]+$/);
|
||||
expect(decodeFilePathFromPathSegment(encoded)).toBe("src/index.ts");
|
||||
});
|
||||
|
||||
it("parses workspace route", () => {
|
||||
expect(
|
||||
parseHostWorkspaceRouteFromPathname("/h/local/workspace/L3RtcC9yZXBv")
|
||||
@@ -63,6 +73,19 @@ describe("workspace route parsing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("parses workspace file route", () => {
|
||||
const encodedPath = encodeFilePathForPathSegment("src/index.ts");
|
||||
expect(
|
||||
parseHostWorkspaceFileRouteFromPathname(
|
||||
`/h/local/workspace/L3RtcC9yZXBv/file/${encodedPath}`
|
||||
)
|
||||
).toEqual({
|
||||
serverId: "local",
|
||||
workspaceId: "/tmp/repo",
|
||||
filePath: "src/index.ts",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses workspace agent route", () => {
|
||||
expect(
|
||||
parseHostWorkspaceAgentRouteFromPathname(
|
||||
@@ -104,4 +127,10 @@ describe("workspace route parsing", () => {
|
||||
"/h/local/workspace/L3RtcC9yZXBv"
|
||||
);
|
||||
});
|
||||
|
||||
it("builds base64url workspace file routes", () => {
|
||||
expect(buildHostWorkspaceFileRoute("local", "/tmp/repo", "src/index.ts")).toBe(
|
||||
`/h/local/workspace/L3RtcC9yZXBv/file/${encodeFilePathForPathSegment("src/index.ts")}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -94,6 +94,26 @@ export function decodeWorkspaceIdFromPathSegment(workspaceIdSegment: string): st
|
||||
return normalizeWorkspaceId(decoded);
|
||||
}
|
||||
|
||||
export function encodeFilePathForPathSegment(filePath: string): string {
|
||||
const normalized = trimNonEmpty(filePath);
|
||||
if (!normalized) {
|
||||
return "";
|
||||
}
|
||||
return toBase64UrlNoPad(normalized);
|
||||
}
|
||||
|
||||
export function decodeFilePathFromPathSegment(filePathSegment: string): string | null {
|
||||
const normalizedSegment = trimNonEmpty(filePathSegment);
|
||||
if (!normalizedSegment) {
|
||||
return null;
|
||||
}
|
||||
const decoded = trimNonEmpty(decodeSegment(normalizedSegment));
|
||||
if (!decoded) {
|
||||
return null;
|
||||
}
|
||||
return tryDecodeBase64UrlNoPadUtf8(decoded);
|
||||
}
|
||||
|
||||
export function parseServerIdFromPathname(pathname: string): string | null {
|
||||
const match = pathname.match(/^\/h\/([^/]+)(?:\/|$)/);
|
||||
if (!match) {
|
||||
@@ -211,6 +231,10 @@ export function parseHostWorkspaceRouteFromPathname(
|
||||
if (terminalIdx >= 0 && terminalIdx > workspaceIdStart) {
|
||||
workspaceIdEnd = Math.min(workspaceIdEnd, terminalIdx);
|
||||
}
|
||||
const fileIdx = pathname.lastIndexOf("/file/");
|
||||
if (fileIdx >= 0 && fileIdx > workspaceIdStart) {
|
||||
workspaceIdEnd = Math.min(workspaceIdEnd, fileIdx);
|
||||
}
|
||||
|
||||
const rawWorkspaceId = pathname.slice(workspaceIdStart, workspaceIdEnd).replace(/\/+$/, "");
|
||||
const workspaceId = decodeWorkspaceIdFromPathSegment(rawWorkspaceId);
|
||||
@@ -320,6 +344,55 @@ export function parseHostWorkspaceTerminalRouteFromPathname(
|
||||
return { serverId, workspaceId, terminalId };
|
||||
}
|
||||
|
||||
export function parseHostWorkspaceFileRouteFromPathname(
|
||||
pathname: string
|
||||
): { serverId: string; workspaceId: string; filePath: string } | null {
|
||||
const prefix = "/h/";
|
||||
if (!pathname.startsWith(prefix)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const serverIdStart = prefix.length;
|
||||
const serverIdEnd = pathname.indexOf("/", serverIdStart);
|
||||
if (serverIdEnd < 0) {
|
||||
return null;
|
||||
}
|
||||
const rawServerId = pathname.slice(serverIdStart, serverIdEnd);
|
||||
const serverId = trimNonEmpty(decodeSegment(rawServerId));
|
||||
if (!serverId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workspacePrefix = "/workspace/";
|
||||
if (!pathname.startsWith(workspacePrefix, serverIdEnd)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workspaceIdStart = serverIdEnd + workspacePrefix.length;
|
||||
const fileMarker = "/file/";
|
||||
const fileIdx = pathname.lastIndexOf(fileMarker);
|
||||
if (fileIdx < 0 || fileIdx <= workspaceIdStart) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawWorkspaceId = pathname.slice(workspaceIdStart, fileIdx).replace(/\/+$/, "");
|
||||
const workspaceId = decodeWorkspaceIdFromPathSegment(rawWorkspaceId);
|
||||
if (!workspaceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filePathStart = fileIdx + fileMarker.length;
|
||||
const filePathEnd = pathname.indexOf("/", filePathStart);
|
||||
const rawFilePath =
|
||||
filePathEnd < 0 ? pathname.slice(filePathStart) : pathname.slice(filePathStart, filePathEnd);
|
||||
const filePath = decodeFilePathFromPathSegment(rawFilePath);
|
||||
if (!filePath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { serverId, workspaceId, filePath };
|
||||
}
|
||||
|
||||
export function buildHostWorkspaceRoute(
|
||||
serverId: string,
|
||||
workspaceId: string
|
||||
@@ -362,6 +435,19 @@ export function buildHostWorkspaceTerminalRoute(
|
||||
return `${base}/terminal/${encodeSegment(normalizedTerminalId)}`;
|
||||
}
|
||||
|
||||
export function buildHostWorkspaceFileRoute(
|
||||
serverId: string,
|
||||
workspaceId: string,
|
||||
filePath: string
|
||||
): string {
|
||||
const base = buildHostWorkspaceRoute(serverId, workspaceId);
|
||||
const encodedFilePath = encodeFilePathForPathSegment(filePath);
|
||||
if (base === "/" || !encodedFilePath) {
|
||||
return "/";
|
||||
}
|
||||
return `${base}/file/${encodeSegment(encodedFilePath)}`;
|
||||
}
|
||||
|
||||
export function buildHostAgentDetailRoute(
|
||||
serverId: string,
|
||||
agentId: string,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { ITheme } from "@xterm/xterm";
|
||||
|
||||
import type { Theme } from "@/styles/theme";
|
||||
|
||||
type TerminalPalette = Theme["colors"]["terminal"];
|
||||
|
||||
export function toXtermTheme(terminal: TerminalPalette): ITheme {
|
||||
return {
|
||||
background: terminal.background,
|
||||
foreground: terminal.foreground,
|
||||
cursor: terminal.cursor,
|
||||
cursorAccent: terminal.cursorAccent,
|
||||
selectionBackground: terminal.selectionBackground,
|
||||
selectionForeground: terminal.selectionForeground,
|
||||
|
||||
black: terminal.black,
|
||||
red: terminal.red,
|
||||
green: terminal.green,
|
||||
yellow: terminal.yellow,
|
||||
blue: terminal.blue,
|
||||
magenta: terminal.magenta,
|
||||
cyan: terminal.cyan,
|
||||
white: terminal.white,
|
||||
|
||||
brightBlack: terminal.brightBlack,
|
||||
brightRed: terminal.brightRed,
|
||||
brightGreen: terminal.brightGreen,
|
||||
brightYellow: terminal.brightYellow,
|
||||
brightBlue: terminal.brightBlue,
|
||||
brightMagenta: terminal.brightMagenta,
|
||||
brightCyan: terminal.brightCyan,
|
||||
brightWhite: terminal.brightWhite,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user