feat: unify workspace routing through tab-based URLs and remove legacy per-type routes
This commit is contained in:
@@ -47,7 +47,6 @@ import { buildNotificationRoute } from "@/utils/notification-routing";
|
|||||||
import {
|
import {
|
||||||
buildHostDraftRoute,
|
buildHostDraftRoute,
|
||||||
parseHostAgentRouteFromPathname,
|
parseHostAgentRouteFromPathname,
|
||||||
parseHostWorkspaceAgentRouteFromPathname,
|
|
||||||
parseHostWorkspaceTabRouteFromPathname,
|
parseHostWorkspaceTabRouteFromPathname,
|
||||||
} from "@/utils/host-routes";
|
} from "@/utils/host-routes";
|
||||||
import { getTauri } from "@/utils/tauri";
|
import { getTauri } from "@/utils/tauri";
|
||||||
@@ -388,9 +387,7 @@ function AppWithSidebar({ children }: { children: ReactNode }) {
|
|||||||
return agentId ? `${workspaceTab.serverId}:${agentId}` : undefined;
|
return agentId ? `${workspaceTab.serverId}:${agentId}` : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const match =
|
const match = parseHostAgentRouteFromPathname(pathname);
|
||||||
parseHostWorkspaceAgentRouteFromPathname(pathname) ??
|
|
||||||
parseHostAgentRouteFromPathname(pathname);
|
|
||||||
return match ? `${match.serverId}:${match.agentId}` : undefined;
|
return match ? `${match.serverId}:${match.agentId}` : undefined;
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
@@ -478,13 +475,6 @@ export default function RootLayout() {
|
|||||||
<Stack.Screen name="h/[serverId]/new" />
|
<Stack.Screen name="h/[serverId]/new" />
|
||||||
<Stack.Screen name="h/[serverId]/workspace/[workspaceId]/index" />
|
<Stack.Screen name="h/[serverId]/workspace/[workspaceId]/index" />
|
||||||
<Stack.Screen name="h/[serverId]/workspace/[workspaceId]/tab/[tabId]" />
|
<Stack.Screen name="h/[serverId]/workspace/[workspaceId]/tab/[tabId]" />
|
||||||
<Stack.Screen
|
|
||||||
name="h/[serverId]/workspace/[workspaceId]/agent/[agentId]"
|
|
||||||
options={{ gestureEnabled: false }}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name="h/[serverId]/workspace/[workspaceId]/terminal/[terminalId]"
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="h/[serverId]/agent/[agentId]"
|
name="h/[serverId]/agent/[agentId]"
|
||||||
options={{ gestureEnabled: false }}
|
options={{ gestureEnabled: false }}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useSessionStore } from "@/stores/session-store";
|
|||||||
import { useHostRuntimeSession } from "@/runtime/host-runtime";
|
import { useHostRuntimeSession } from "@/runtime/host-runtime";
|
||||||
import {
|
import {
|
||||||
buildHostDraftRoute,
|
buildHostDraftRoute,
|
||||||
buildHostWorkspaceAgentRoute,
|
buildHostWorkspaceAgentTabRoute,
|
||||||
} from "@/utils/host-routes";
|
} from "@/utils/host-routes";
|
||||||
|
|
||||||
export default function HostAgentReadyRoute() {
|
export default function HostAgentReadyRoute() {
|
||||||
@@ -38,7 +38,7 @@ export default function HostAgentReadyRoute() {
|
|||||||
if (normalizedCwd) {
|
if (normalizedCwd) {
|
||||||
redirectedRef.current = true;
|
redirectedRef.current = true;
|
||||||
router.replace(
|
router.replace(
|
||||||
buildHostWorkspaceAgentRoute(serverId, normalizedCwd, agentId) as any
|
buildHostWorkspaceAgentTabRoute(serverId, normalizedCwd, agentId) as any
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [agentCwd, agentId, router, serverId]);
|
}, [agentCwd, agentId, router, serverId]);
|
||||||
@@ -77,7 +77,7 @@ export default function HostAgentReadyRoute() {
|
|||||||
const cwd = result?.agent?.cwd?.trim();
|
const cwd = result?.agent?.cwd?.trim();
|
||||||
redirectedRef.current = true;
|
redirectedRef.current = true;
|
||||||
if (cwd) {
|
if (cwd) {
|
||||||
router.replace(buildHostWorkspaceAgentRoute(serverId, cwd, agentId) as any);
|
router.replace(buildHostWorkspaceAgentTabRoute(serverId, cwd, agentId) as any);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
router.replace(buildHostDraftRoute(serverId) as any);
|
router.replace(buildHostDraftRoute(serverId) as any);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useLocalSearchParams, useRouter } from "expo-router";
|
|||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import {
|
import {
|
||||||
buildHostDraftRoute,
|
buildHostDraftRoute,
|
||||||
buildHostWorkspaceAgentRoute,
|
buildHostWorkspaceAgentTabRoute,
|
||||||
} from "@/utils/host-routes";
|
} from "@/utils/host-routes";
|
||||||
|
|
||||||
export default function HostIndexRoute() {
|
export default function HostIndexRoute() {
|
||||||
@@ -31,7 +31,7 @@ export default function HostIndexRoute() {
|
|||||||
const primaryAgent = visibleAgents[0];
|
const primaryAgent = visibleAgents[0];
|
||||||
if (primaryAgent?.cwd?.trim()) {
|
if (primaryAgent?.cwd?.trim()) {
|
||||||
router.replace(
|
router.replace(
|
||||||
buildHostWorkspaceAgentRoute(
|
buildHostWorkspaceAgentTabRoute(
|
||||||
serverId,
|
serverId,
|
||||||
primaryAgent.cwd.trim(),
|
primaryAgent.cwd.trim(),
|
||||||
primaryAgent.id
|
primaryAgent.id
|
||||||
|
|||||||
+4
-4
@@ -1,20 +1,20 @@
|
|||||||
import { useLocalSearchParams } from "expo-router";
|
import { useLocalSearchParams } from "expo-router";
|
||||||
import { WorkspaceScreen } from "@/screens/workspace/workspace-screen";
|
import { WorkspaceScreen } from "@/screens/workspace/workspace-screen";
|
||||||
|
|
||||||
export default function HostWorkspaceAgentRoute() {
|
export default function HostWorkspaceLayout() {
|
||||||
const params = useLocalSearchParams<{
|
const params = useLocalSearchParams<{
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
workspaceId?: string;
|
workspaceId?: string;
|
||||||
agentId?: string;
|
tabId?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const agentId = typeof params.agentId === "string" ? params.agentId : "";
|
const tabId = typeof params.tabId === "string" ? params.tabId : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkspaceScreen
|
<WorkspaceScreen
|
||||||
serverId={typeof params.serverId === "string" ? params.serverId : ""}
|
serverId={typeof params.serverId === "string" ? params.serverId : ""}
|
||||||
workspaceId={typeof params.workspaceId === "string" ? params.workspaceId : ""}
|
workspaceId={typeof params.workspaceId === "string" ? params.workspaceId : ""}
|
||||||
routeTab={agentId ? { kind: "agent", agentId } : null}
|
routeTabId={tabId || null}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { useLocalSearchParams } from "expo-router";
|
|
||||||
import { WorkspaceScreen } from "@/screens/workspace/workspace-screen";
|
|
||||||
import { decodeFilePathFromPathSegment } from "@/utils/host-routes";
|
|
||||||
|
|
||||||
export default function HostWorkspaceFileRoute() {
|
|
||||||
const params = useLocalSearchParams<{
|
|
||||||
serverId?: string;
|
|
||||||
workspaceId?: string;
|
|
||||||
fileId?: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const fileId = typeof params.fileId === "string" ? params.fileId : "";
|
|
||||||
const filePath = fileId ? decodeFilePathFromPathSegment(fileId) ?? "" : "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WorkspaceScreen
|
|
||||||
serverId={typeof params.serverId === "string" ? params.serverId : ""}
|
|
||||||
workspaceId={typeof params.workspaceId === "string" ? params.workspaceId : ""}
|
|
||||||
routeTab={filePath ? { kind: "file", path: filePath } : null}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,14 +1,3 @@
|
|||||||
import { useLocalSearchParams } from "expo-router";
|
|
||||||
import { WorkspaceScreen } from "@/screens/workspace/workspace-screen";
|
|
||||||
|
|
||||||
export default function HostWorkspaceIndexRoute() {
|
export default function HostWorkspaceIndexRoute() {
|
||||||
const params = useLocalSearchParams<{ serverId?: string; workspaceId?: string }>();
|
return null;
|
||||||
|
|
||||||
return (
|
|
||||||
<WorkspaceScreen
|
|
||||||
serverId={typeof params.serverId === "string" ? params.serverId : ""}
|
|
||||||
workspaceId={typeof params.workspaceId === "string" ? params.workspaceId : ""}
|
|
||||||
routeTab={null}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,3 @@
|
|||||||
import { useLocalSearchParams } from "expo-router";
|
|
||||||
import { WorkspaceScreen } from "@/screens/workspace/workspace-screen";
|
|
||||||
|
|
||||||
export default function HostWorkspaceTabRoute() {
|
export default function HostWorkspaceTabRoute() {
|
||||||
const params = useLocalSearchParams<{
|
return null;
|
||||||
serverId?: string;
|
|
||||||
workspaceId?: string;
|
|
||||||
tabId?: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const tabId = typeof params.tabId === "string" ? params.tabId : "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WorkspaceScreen
|
|
||||||
serverId={typeof params.serverId === "string" ? params.serverId : ""}
|
|
||||||
workspaceId={typeof params.workspaceId === "string" ? params.workspaceId : ""}
|
|
||||||
routeTab={null}
|
|
||||||
routeTabId={tabId || null}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import { useLocalSearchParams } from "expo-router";
|
|
||||||
import { WorkspaceScreen } from "@/screens/workspace/workspace-screen";
|
|
||||||
|
|
||||||
export default function HostWorkspaceTerminalRoute() {
|
|
||||||
const params = useLocalSearchParams<{
|
|
||||||
serverId?: string;
|
|
||||||
workspaceId?: string;
|
|
||||||
terminalId?: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const terminalId =
|
|
||||||
typeof params.terminalId === "string" ? params.terminalId : "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WorkspaceScreen
|
|
||||||
serverId={typeof params.serverId === "string" ? params.serverId : ""}
|
|
||||||
workspaceId={typeof params.workspaceId === "string" ? params.workspaceId : ""}
|
|
||||||
routeTab={terminalId ? { kind: "terminal", terminalId } : null}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -82,7 +82,7 @@ import { createMarkdownStyles } from "@/styles/markdown-styles";
|
|||||||
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
|
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
|
||||||
import { isPerfLoggingEnabled, measurePayload, perfLog } from "@/utils/perf";
|
import { isPerfLoggingEnabled, measurePayload, perfLog } from "@/utils/perf";
|
||||||
import { getMarkdownListMarker } from "@/utils/markdown-list";
|
import { getMarkdownListMarker } from "@/utils/markdown-list";
|
||||||
import { buildHostWorkspaceFileRoute } from "@/utils/host-routes";
|
import { buildHostWorkspaceFileTabRoute } from "@/utils/host-routes";
|
||||||
|
|
||||||
const isUserMessageItem = (item?: StreamItem) => item?.kind === "user_message";
|
const isUserMessageItem = (item?: StreamItem) => item?.kind === "user_message";
|
||||||
const isToolSequenceItem = (item?: StreamItem) =>
|
const isToolSequenceItem = (item?: StreamItem) =>
|
||||||
@@ -213,7 +213,7 @@ export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamView
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (normalized.file) {
|
if (normalized.file) {
|
||||||
const route = buildHostWorkspaceFileRoute(
|
const route = buildHostWorkspaceFileTabRoute(
|
||||||
resolvedServerId,
|
resolvedServerId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
normalized.file
|
normalized.file
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ import { getIsTauri } from '@/constants/layout'
|
|||||||
import { projectIconQueryKey } from '@/hooks/use-project-icon-query'
|
import { projectIconQueryKey } from '@/hooks/use-project-icon-query'
|
||||||
import {
|
import {
|
||||||
buildHostWorkspaceRoute,
|
buildHostWorkspaceRoute,
|
||||||
parseHostWorkspaceAgentRouteFromPathname,
|
|
||||||
parseHostWorkspaceRouteFromPathname,
|
parseHostWorkspaceRouteFromPathname,
|
||||||
parseHostWorkspaceTerminalRouteFromPathname,
|
|
||||||
} from '@/utils/host-routes'
|
} from '@/utils/host-routes'
|
||||||
import {
|
import {
|
||||||
type SidebarProjectEntry,
|
type SidebarProjectEntry,
|
||||||
@@ -516,10 +514,7 @@ export function SidebarAgentList({
|
|||||||
if (!pathname) {
|
if (!pathname) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const parsed =
|
const parsed = parseHostWorkspaceRouteFromPathname(pathname)
|
||||||
parseHostWorkspaceAgentRouteFromPathname(pathname) ??
|
|
||||||
parseHostWorkspaceTerminalRouteFromPathname(pathname) ??
|
|
||||||
parseHostWorkspaceRouteFromPathname(pathname)
|
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ describe("keyboard-shortcut-routing", () => {
|
|||||||
it("allows the shortcut on selected-agent routes", () => {
|
it("allows the shortcut on selected-agent routes", () => {
|
||||||
const canToggle = canToggleFileExplorerShortcut({
|
const canToggle = canToggleFileExplorerShortcut({
|
||||||
selectedAgentId: "server-1:agent-1",
|
selectedAgentId: "server-1:agent-1",
|
||||||
pathname: "/h/server-1/workspace/workspace-1/agent/agent-1",
|
pathname: "/h/server-1/workspace/workspace-1/tab/agent_agent-1",
|
||||||
toggleFileExplorer,
|
toggleFileExplorer,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ describe("keyboard-shortcut-routing", () => {
|
|||||||
|
|
||||||
it("allows the shortcut on workspace terminal routes", () => {
|
it("allows the shortcut on workspace terminal routes", () => {
|
||||||
const canToggle = canToggleFileExplorerShortcut({
|
const canToggle = canToggleFileExplorerShortcut({
|
||||||
pathname: "/h/server-1/workspace/workspace-1/terminal/terminal-1",
|
pathname: "/h/server-1/workspace/workspace-1/tab/terminal_terminal-1",
|
||||||
toggleFileExplorer,
|
toggleFileExplorer,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
parseHostAgentDraftRouteFromPathname,
|
parseHostAgentDraftRouteFromPathname,
|
||||||
parseHostAgentRouteFromPathname,
|
parseHostAgentRouteFromPathname,
|
||||||
parseHostWorkspaceAgentRouteFromPathname,
|
|
||||||
parseHostWorkspaceRouteFromPathname,
|
parseHostWorkspaceRouteFromPathname,
|
||||||
parseHostWorkspaceTabRouteFromPathname,
|
parseHostWorkspaceTabRouteFromPathname,
|
||||||
} from "@/utils/host-routes";
|
} from "@/utils/host-routes";
|
||||||
@@ -28,9 +27,7 @@ export function resolveSelectedOrRouteAgentKey(input: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const route =
|
const route = parseHostAgentRouteFromPathname(input.pathname);
|
||||||
parseHostWorkspaceAgentRouteFromPathname(input.pathname) ??
|
|
||||||
parseHostAgentRouteFromPathname(input.pathname);
|
|
||||||
if (!route) {
|
if (!route) {
|
||||||
const draftRoute = parseHostAgentDraftRouteFromPathname(input.pathname);
|
const draftRoute = parseHostAgentDraftRouteFromPathname(input.pathname);
|
||||||
if (!draftRoute) {
|
if (!draftRoute) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useMemo, useState } from "react";
|
import { useMemo } from "react";
|
||||||
import type { LayoutChangeEvent } from "react-native";
|
import { useWindowDimensions } from "react-native";
|
||||||
import {
|
import {
|
||||||
computeWorkspaceTabLayout,
|
computeWorkspaceTabLayout,
|
||||||
type WorkspaceTabLayoutResult,
|
type WorkspaceTabLayoutResult,
|
||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
type UseWorkspaceTabLayoutInput = {
|
type UseWorkspaceTabLayoutInput = {
|
||||||
tabLabels: string[];
|
tabLabels: string[];
|
||||||
metrics: {
|
metrics: {
|
||||||
|
rowHorizontalInset: number;
|
||||||
|
actionsReservedWidth: number;
|
||||||
rowPaddingHorizontal: number;
|
rowPaddingHorizontal: number;
|
||||||
tabGap: number;
|
tabGap: number;
|
||||||
maxTabWidth: number;
|
maxTabWidth: number;
|
||||||
@@ -20,36 +22,22 @@ type UseWorkspaceTabLayoutInput = {
|
|||||||
|
|
||||||
type UseWorkspaceTabLayoutResult = {
|
type UseWorkspaceTabLayoutResult = {
|
||||||
layout: WorkspaceTabLayoutResult;
|
layout: WorkspaceTabLayoutResult;
|
||||||
onContainerLayout: (event: LayoutChangeEvent) => void;
|
|
||||||
onActionsLayout: (event: LayoutChangeEvent) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useWorkspaceTabLayout(input: UseWorkspaceTabLayoutInput): UseWorkspaceTabLayoutResult {
|
export function useWorkspaceTabLayout(input: UseWorkspaceTabLayoutInput): UseWorkspaceTabLayoutResult {
|
||||||
const [containerWidth, setContainerWidth] = useState(0);
|
const { width: viewportWidth } = useWindowDimensions();
|
||||||
const [actionsWidth, setActionsWidth] = useState(0);
|
|
||||||
|
|
||||||
const onContainerLayout = useCallback((event: LayoutChangeEvent) => {
|
|
||||||
setContainerWidth(event.nativeEvent.layout.width);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onActionsLayout = useCallback((event: LayoutChangeEvent) => {
|
|
||||||
setActionsWidth(event.nativeEvent.layout.width);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const layout = useMemo(
|
const layout = useMemo(
|
||||||
() =>
|
() =>
|
||||||
computeWorkspaceTabLayout({
|
computeWorkspaceTabLayout({
|
||||||
containerWidth,
|
viewportWidth,
|
||||||
actionsWidth,
|
|
||||||
tabLabelLengths: input.tabLabels.map((label) => label.length),
|
tabLabelLengths: input.tabLabels.map((label) => label.length),
|
||||||
metrics: input.metrics,
|
metrics: input.metrics,
|
||||||
}),
|
}),
|
||||||
[actionsWidth, containerWidth, input.metrics, input.tabLabels]
|
[input.metrics, input.tabLabels, viewportWidth]
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
layout,
|
layout,
|
||||||
onContainerLayout,
|
|
||||||
onActionsLayout,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
|
|
||||||
const layoutMetrics = useMemo(
|
const layoutMetrics = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
rowHorizontalInset: 0,
|
||||||
|
actionsReservedWidth: 120,
|
||||||
rowPaddingHorizontal: theme.spacing[2],
|
rowPaddingHorizontal: theme.spacing[2],
|
||||||
tabGap: theme.spacing[1],
|
tabGap: theme.spacing[1],
|
||||||
maxTabWidth: 260,
|
maxTabWidth: 260,
|
||||||
@@ -87,13 +89,13 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
[theme.spacing]
|
[theme.spacing]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { layout, onContainerLayout, onActionsLayout } = useWorkspaceTabLayout({
|
const { layout } = useWorkspaceTabLayout({
|
||||||
tabLabels: tabs.map((tab) => tab.label),
|
tabLabels: tabs.map((tab) => tab.label),
|
||||||
metrics: layoutMetrics,
|
metrics: layoutMetrics,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.tabsContainer} testID="workspace-tabs-row" onLayout={onContainerLayout}>
|
<View style={styles.tabsContainer} testID="workspace-tabs-row">
|
||||||
<ScrollView
|
<ScrollView
|
||||||
horizontal
|
horizontal
|
||||||
scrollEnabled={layout.shouldScroll}
|
scrollEnabled={layout.shouldScroll}
|
||||||
@@ -304,7 +306,7 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<View style={styles.tabsActions} onLayout={onActionsLayout}>
|
<View style={styles.tabsActions}>
|
||||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
testID="workspace-new-agent-tab"
|
testID="workspace-new-agent-tab"
|
||||||
|
|||||||
@@ -59,10 +59,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
buildHostAgentDetailRoute,
|
buildHostAgentDetailRoute,
|
||||||
buildHostWorkspaceRoute,
|
buildHostWorkspaceRoute,
|
||||||
buildHostWorkspaceAgentRoute,
|
|
||||||
buildHostWorkspaceFileRoute,
|
|
||||||
buildHostWorkspaceTabRoute,
|
buildHostWorkspaceTabRoute,
|
||||||
buildHostWorkspaceTerminalRoute,
|
|
||||||
decodeWorkspaceIdFromPathSegment,
|
decodeWorkspaceIdFromPathSegment,
|
||||||
} from "@/utils/host-routes";
|
} from "@/utils/host-routes";
|
||||||
import { useHostRuntimeSession } from "@/runtime/host-runtime";
|
import { useHostRuntimeSession } from "@/runtime/host-runtime";
|
||||||
@@ -98,12 +95,9 @@ const EMPTY_WORKSPACE_TABS: WorkspaceTab[] = [];
|
|||||||
|
|
||||||
type TabAvailability = "available" | "invalid" | "unknown";
|
type TabAvailability = "available" | "invalid" | "unknown";
|
||||||
|
|
||||||
type RouteTabTarget = WorkspaceTabTarget | null;
|
|
||||||
|
|
||||||
type WorkspaceScreenProps = {
|
type WorkspaceScreenProps = {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
routeTab: RouteTabTarget;
|
|
||||||
routeTabId?: string | null;
|
routeTabId?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -189,98 +183,6 @@ function formatProviderLabel(provider: Agent["provider"]): string {
|
|||||||
return provider.charAt(0).toUpperCase() + provider.slice(1);
|
return provider.charAt(0).toUpperCase() + provider.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeWorkspaceTab(
|
|
||||||
value: WorkspaceTabTarget | null | undefined
|
|
||||||
): WorkspaceTabTarget | null {
|
|
||||||
if (!value || typeof value !== "object") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (value.kind === "draft") {
|
|
||||||
const draftId = trimNonEmpty(decodeSegment(value.draftId));
|
|
||||||
if (!draftId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return { kind: "draft", draftId };
|
|
||||||
}
|
|
||||||
if (value.kind === "agent") {
|
|
||||||
const agentId = trimNonEmpty(decodeSegment(value.agentId));
|
|
||||||
if (!agentId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return { kind: "agent", agentId };
|
|
||||||
}
|
|
||||||
if (value.kind === "terminal") {
|
|
||||||
const terminalId = trimNonEmpty(decodeSegment(value.terminalId));
|
|
||||||
if (!terminalId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return { kind: "terminal", terminalId };
|
|
||||||
}
|
|
||||||
if (value.kind === "file") {
|
|
||||||
const path = trimNonEmpty(value.path);
|
|
||||||
if (!path) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return { kind: "file", path: path.replace(/\\/g, "/") };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function tabEquals(left: WorkspaceTabTarget | null, right: WorkspaceTabTarget | null): boolean {
|
|
||||||
if (!left || !right) {
|
|
||||||
return left === right;
|
|
||||||
}
|
|
||||||
if (left.kind !== right.kind) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (left.kind === "draft" && right.kind === "draft") {
|
|
||||||
return left.draftId === right.draftId;
|
|
||||||
}
|
|
||||||
if (left.kind === "agent" && right.kind === "agent") {
|
|
||||||
return left.agentId === right.agentId;
|
|
||||||
}
|
|
||||||
if (left.kind === "terminal" && right.kind === "terminal") {
|
|
||||||
return left.terminalId === right.terminalId;
|
|
||||||
}
|
|
||||||
if (left.kind === "file" && right.kind === "file") {
|
|
||||||
return left.path === right.path;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildTabRoute(input: {
|
|
||||||
serverId: string;
|
|
||||||
workspaceId: string;
|
|
||||||
tab: WorkspaceTabTarget;
|
|
||||||
}): string {
|
|
||||||
if (input.tab.kind === "draft") {
|
|
||||||
return buildHostWorkspaceTabRoute(
|
|
||||||
input.serverId,
|
|
||||||
input.workspaceId,
|
|
||||||
input.tab.draftId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (input.tab.kind === "agent") {
|
|
||||||
return buildHostWorkspaceAgentRoute(
|
|
||||||
input.serverId,
|
|
||||||
input.workspaceId,
|
|
||||||
input.tab.agentId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (input.tab.kind === "file") {
|
|
||||||
return buildHostWorkspaceFileRoute(
|
|
||||||
input.serverId,
|
|
||||||
input.workspaceId,
|
|
||||||
input.tab.path
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return buildHostWorkspaceTerminalRoute(
|
|
||||||
input.serverId,
|
|
||||||
input.workspaceId,
|
|
||||||
input.tab.terminalId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveTabAvailability(input: {
|
function resolveTabAvailability(input: {
|
||||||
tab: WorkspaceTabTarget;
|
tab: WorkspaceTabTarget;
|
||||||
agentsHydrated: boolean;
|
agentsHydrated: boolean;
|
||||||
@@ -317,25 +219,9 @@ function sortAgentsByCreatedAtDescending(agents: Agent[]): Agent[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function toWorkspaceTabTarget(
|
|
||||||
tab: WorkspaceTabDescriptor
|
|
||||||
): WorkspaceTabTarget {
|
|
||||||
if (tab.kind === "draft") {
|
|
||||||
return { kind: "draft", draftId: tab.draftId };
|
|
||||||
}
|
|
||||||
if (tab.kind === "agent") {
|
|
||||||
return { kind: "agent", agentId: tab.agentId };
|
|
||||||
}
|
|
||||||
if (tab.kind === "file") {
|
|
||||||
return { kind: "file", path: tab.filePath };
|
|
||||||
}
|
|
||||||
return { kind: "terminal", terminalId: tab.terminalId };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function WorkspaceScreen({
|
export function WorkspaceScreen({
|
||||||
serverId,
|
serverId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
routeTab,
|
|
||||||
routeTabId,
|
routeTabId,
|
||||||
}: WorkspaceScreenProps) {
|
}: WorkspaceScreenProps) {
|
||||||
return (
|
return (
|
||||||
@@ -343,7 +229,6 @@ export function WorkspaceScreen({
|
|||||||
<WorkspaceScreenContent
|
<WorkspaceScreenContent
|
||||||
serverId={serverId}
|
serverId={serverId}
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
routeTab={routeTab}
|
|
||||||
routeTabId={routeTabId}
|
routeTabId={routeTabId}
|
||||||
/>
|
/>
|
||||||
</ExplorerSidebarAnimationProvider>
|
</ExplorerSidebarAnimationProvider>
|
||||||
@@ -353,7 +238,6 @@ export function WorkspaceScreen({
|
|||||||
function WorkspaceScreenContent({
|
function WorkspaceScreenContent({
|
||||||
serverId,
|
serverId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
routeTab,
|
|
||||||
routeTabId,
|
routeTabId,
|
||||||
}: WorkspaceScreenProps) {
|
}: WorkspaceScreenProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
@@ -632,11 +516,6 @@ function WorkspaceScreenContent({
|
|||||||
return set;
|
return set;
|
||||||
}, [terminals]);
|
}, [terminals]);
|
||||||
|
|
||||||
const requestedTab = useMemo(
|
|
||||||
() => normalizeWorkspaceTab(routeTab),
|
|
||||||
[routeTab]
|
|
||||||
);
|
|
||||||
|
|
||||||
const persistenceKey = useMemo(
|
const persistenceKey = useMemo(
|
||||||
() =>
|
() =>
|
||||||
buildWorkspaceTabPersistenceKey({
|
buildWorkspaceTabPersistenceKey({
|
||||||
@@ -772,31 +651,6 @@ function WorkspaceScreenContent({
|
|||||||
routeTabId,
|
routeTabId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!requestedTab || !persistenceKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const tabId = openOrFocusTab({
|
|
||||||
serverId: normalizedServerId,
|
|
||||||
workspaceId: normalizedWorkspaceId,
|
|
||||||
target: requestedTab,
|
|
||||||
});
|
|
||||||
if (tabId) {
|
|
||||||
focusTab({
|
|
||||||
serverId: normalizedServerId,
|
|
||||||
workspaceId: normalizedWorkspaceId,
|
|
||||||
tabId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
focusTab,
|
|
||||||
normalizedServerId,
|
|
||||||
normalizedWorkspaceId,
|
|
||||||
openOrFocusTab,
|
|
||||||
persistenceKey,
|
|
||||||
requestedTab,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const activeTabId = useMemo(() => {
|
const activeTabId = useMemo(() => {
|
||||||
const fromRoute =
|
const fromRoute =
|
||||||
typeof routeTabId === "string" ? trimNonEmpty(routeTabId.trim()) : null;
|
typeof routeTabId === "string" ? trimNonEmpty(routeTabId.trim()) : null;
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { describe, expect, it } from "vitest";
|
|||||||
import { computeWorkspaceTabLayout } from "@/screens/workspace/workspace-tab-layout";
|
import { computeWorkspaceTabLayout } from "@/screens/workspace/workspace-tab-layout";
|
||||||
|
|
||||||
const metrics = {
|
const metrics = {
|
||||||
|
rowHorizontalInset: 0,
|
||||||
|
actionsReservedWidth: 120,
|
||||||
rowPaddingHorizontal: 8,
|
rowPaddingHorizontal: 8,
|
||||||
tabGap: 4,
|
tabGap: 4,
|
||||||
maxTabWidth: 260,
|
maxTabWidth: 260,
|
||||||
@@ -14,8 +16,7 @@ const metrics = {
|
|||||||
describe("computeWorkspaceTabLayout", () => {
|
describe("computeWorkspaceTabLayout", () => {
|
||||||
it("keeps full width tabs when space is available", () => {
|
it("keeps full width tabs when space is available", () => {
|
||||||
const result = computeWorkspaceTabLayout({
|
const result = computeWorkspaceTabLayout({
|
||||||
containerWidth: 1200,
|
viewportWidth: 1200,
|
||||||
actionsWidth: 100,
|
|
||||||
tabLabelLengths: [8, 10, 7],
|
tabLabelLengths: [8, 10, 7],
|
||||||
metrics,
|
metrics,
|
||||||
});
|
});
|
||||||
@@ -28,8 +29,7 @@ describe("computeWorkspaceTabLayout", () => {
|
|||||||
|
|
||||||
it("shrinks proportionally in compact mode before icon-only", () => {
|
it("shrinks proportionally in compact mode before icon-only", () => {
|
||||||
const result = computeWorkspaceTabLayout({
|
const result = computeWorkspaceTabLayout({
|
||||||
containerWidth: 500,
|
viewportWidth: 500,
|
||||||
actionsWidth: 110,
|
|
||||||
tabLabelLengths: [24, 12, 8],
|
tabLabelLengths: [24, 12, 8],
|
||||||
metrics,
|
metrics,
|
||||||
});
|
});
|
||||||
@@ -43,8 +43,7 @@ describe("computeWorkspaceTabLayout", () => {
|
|||||||
|
|
||||||
it("falls back to icon mode when compact labels still cannot fit", () => {
|
it("falls back to icon mode when compact labels still cannot fit", () => {
|
||||||
const result = computeWorkspaceTabLayout({
|
const result = computeWorkspaceTabLayout({
|
||||||
containerWidth: 300,
|
viewportWidth: 300,
|
||||||
actionsWidth: 120,
|
|
||||||
tabLabelLengths: [14, 14, 14, 14],
|
tabLabelLengths: [14, 14, 14, 14],
|
||||||
metrics,
|
metrics,
|
||||||
});
|
});
|
||||||
@@ -57,8 +56,7 @@ describe("computeWorkspaceTabLayout", () => {
|
|||||||
|
|
||||||
it("keeps icon mode without scroll when icons can fit", () => {
|
it("keeps icon mode without scroll when icons can fit", () => {
|
||||||
const result = computeWorkspaceTabLayout({
|
const result = computeWorkspaceTabLayout({
|
||||||
containerWidth: 380,
|
viewportWidth: 380,
|
||||||
actionsWidth: 120,
|
|
||||||
tabLabelLengths: [20, 20, 20, 20],
|
tabLabelLengths: [20, 20, 20, 20],
|
||||||
metrics,
|
metrics,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
export type WorkspaceTabLayoutMode = "full" | "compact" | "icon";
|
export type WorkspaceTabLayoutMode = "full" | "compact" | "icon";
|
||||||
|
|
||||||
export type WorkspaceTabLayoutInput = {
|
export type WorkspaceTabLayoutInput = {
|
||||||
containerWidth: number;
|
viewportWidth: number;
|
||||||
actionsWidth: number;
|
|
||||||
tabLabelLengths: number[];
|
tabLabelLengths: number[];
|
||||||
metrics: {
|
metrics: {
|
||||||
|
rowHorizontalInset: number;
|
||||||
|
actionsReservedWidth: number;
|
||||||
rowPaddingHorizontal: number;
|
rowPaddingHorizontal: number;
|
||||||
tabGap: number;
|
tabGap: number;
|
||||||
maxTabWidth: number;
|
maxTabWidth: number;
|
||||||
@@ -70,7 +71,10 @@ export function computeWorkspaceTabLayout(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableWidth = Math.max(0, input.containerWidth - input.actionsWidth);
|
const availableWidth = Math.max(
|
||||||
|
0,
|
||||||
|
input.viewportWidth - input.metrics.rowHorizontalInset * 2 - input.metrics.actionsReservedWidth
|
||||||
|
);
|
||||||
const minLabelWidth =
|
const minLabelWidth =
|
||||||
input.metrics.tabBaseWidthWithClose + input.metrics.charWidth * input.metrics.minLabelChars;
|
input.metrics.tabBaseWidthWithClose + input.metrics.charWidth * input.metrics.minLabelChars;
|
||||||
|
|
||||||
|
|||||||
@@ -128,7 +128,10 @@ function createControllerHarness(input?: {
|
|||||||
client,
|
client,
|
||||||
getPreferredSize: () => ({ rows: 24, cols: 80 }),
|
getPreferredSize: () => ({ rows: 24, cols: 80 }),
|
||||||
onChunk: (chunk) => {
|
onChunk: (chunk) => {
|
||||||
chunks.push(chunk);
|
chunks.push({
|
||||||
|
terminalId: chunk.terminalId,
|
||||||
|
text: chunk.text,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onStatusChange: (status) => {
|
onStatusChange: (status) => {
|
||||||
statuses.push(status);
|
statuses.push(status);
|
||||||
@@ -212,6 +215,7 @@ describe("terminal-stream-controller", () => {
|
|||||||
|
|
||||||
expect(harness.client.attachCalls.length).toBe(2);
|
expect(harness.client.attachCalls.length).toBe(2);
|
||||||
expect(harness.client.attachCalls[1]?.options).toEqual({
|
expect(harness.client.attachCalls[1]?.options).toEqual({
|
||||||
|
resumeOffset: 0,
|
||||||
rows: 24,
|
rows: 24,
|
||||||
cols: 80,
|
cols: 80,
|
||||||
});
|
});
|
||||||
@@ -268,6 +272,38 @@ describe("terminal-stream-controller", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("recovers when stream exits before initial attach completes", async () => {
|
||||||
|
const harness = createControllerHarness();
|
||||||
|
harness.client.nextAttachResponses.push({
|
||||||
|
streamId: 1,
|
||||||
|
currentOffset: 1200,
|
||||||
|
reset: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
harness.client.nextAttachResponses.push({
|
||||||
|
streamId: 2,
|
||||||
|
currentOffset: 1200,
|
||||||
|
reset: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
harness.controller.setTerminal({ terminalId: "term-attach-exit" });
|
||||||
|
harness.controller.handleStreamExit({
|
||||||
|
terminalId: "term-attach-exit",
|
||||||
|
streamId: 1,
|
||||||
|
});
|
||||||
|
await flushAsyncWork();
|
||||||
|
|
||||||
|
expect(harness.client.attachCalls).toHaveLength(2);
|
||||||
|
expect(harness.client.attachCalls[1]?.options).toEqual({
|
||||||
|
resumeOffset: 1200,
|
||||||
|
rows: 24,
|
||||||
|
cols: 80,
|
||||||
|
});
|
||||||
|
expect(harness.client.detachCalls).toContain(1);
|
||||||
|
expect(harness.controller.getActiveStreamId()).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
it("emits reset callback when attach indicates output reset", async () => {
|
it("emits reset callback when attach indicates output reset", async () => {
|
||||||
const harness = createControllerHarness();
|
const harness = createControllerHarness();
|
||||||
harness.client.nextAttachResponses.push({
|
harness.client.nextAttachResponses.push({
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ const DEFAULT_RECONNECT_ERROR_MESSAGE = "Terminal stream ended. Reconnecting…"
|
|||||||
|
|
||||||
export class TerminalStreamController {
|
export class TerminalStreamController {
|
||||||
private readonly resumeOffsetByTerminalId = new Map<string, number>();
|
private readonly resumeOffsetByTerminalId = new Map<string, number>();
|
||||||
|
private readonly exitedStreamsWhileAttaching = new Set<number>();
|
||||||
private selectedTerminalId: string | null = null;
|
private selectedTerminalId: string | null = null;
|
||||||
private activeStream: TerminalStreamControllerActiveStream | null = null;
|
private activeStream: TerminalStreamControllerActiveStream | null = null;
|
||||||
private attachGeneration = 0;
|
private attachGeneration = 0;
|
||||||
@@ -167,31 +168,36 @@ export class TerminalStreamController {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const activeStream = this.activeStream;
|
|
||||||
if (!activeStream) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (activeStream.terminalId !== input.terminalId || activeStream.streamId !== input.streamId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.selectedTerminalId !== input.terminalId) {
|
if (this.selectedTerminalId !== input.terminalId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.attachGeneration += 1;
|
const activeStream = this.activeStream;
|
||||||
const generation = this.attachGeneration;
|
const activeMatches =
|
||||||
void this.detachActiveStream({ shouldDetach: false });
|
Boolean(activeStream) &&
|
||||||
this.updateStatus({
|
activeStream!.terminalId === input.terminalId &&
|
||||||
terminalId: input.terminalId,
|
activeStream!.streamId === input.streamId;
|
||||||
streamId: null,
|
if (activeMatches) {
|
||||||
isAttaching: true,
|
this.attachGeneration += 1;
|
||||||
error:
|
const generation = this.attachGeneration;
|
||||||
this.options.reconnectErrorMessage ?? DEFAULT_RECONNECT_ERROR_MESSAGE,
|
void this.detachActiveStream({ shouldDetach: false });
|
||||||
});
|
this.updateStatus({
|
||||||
void this.attachTerminal({
|
terminalId: input.terminalId,
|
||||||
terminalId: input.terminalId,
|
streamId: null,
|
||||||
generation,
|
isAttaching: true,
|
||||||
});
|
error:
|
||||||
|
this.options.reconnectErrorMessage ?? DEFAULT_RECONNECT_ERROR_MESSAGE,
|
||||||
|
});
|
||||||
|
void this.attachTerminal({
|
||||||
|
terminalId: input.terminalId,
|
||||||
|
generation,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.status.isAttaching && this.status.terminalId === input.terminalId) {
|
||||||
|
this.exitedStreamsWhileAttaching.add(input.streamId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pruneResumeOffsets(input: { terminalIds: string[] }): void {
|
pruneResumeOffsets(input: { terminalIds: string[] }): void {
|
||||||
@@ -267,6 +273,12 @@ export class TerminalStreamController {
|
|||||||
timeoutMessage: "Timed out attaching terminal stream",
|
timeoutMessage: "Timed out attaching terminal stream",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updateTerminalResumeOffset({
|
||||||
|
terminalId: input.terminalId,
|
||||||
|
offset: attachPayload.currentOffset,
|
||||||
|
resumeOffsetByTerminalId: this.resumeOffsetByTerminalId,
|
||||||
|
});
|
||||||
|
|
||||||
if (!this.isAttachGenerationCurrent({ generation: input.generation, terminalId: input.terminalId })) {
|
if (!this.isAttachGenerationCurrent({ generation: input.generation, terminalId: input.terminalId })) {
|
||||||
if (typeof attachPayload.streamId === "number") {
|
if (typeof attachPayload.streamId === "number") {
|
||||||
void this.options.client.detachTerminalStream(attachPayload.streamId).catch(() => {});
|
void this.options.client.detachTerminalStream(attachPayload.streamId).catch(() => {});
|
||||||
@@ -300,17 +312,22 @@ export class TerminalStreamController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.exitedStreamsWhileAttaching.has(attachPayload.streamId)) {
|
||||||
|
this.exitedStreamsWhileAttaching.delete(attachPayload.streamId);
|
||||||
|
void this.options.client.detachTerminalStream(attachPayload.streamId).catch(() => {});
|
||||||
|
this.attachGeneration += 1;
|
||||||
|
void this.attachTerminal({
|
||||||
|
terminalId: input.terminalId,
|
||||||
|
generation: this.attachGeneration,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (attachPayload.reset) {
|
if (attachPayload.reset) {
|
||||||
this.resumeOffsetByTerminalId.delete(input.terminalId);
|
this.resumeOffsetByTerminalId.delete(input.terminalId);
|
||||||
this.options.onReset?.({ terminalId: input.terminalId });
|
this.options.onReset?.({ terminalId: input.terminalId });
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTerminalResumeOffset({
|
|
||||||
terminalId: input.terminalId,
|
|
||||||
offset: attachPayload.currentOffset,
|
|
||||||
resumeOffsetByTerminalId: this.resumeOffsetByTerminalId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
const streamId = attachPayload.streamId;
|
const streamId = attachPayload.streamId;
|
||||||
const replayedFromOffset =
|
const replayedFromOffset =
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
|
buildHostWorkspaceAgentTabRoute,
|
||||||
|
buildHostWorkspaceFileTabRoute,
|
||||||
buildHostWorkspaceRoute,
|
buildHostWorkspaceRoute,
|
||||||
buildHostWorkspaceFileRoute,
|
buildHostWorkspaceTerminalTabRoute,
|
||||||
decodeFilePathFromPathSegment,
|
decodeFilePathFromPathSegment,
|
||||||
decodeWorkspaceIdFromPathSegment,
|
decodeWorkspaceIdFromPathSegment,
|
||||||
encodeFilePathForPathSegment,
|
encodeFilePathForPathSegment,
|
||||||
@@ -9,10 +11,7 @@ import {
|
|||||||
parseHostAgentDraftRouteFromPathname,
|
parseHostAgentDraftRouteFromPathname,
|
||||||
parseHostAgentRouteFromPathname,
|
parseHostAgentRouteFromPathname,
|
||||||
parseHostDraftRouteFromPathname,
|
parseHostDraftRouteFromPathname,
|
||||||
parseHostWorkspaceAgentRouteFromPathname,
|
|
||||||
parseHostWorkspaceFileRouteFromPathname,
|
|
||||||
parseHostWorkspaceTabRouteFromPathname,
|
parseHostWorkspaceTabRouteFromPathname,
|
||||||
parseHostWorkspaceTerminalRouteFromPathname,
|
|
||||||
parseHostWorkspaceRouteFromPathname,
|
parseHostWorkspaceRouteFromPathname,
|
||||||
} from "./host-routes";
|
} from "./host-routes";
|
||||||
|
|
||||||
@@ -24,9 +23,7 @@ describe("parseHostAgentDraftRouteFromPathname", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("parses encoded server id", () => {
|
it("parses encoded server id", () => {
|
||||||
expect(
|
expect(parseHostAgentDraftRouteFromPathname("/h/team%20host/new")).toEqual({
|
||||||
parseHostAgentDraftRouteFromPathname("/h/team%20host/new")
|
|
||||||
).toEqual({
|
|
||||||
serverId: "team host",
|
serverId: "team host",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -66,9 +63,7 @@ describe("workspace route parsing", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("parses workspace route", () => {
|
it("parses workspace route", () => {
|
||||||
expect(
|
expect(parseHostWorkspaceRouteFromPathname("/h/local/workspace/L3RtcC9yZXBv")).toEqual({
|
||||||
parseHostWorkspaceRouteFromPathname("/h/local/workspace/L3RtcC9yZXBv")
|
|
||||||
).toEqual({
|
|
||||||
serverId: "local",
|
serverId: "local",
|
||||||
workspaceId: "/tmp/repo",
|
workspaceId: "/tmp/repo",
|
||||||
});
|
});
|
||||||
@@ -83,48 +78,9 @@ 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(
|
|
||||||
"/h/local/workspace/L3RtcC9yZXBv/agent/agent-1"
|
|
||||||
)
|
|
||||||
).toEqual({
|
|
||||||
serverId: "local",
|
|
||||||
workspaceId: "/tmp/repo",
|
|
||||||
agentId: "agent-1",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("parses workspace terminal route", () => {
|
|
||||||
expect(
|
|
||||||
parseHostWorkspaceTerminalRouteFromPathname(
|
|
||||||
"/h/local/workspace/L3RtcC9yZXBv/terminal/term-1"
|
|
||||||
)
|
|
||||||
).toEqual({
|
|
||||||
serverId: "local",
|
|
||||||
workspaceId: "/tmp/repo",
|
|
||||||
terminalId: "term-1",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("parses workspace tab route", () => {
|
it("parses workspace tab route", () => {
|
||||||
expect(
|
expect(
|
||||||
parseHostWorkspaceTabRouteFromPathname(
|
parseHostWorkspaceTabRouteFromPathname("/h/local/workspace/L3RtcC9yZXBv/tab/draft_abc123")
|
||||||
"/h/local/workspace/L3RtcC9yZXBv/tab/draft_abc123"
|
|
||||||
)
|
|
||||||
).toEqual({
|
).toEqual({
|
||||||
serverId: "local",
|
serverId: "local",
|
||||||
workspaceId: "/tmp/repo",
|
workspaceId: "/tmp/repo",
|
||||||
@@ -132,27 +88,25 @@ describe("workspace route parsing", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("still parses legacy percent-encoded workspace routes", () => {
|
it("builds base64url workspace routes", () => {
|
||||||
expect(
|
expect(buildHostWorkspaceRoute("local", "/tmp/repo")).toBe("/h/local/workspace/L3RtcC9yZXBv");
|
||||||
parseHostWorkspaceAgentRouteFromPathname(
|
|
||||||
"/h/local/workspace/%2Ftmp%2Frepo/agent/agent-1"
|
|
||||||
)
|
|
||||||
).toEqual({
|
|
||||||
serverId: "local",
|
|
||||||
workspaceId: "/tmp/repo",
|
|
||||||
agentId: "agent-1",
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds base64url workspace routes", () => {
|
it("builds workspace agent tab routes", () => {
|
||||||
expect(buildHostWorkspaceRoute("local", "/tmp/repo")).toBe(
|
expect(buildHostWorkspaceAgentTabRoute("local", "/tmp/repo", "agent-1")).toBe(
|
||||||
"/h/local/workspace/L3RtcC9yZXBv"
|
"/h/local/workspace/L3RtcC9yZXBv/tab/agent_agent-1"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds base64url workspace file routes", () => {
|
it("builds workspace terminal tab routes", () => {
|
||||||
expect(buildHostWorkspaceFileRoute("local", "/tmp/repo", "src/index.ts")).toBe(
|
expect(buildHostWorkspaceTerminalTabRoute("local", "/tmp/repo", "term-1")).toBe(
|
||||||
`/h/local/workspace/L3RtcC9yZXBv/file/${encodeFilePathForPathSegment("src/index.ts")}`
|
"/h/local/workspace/L3RtcC9yZXBv/tab/terminal_term-1"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds workspace file tab routes", () => {
|
||||||
|
expect(buildHostWorkspaceFileTabRoute("local", "/tmp/repo", "src/index.ts")).toBe(
|
||||||
|
"/h/local/workspace/L3RtcC9yZXBv/tab/file_src%2Findex.ts"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -61,6 +61,21 @@ function normalizeWorkspaceId(value: string): string {
|
|||||||
return value.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
return value.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildWorkspaceAgentTabId(agentId: string): string | null {
|
||||||
|
const normalized = trimNonEmpty(agentId);
|
||||||
|
return normalized ? `agent_${normalized}` : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWorkspaceTerminalTabId(terminalId: string): string | null {
|
||||||
|
const normalized = trimNonEmpty(terminalId);
|
||||||
|
return normalized ? `terminal_${normalized}` : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWorkspaceFileTabId(filePath: string): string | null {
|
||||||
|
const normalized = trimNonEmpty(filePath);
|
||||||
|
return normalized ? `file_${normalized.replace(/\\/g, "/")}` : null;
|
||||||
|
}
|
||||||
|
|
||||||
export function encodeWorkspaceIdForPathSegment(workspaceId: string): string {
|
export function encodeWorkspaceIdForPathSegment(workspaceId: string): string {
|
||||||
const normalized = trimNonEmpty(workspaceId);
|
const normalized = trimNonEmpty(workspaceId);
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
@@ -222,22 +237,9 @@ export function parseHostWorkspaceRouteFromPathname(
|
|||||||
|
|
||||||
const workspaceIdStart = serverIdEnd + workspacePrefix.length;
|
const workspaceIdStart = serverIdEnd + workspacePrefix.length;
|
||||||
let workspaceIdEnd = pathname.length;
|
let workspaceIdEnd = pathname.length;
|
||||||
|
|
||||||
const agentIdx = pathname.lastIndexOf("/agent/");
|
|
||||||
if (agentIdx >= 0 && agentIdx > workspaceIdStart) {
|
|
||||||
workspaceIdEnd = Math.min(workspaceIdEnd, agentIdx);
|
|
||||||
}
|
|
||||||
const terminalIdx = pathname.lastIndexOf("/terminal/");
|
|
||||||
if (terminalIdx >= 0 && terminalIdx > workspaceIdStart) {
|
|
||||||
workspaceIdEnd = Math.min(workspaceIdEnd, terminalIdx);
|
|
||||||
}
|
|
||||||
const tabIdx = pathname.lastIndexOf("/tab/");
|
const tabIdx = pathname.lastIndexOf("/tab/");
|
||||||
if (tabIdx >= 0 && tabIdx > workspaceIdStart) {
|
if (tabIdx >= 0 && tabIdx > workspaceIdStart) {
|
||||||
workspaceIdEnd = Math.min(workspaceIdEnd, tabIdx);
|
workspaceIdEnd = tabIdx;
|
||||||
}
|
|
||||||
const fileIdx = pathname.lastIndexOf("/file/");
|
|
||||||
if (fileIdx >= 0 && fileIdx > workspaceIdStart) {
|
|
||||||
workspaceIdEnd = Math.min(workspaceIdEnd, fileIdx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawWorkspaceId = pathname.slice(workspaceIdStart, workspaceIdEnd).replace(/\/+$/, "");
|
const rawWorkspaceId = pathname.slice(workspaceIdStart, workspaceIdEnd).replace(/\/+$/, "");
|
||||||
@@ -248,106 +250,6 @@ export function parseHostWorkspaceRouteFromPathname(
|
|||||||
return { serverId, workspaceId };
|
return { serverId, workspaceId };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseHostWorkspaceAgentRouteFromPathname(
|
|
||||||
pathname: string
|
|
||||||
): { serverId: string; workspaceId: string; agentId: 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 agentMarker = "/agent/";
|
|
||||||
const agentIdx = pathname.lastIndexOf(agentMarker);
|
|
||||||
if (agentIdx < 0 || agentIdx <= workspaceIdStart) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawWorkspaceId = pathname.slice(workspaceIdStart, agentIdx).replace(/\/+$/, "");
|
|
||||||
const workspaceId = decodeWorkspaceIdFromPathSegment(rawWorkspaceId);
|
|
||||||
if (!workspaceId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const agentIdStart = agentIdx + agentMarker.length;
|
|
||||||
const agentIdEnd = pathname.indexOf("/", agentIdStart);
|
|
||||||
const rawAgentId =
|
|
||||||
agentIdEnd < 0 ? pathname.slice(agentIdStart) : pathname.slice(agentIdStart, agentIdEnd);
|
|
||||||
const agentId = trimNonEmpty(decodeSegment(rawAgentId));
|
|
||||||
if (!agentId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { serverId, workspaceId, agentId };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseHostWorkspaceTerminalRouteFromPathname(
|
|
||||||
pathname: string
|
|
||||||
): { serverId: string; workspaceId: string; terminalId: 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 terminalMarker = "/terminal/";
|
|
||||||
const terminalIdx = pathname.lastIndexOf(terminalMarker);
|
|
||||||
if (terminalIdx < 0 || terminalIdx <= workspaceIdStart) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawWorkspaceId = pathname.slice(workspaceIdStart, terminalIdx).replace(/\/+$/, "");
|
|
||||||
const workspaceId = decodeWorkspaceIdFromPathSegment(rawWorkspaceId);
|
|
||||||
if (!workspaceId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const terminalIdStart = terminalIdx + terminalMarker.length;
|
|
||||||
const terminalIdEnd = pathname.indexOf("/", terminalIdStart);
|
|
||||||
const rawTerminalId =
|
|
||||||
terminalIdEnd < 0
|
|
||||||
? pathname.slice(terminalIdStart)
|
|
||||||
: pathname.slice(terminalIdStart, terminalIdEnd);
|
|
||||||
const terminalId = trimNonEmpty(decodeSegment(rawTerminalId));
|
|
||||||
if (!terminalId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { serverId, workspaceId, terminalId };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseHostWorkspaceTabRouteFromPathname(
|
export function parseHostWorkspaceTabRouteFromPathname(
|
||||||
pathname: string
|
pathname: string
|
||||||
): { serverId: string; workspaceId: string; tabId: string } | null {
|
): { serverId: string; workspaceId: string; tabId: string } | null {
|
||||||
@@ -397,55 +299,6 @@ export function parseHostWorkspaceTabRouteFromPathname(
|
|||||||
return { serverId, workspaceId, tabId };
|
return { serverId, workspaceId, tabId };
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
export function buildHostWorkspaceRoute(
|
||||||
serverId: string,
|
serverId: string,
|
||||||
workspaceId: string
|
workspaceId: string
|
||||||
@@ -462,30 +315,28 @@ export function buildHostWorkspaceRoute(
|
|||||||
return `/h/${encodeSegment(normalizedServerId)}/workspace/${encodeSegment(encodedWorkspaceId)}`;
|
return `/h/${encodeSegment(normalizedServerId)}/workspace/${encodeSegment(encodedWorkspaceId)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildHostWorkspaceAgentRoute(
|
export function buildHostWorkspaceAgentTabRoute(
|
||||||
serverId: string,
|
serverId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
agentId: string
|
agentId: string
|
||||||
): string {
|
): string {
|
||||||
const base = buildHostWorkspaceRoute(serverId, workspaceId);
|
const tabId = buildWorkspaceAgentTabId(agentId);
|
||||||
const normalizedAgentId = trimNonEmpty(agentId);
|
if (!tabId) {
|
||||||
if (base === "/" || !normalizedAgentId) {
|
|
||||||
return "/";
|
return "/";
|
||||||
}
|
}
|
||||||
return `${base}/agent/${encodeSegment(normalizedAgentId)}`;
|
return buildHostWorkspaceTabRoute(serverId, workspaceId, tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildHostWorkspaceTerminalRoute(
|
export function buildHostWorkspaceTerminalTabRoute(
|
||||||
serverId: string,
|
serverId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
terminalId: string
|
terminalId: string
|
||||||
): string {
|
): string {
|
||||||
const base = buildHostWorkspaceRoute(serverId, workspaceId);
|
const tabId = buildWorkspaceTerminalTabId(terminalId);
|
||||||
const normalizedTerminalId = trimNonEmpty(terminalId);
|
if (!tabId) {
|
||||||
if (base === "/" || !normalizedTerminalId) {
|
|
||||||
return "/";
|
return "/";
|
||||||
}
|
}
|
||||||
return `${base}/terminal/${encodeSegment(normalizedTerminalId)}`;
|
return buildHostWorkspaceTabRoute(serverId, workspaceId, tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildHostWorkspaceTabRoute(
|
export function buildHostWorkspaceTabRoute(
|
||||||
@@ -501,17 +352,16 @@ export function buildHostWorkspaceTabRoute(
|
|||||||
return `${base}/tab/${encodeSegment(normalizedTabId)}`;
|
return `${base}/tab/${encodeSegment(normalizedTabId)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildHostWorkspaceFileRoute(
|
export function buildHostWorkspaceFileTabRoute(
|
||||||
serverId: string,
|
serverId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
filePath: string
|
filePath: string
|
||||||
): string {
|
): string {
|
||||||
const base = buildHostWorkspaceRoute(serverId, workspaceId);
|
const tabId = buildWorkspaceFileTabId(filePath);
|
||||||
const encodedFilePath = encodeFilePathForPathSegment(filePath);
|
if (!tabId) {
|
||||||
if (base === "/" || !encodedFilePath) {
|
|
||||||
return "/";
|
return "/";
|
||||||
}
|
}
|
||||||
return `${base}/file/${encodeSegment(encodedFilePath)}`;
|
return buildHostWorkspaceTabRoute(serverId, workspaceId, tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildHostAgentDetailRoute(
|
export function buildHostAgentDetailRoute(
|
||||||
@@ -521,7 +371,7 @@ export function buildHostAgentDetailRoute(
|
|||||||
): string {
|
): string {
|
||||||
const normalizedWorkspaceId = trimNonEmpty(workspaceId);
|
const normalizedWorkspaceId = trimNonEmpty(workspaceId);
|
||||||
if (normalizedWorkspaceId) {
|
if (normalizedWorkspaceId) {
|
||||||
return buildHostWorkspaceAgentRoute(
|
return buildHostWorkspaceAgentTabRoute(
|
||||||
serverId,
|
serverId,
|
||||||
normalizedWorkspaceId,
|
normalizedWorkspaceId,
|
||||||
agentId
|
agentId
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ describe("resolveSelectedAgentForNewAgent", () => {
|
|||||||
it("prefers the agent in the current route", () => {
|
it("prefers the agent in the current route", () => {
|
||||||
expect(
|
expect(
|
||||||
resolveSelectedAgentForNewAgent({
|
resolveSelectedAgentForNewAgent({
|
||||||
pathname: "/h/srv-1/workspace/%2Frepo/agent/agent-2",
|
pathname: "/h/srv-1/workspace/%2Frepo/tab/agent_agent-2",
|
||||||
selectedAgentId: "srv-9:agent-9",
|
selectedAgentId: "srv-9:agent-9",
|
||||||
})
|
})
|
||||||
).toEqual({
|
).toEqual({
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { CheckoutStatusPayload } from "@/hooks/use-checkout-status-query";
|
|||||||
import {
|
import {
|
||||||
buildHostAgentDraftRoute,
|
buildHostAgentDraftRoute,
|
||||||
parseHostAgentRouteFromPathname,
|
parseHostAgentRouteFromPathname,
|
||||||
parseHostWorkspaceAgentRouteFromPathname,
|
parseHostWorkspaceTabRouteFromPathname,
|
||||||
} from "@/utils/host-routes";
|
} from "@/utils/host-routes";
|
||||||
|
|
||||||
export function parseAgentKey(
|
export function parseAgentKey(
|
||||||
@@ -27,9 +27,12 @@ export function resolveSelectedAgentForNewAgent(input: {
|
|||||||
pathname: string;
|
pathname: string;
|
||||||
selectedAgentId?: string;
|
selectedAgentId?: string;
|
||||||
}): { serverId: string; agentId: string } | null {
|
}): { serverId: string; agentId: string } | null {
|
||||||
const workspaceRoute = parseHostWorkspaceAgentRouteFromPathname(input.pathname);
|
const workspaceTabRoute = parseHostWorkspaceTabRouteFromPathname(input.pathname);
|
||||||
if (workspaceRoute) {
|
if (workspaceTabRoute?.tabId?.startsWith("agent_")) {
|
||||||
return { serverId: workspaceRoute.serverId, agentId: workspaceRoute.agentId };
|
const agentId = workspaceTabRoute.tabId.slice("agent_".length).trim();
|
||||||
|
if (agentId) {
|
||||||
|
return { serverId: workspaceTabRoute.serverId, agentId };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
parseHostAgentRouteFromPathname(input.pathname) ??
|
parseHostAgentRouteFromPathname(input.pathname) ??
|
||||||
|
|||||||
@@ -2802,6 +2802,23 @@ export class DaemonClient {
|
|||||||
const startedAtMs = getNowMs()
|
const startedAtMs = getNowMs()
|
||||||
const rawData =
|
const rawData =
|
||||||
data && typeof data === 'object' && 'data' in data ? (data as { data: unknown }).data : data
|
data && typeof data === 'object' && 'data' in data ? (data as { data: unknown }).data : data
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof Blob !== 'undefined' &&
|
||||||
|
rawData instanceof Blob &&
|
||||||
|
typeof rawData.arrayBuffer === 'function'
|
||||||
|
) {
|
||||||
|
void rawData
|
||||||
|
.arrayBuffer()
|
||||||
|
.then((buffer) => {
|
||||||
|
this.handleTransportMessage(buffer)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Ignore failed blob decoding and allow reconnect logic to recover.
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const rawBytes = asUint8Array(rawData)
|
const rawBytes = asUint8Array(rawData)
|
||||||
if (rawBytes) {
|
if (rawBytes) {
|
||||||
const frame = decodeBinaryMuxFrame(rawBytes)
|
const frame = decodeBinaryMuxFrame(rawBytes)
|
||||||
|
|||||||
@@ -6755,7 +6755,11 @@ export class Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const streamId = this.allocateTerminalStreamId()
|
const streamId = this.allocateTerminalStreamId()
|
||||||
const initialOffset = Math.max(0, Math.floor(msg.resumeOffset ?? 0))
|
const requestedResumeOffset =
|
||||||
|
typeof msg.resumeOffset === 'number'
|
||||||
|
? msg.resumeOffset
|
||||||
|
: session.getOutputOffset()
|
||||||
|
const initialOffset = Math.max(0, Math.floor(requestedResumeOffset))
|
||||||
const binding: {
|
const binding: {
|
||||||
terminalId: string
|
terminalId: string
|
||||||
unsubscribe: () => void
|
unsubscribe: () => void
|
||||||
@@ -6789,7 +6793,7 @@ export class Session {
|
|||||||
replay: chunk.replay,
|
replay: chunk.replay,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{ fromOffset: msg.resumeOffset ?? 0 }
|
{ fromOffset: requestedResumeOffset }
|
||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.terminalStreams.delete(streamId)
|
this.terminalStreams.delete(streamId)
|
||||||
|
|||||||
Reference in New Issue
Block a user