Merge branch 'storage-terminal-ui-dev' into dev

# Conflicts:
#	nix/package.nix
#	packages/app/src/app/_layout.tsx
#	packages/app/src/components/sidebar-workspace-list.tsx
#	packages/app/src/hooks/use-command-center.ts
#	packages/app/src/screens/agent/draft-agent-screen.tsx
#	packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx
#	packages/app/src/screens/workspace/workspace-screen.tsx
#	packages/server/src/server/session.ts
#	packages/server/src/server/session.workspaces.test.ts
#	packages/server/src/terminal/terminal.test.ts
#	packages/server/src/terminal/terminal.ts
This commit is contained in:
Mohamed Boudra
2026-04-01 21:51:29 +07:00
217 changed files with 21075 additions and 4943 deletions
+20 -2
View File
@@ -60,6 +60,7 @@ import { getIsElectronRuntime, isCompactFormFactor } from "@/constants/layout";
import { CommandCenter } from "@/components/command-center";
import { ProjectPickerModal } from "@/components/project-picker-modal";
import { KeyboardShortcutsDialog } from "@/components/keyboard-shortcuts-dialog";
import { WorkspaceSetupDialog } from "@/components/workspace-setup-dialog";
import { useKeyboardShortcuts } from "@/hooks/use-keyboard-shortcuts";
import { queryClient } from "@/query/query-client";
import {
@@ -76,6 +77,7 @@ import {
parseServerIdFromPathname,
parseHostAgentRouteFromPathname,
parseWorkspaceOpenIntent,
decodeWorkspaceIdFromPathSegment,
} from "@/utils/host-routes";
import { syncNavigationActiveWorkspace } from "@/stores/navigation-active-workspace-store";
@@ -372,6 +374,7 @@ function AppContainer({
<UpdateBanner />
<CommandCenter />
<ProjectPickerModal />
<WorkspaceSetupDialog />
<KeyboardShortcutsDialog />
</View>
);
@@ -607,7 +610,6 @@ function FaviconStatusSync() {
function RootStack() {
const storeReady = useStoreReady();
const { theme } = useUnistyles();
return (
<Stack
screenOptions={{
@@ -621,7 +623,23 @@ function RootStack() {
<Stack.Protected guard={storeReady}>
<Stack.Screen name="welcome" />
<Stack.Screen name="settings" />
<Stack.Screen name="h/[serverId]/workspace/[workspaceId]" />
<Stack.Screen
name="h/[serverId]/workspace/[workspaceId]"
getId={({ params }) => {
const serverValue = Array.isArray(params?.serverId)
? params.serverId[0]
: params?.serverId;
const workspaceValue = Array.isArray(params?.workspaceId)
? params.workspaceId[0]
: params?.workspaceId;
const serverId = typeof serverValue === "string" ? serverValue.trim() : "";
const workspaceId =
typeof workspaceValue === "string"
? (decodeWorkspaceIdFromPathSegment(workspaceValue) ?? workspaceValue.trim())
: "";
return `${serverId}:${workspaceId}`;
}}
/>
<Stack.Screen
name="h/[serverId]/agent/[agentId]"
options={{ gestureEnabled: false }}
@@ -3,6 +3,7 @@ import { useLocalSearchParams, useRouter } from "expo-router";
import { useSessionStore } from "@/stores/session-store";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { buildHostRootRoute } from "@/utils/host-routes";
import { resolveWorkspaceIdByExecutionDirectory } from "@/utils/workspace-execution";
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
export default function HostAgentReadyRoute() {
@@ -22,6 +23,21 @@ export default function HostAgentReadyRoute() {
}
return state.sessions[serverId]?.agents?.get(agentId)?.cwd ?? null;
});
const sessionWorkspaces = useSessionStore((state) =>
serverId ? state.sessions[serverId]?.workspaces : undefined,
);
const hasHydratedWorkspaces = useSessionStore((state) =>
serverId ? (state.sessions[serverId]?.hasHydratedWorkspaces ?? false) : false,
);
const resolvedWorkspaceId = useSessionStore((state) => {
if (!serverId || !agentId) {
return null;
}
return resolveWorkspaceIdByExecutionDirectory({
workspaces: state.sessions[serverId]?.workspaces?.values(),
workspaceDirectory: state.sessions[serverId]?.agents?.get(agentId)?.cwd,
});
});
useEffect(() => {
if (redirectedRef.current) {
@@ -33,18 +49,17 @@ export default function HostAgentReadyRoute() {
return;
}
const normalizedCwd = agentCwd?.trim();
if (normalizedCwd) {
if (resolvedWorkspaceId) {
redirectedRef.current = true;
router.replace(
prepareWorkspaceTab({
serverId,
workspaceId: normalizedCwd,
workspaceId: resolvedWorkspaceId,
target: { kind: "agent", agentId },
}) as any,
);
}
}, [agentCwd, agentId, router, serverId]);
}, [agentId, resolvedWorkspaceId, router, serverId]);
useEffect(() => {
if (redirectedRef.current) {
@@ -53,14 +68,14 @@ export default function HostAgentReadyRoute() {
if (!serverId || !agentId) {
return;
}
if (agentCwd?.trim()) {
if (agentCwd?.trim() && !hasHydratedWorkspaces) {
return;
}
if (!client || !isConnected) {
redirectedRef.current = true;
router.replace(buildHostRootRoute(serverId) as any);
}
}, [agentCwd, agentId, client, isConnected, router, serverId]);
}, [agentCwd, agentId, client, hasHydratedWorkspaces, isConnected, router, serverId]);
useEffect(() => {
if (redirectedRef.current) {
@@ -78,12 +93,19 @@ export default function HostAgentReadyRoute() {
return;
}
const cwd = result?.agent?.cwd?.trim();
const workspaceId = resolveWorkspaceIdByExecutionDirectory({
workspaces: sessionWorkspaces?.values(),
workspaceDirectory: cwd,
});
if (!workspaceId && !hasHydratedWorkspaces) {
return;
}
redirectedRef.current = true;
if (cwd) {
if (workspaceId) {
router.replace(
prepareWorkspaceTab({
serverId,
workspaceId: cwd,
workspaceId,
target: { kind: "agent", agentId },
}) as any,
);
@@ -102,7 +124,7 @@ export default function HostAgentReadyRoute() {
return () => {
cancelled = true;
};
}, [agentId, client, isConnected, router, serverId]);
}, [agentId, client, hasHydratedWorkspaces, isConnected, router, serverId, sessionWorkspaces]);
return null;
}
+27 -6
View File
@@ -3,14 +3,23 @@ import { useLocalSearchParams, usePathname, useRouter } from "expo-router";
import { useSessionStore } from "@/stores/session-store";
import { useFormPreferences } from "@/hooks/use-form-preferences";
import {
buildHostAgentDetailRoute,
buildHostOpenProjectRoute,
buildHostRootRoute,
buildHostWorkspaceRoute,
buildHostWorkspaceOpenRoute,
} from "@/utils/host-routes";
import { resolveWorkspaceIdByExecutionDirectory } from "@/utils/workspace-execution";
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
const HOST_ROOT_REDIRECT_DELAY_MS = 300;
function getCurrentPathname(fallbackPathname: string): string {
if (typeof window === "undefined") {
return fallbackPathname;
}
return window.location.pathname || fallbackPathname;
}
export default function HostIndexRoute() {
const router = useRouter();
const pathname = usePathname();
@@ -32,11 +41,13 @@ export default function HostIndexRoute() {
return;
}
const rootRoute = buildHostRootRoute(serverId);
if (pathname !== rootRoute && pathname !== `${rootRoute}/`) {
const currentPathname = getCurrentPathname(pathname);
if (currentPathname !== rootRoute && currentPathname !== `${rootRoute}/`) {
return;
}
const timer = setTimeout(() => {
if (pathname !== rootRoute && pathname !== `${rootRoute}/`) {
const latestPathname = getCurrentPathname(pathname);
if (latestPathname !== rootRoute && latestPathname !== `${rootRoute}/`) {
return;
}
@@ -55,20 +66,30 @@ export default function HostIndexRoute() {
});
const primaryAgent = visibleAgents[0];
if (primaryAgent?.cwd?.trim()) {
const primaryAgentWorkspaceId = resolveWorkspaceIdByExecutionDirectory({
workspaces: sessionWorkspaces?.values(),
workspaceDirectory: primaryAgent?.cwd,
});
if (primaryAgent && primaryAgentWorkspaceId) {
router.replace(
prepareWorkspaceTab({
serverId,
workspaceId: primaryAgent.cwd.trim(),
workspaceId: primaryAgentWorkspaceId,
target: { kind: "agent", agentId: primaryAgent.id },
}) as any,
);
return;
}
if (primaryAgent) {
router.replace(buildHostAgentDetailRoute(serverId, primaryAgent.id) as any);
return;
}
const primaryWorkspace = visibleWorkspaces[0];
if (primaryWorkspace?.id?.trim()) {
router.replace(buildHostWorkspaceRoute(serverId, primaryWorkspace.id.trim()) as any);
router.replace(
buildHostWorkspaceOpenRoute(serverId, primaryWorkspace.id.trim(), "draft:new") as any,
);
return;
}
@@ -1,10 +1,10 @@
import { useEffect, useRef } from "react";
import { useGlobalSearchParams, useLocalSearchParams, useRouter } from "expo-router";
import { useGlobalSearchParams, usePathname, useRouter } from "expo-router";
import type { WorkspaceTabTarget } from "@/stores/workspace-tabs-store";
import { WorkspaceScreen } from "@/screens/workspace/workspace-screen";
import {
buildHostWorkspaceRoute,
decodeWorkspaceIdFromPathSegment,
parseHostWorkspaceRouteFromPathname,
parseWorkspaceOpenIntent,
type WorkspaceOpenIntent,
} from "@/utils/host-routes";
@@ -37,18 +37,13 @@ function getOpenIntentTarget(openIntent: WorkspaceOpenIntent): WorkspaceTabTarge
export default function HostWorkspaceLayout() {
const router = useRouter();
const consumedIntentRef = useRef<string | null>(null);
const params = useLocalSearchParams<{
serverId?: string | string[];
workspaceId?: string | string[];
}>();
const pathname = usePathname();
const globalParams = useGlobalSearchParams<{
open?: string | string[];
}>();
const serverId = getParamValue(params.serverId);
const workspaceValue = getParamValue(params.workspaceId);
const workspaceId = workspaceValue
? (decodeWorkspaceIdFromPathSegment(workspaceValue) ?? "")
: "";
const parsedWorkspaceRoute = parseHostWorkspaceRouteFromPathname(pathname);
const serverId = parsedWorkspaceRoute?.serverId ?? "";
const workspaceId = parsedWorkspaceRoute?.workspaceId ?? "";
const openValue = getParamValue(globalParams.open);
useEffect(() => {
+9 -1
View File
@@ -14,6 +14,13 @@ import { buildHostRootRoute } from "@/utils/host-routes";
const WELCOME_ROUTE = "/welcome";
function getCurrentPathname(fallbackPathname: string): string {
if (typeof window === "undefined") {
return fallbackPathname;
}
return window.location.pathname || fallbackPathname;
}
function useAnyOnlineHostServerId(serverIds: string[]): string | null {
const runtime = getHostRuntimeStore();
@@ -51,7 +58,8 @@ export default function Index() {
if (!storeReady) {
return;
}
if (pathname !== "/" && pathname !== "") {
const currentPathname = getCurrentPathname(pathname);
if (currentPathname !== "/" && currentPathname !== "") {
return;
}