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
@@ -11,15 +11,14 @@ import Animated from "react-native-reanimated";
import { Folder, GitBranch, PanelRight } from "lucide-react-native";
import { SidebarMenuToggle } from "@/components/headers/menu-header";
import { HeaderToggleButton } from "@/components/headers/header-toggle-button";
import { AgentInputArea } from "@/components/agent-input-area";
import { Composer } from "@/components/composer";
import { AgentStreamView } from "@/components/agent-stream-view";
import { FormSelectTrigger } from "@/components/agent-form/agent-form-dropdowns";
import { ExplorerSidebar } from "@/components/explorer-sidebar";
import { Combobox } from "@/components/ui/combobox";
import { FileDropZone } from "@/components/file-drop-zone";
import { useQuery } from "@tanstack/react-query";
import { useAgentFormState, type CreateAgentInitialValues } from "@/hooks/use-agent-form-state";
import type { DraftCommandConfig } from "@/hooks/use-agent-commands-query";
import type { CreateAgentInitialValues } from "@/hooks/use-agent-form-state";
import {
CHECKOUT_STATUS_STALE_TIME,
checkoutStatusQueryKey,
@@ -27,6 +26,7 @@ import {
import { useAllAgentsList } from "@/hooks/use-all-agents-list";
import { useHosts } from "@/runtime/host-runtime";
import { buildBranchComboOptions, normalizeBranchOptionName } from "@/utils/branch-suggestions";
import { buildHostAgentDetailRoute } from "@/utils/host-routes";
import { shortenPath } from "@/utils/shorten-path";
import { collectAgentWorkingDirectorySuggestions } from "@/utils/agent-working-directory-suggestions";
import { buildWorkingDirectorySuggestions } from "@/utils/working-directory-suggestions";
@@ -50,6 +50,7 @@ import type {
AgentSessionConfig,
} from "@server/server/agent/agent-sdk-types";
import { AGENT_PROVIDER_DEFINITIONS } from "@server/server/agent/provider-manifest";
import { resolveWorkspaceIdByExecutionDirectory } from "@/utils/workspace-execution";
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
@@ -65,6 +66,7 @@ const DRAFT_CAPABILITIES: AgentCapabilityFlags = {
supportsMcpServers: false,
supportsReasoningStream: false,
supportsToolInvocations: false,
supportsTerminalMode: false,
};
const PROVIDER_DEFINITION_MAP = new Map(
AGENT_PROVIDER_DEFINITIONS.map((definition) => [definition.id, definition]),
@@ -202,37 +204,45 @@ function DraftAgentScreenContent({
return values;
}, [resolvedMode, resolvedModel, resolvedProvider, resolvedThinkingOptionId, resolvedWorkingDir]);
const draftIdRef = useRef(generateDraftId());
const draftAgentIdRef = useRef(generateDraftId());
const draftInput = useAgentInputDraft(
{
draftKey: ({ selectedServerId }) =>
buildDraftStoreKey({
serverId: selectedServerId ?? "",
agentId: draftAgentIdRef.current,
draftId: draftIdRef.current,
}),
composer: {
initialServerId: resolvedServerId ?? null,
initialValues,
isVisible,
onlineServerIds,
},
},
);
const composerState = draftInput.composerState;
if (!composerState) {
throw new Error("Draft agent composer state is required");
}
const {
selectedServerId,
setSelectedServerIdFromUser,
selectedProvider,
setProviderFromUser,
selectedMode,
setModeFromUser,
selectedModel,
setModelFromUser,
selectedThinkingOptionId,
setThinkingOptionFromUser,
providerDefinitions,
workingDir,
setWorkingDirFromUser,
providerDefinitions,
modeOptions,
availableModels,
allProviderModels,
isAllModelsLoading,
availableThinkingOptions,
isModelLoading,
modelError,
refreshProviderModels,
setProviderAndModelFromUser,
persistFormPreferences,
} = useAgentFormState({
initialServerId: resolvedServerId ?? null,
initialValues,
isVisible,
isCreateFlow: true,
onlineServerIds,
});
effectiveModelId,
effectiveThinkingOptionId,
commandDraftConfig,
statusControls,
} = composerState;
const isMobile = isCompactFormFactor();
const mobileView = usePanelStore((state) => state.mobileView);
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
@@ -244,15 +254,6 @@ function DraftAgentScreenContent({
(state) => state.activateExplorerTabForCheckout,
);
const isExplorerOpen = isMobile ? mobileView === "file-explorer" : desktopFileExplorerOpen;
const draftIdRef = useRef(generateDraftId());
const draftAgentIdRef = useRef(generateDraftId());
const draftInput = useAgentInputDraft(
buildDraftStoreKey({
serverId: selectedServerId ?? "",
agentId: draftAgentIdRef.current,
draftId: draftIdRef.current,
}),
);
const [worktreeMode, setWorktreeMode] = useState<"none" | "create" | "attach">(
initialWorktreeMode,
@@ -632,6 +633,16 @@ function DraftAgentScreenContent({
isGit: isAttachWorktree && selectedWorktreePath ? true : checkout?.isGit === true,
};
}, [selectedServerId, explorerCwd, isAttachWorktree, selectedWorktreePath, checkout?.isGit]);
const draftExplorerWorkspaceId = useSessionStore(
useCallback(
(state) =>
resolveWorkspaceIdByExecutionDirectory({
workspaces: selectedServerId ? state.sessions[selectedServerId]?.workspaces?.values() : null,
workspaceDirectory: explorerCwd,
}),
[explorerCwd, selectedServerId],
),
);
const canOpenExplorer = draftExplorerCheckout !== null;
const openExplorerForDraftCheckout = useCallback(() => {
if (!draftExplorerCheckout) {
@@ -743,47 +754,6 @@ function DraftAgentScreenContent({
}, [baseBranch, branchSearchQuery, branchSuggestionsQuery.data, checkout, worktreeOptions]);
const createAgentClient = sessionClient;
const effectiveDraftModelId = useMemo(() => {
if (selectedModel.trim()) {
return selectedModel.trim();
}
return availableModels.find((model) => model.isDefault)?.id ?? availableModels[0]?.id ?? "";
}, [availableModels, selectedModel]);
const effectiveDraftThinkingOptionId = useMemo(() => {
if (selectedThinkingOptionId.trim()) {
return selectedThinkingOptionId.trim();
}
const selectedModelDefinition =
availableModels.find((model) => model.id === effectiveDraftModelId) ?? null;
return selectedModelDefinition?.defaultThinkingOptionId ?? "";
}, [availableModels, effectiveDraftModelId, selectedThinkingOptionId]);
const draftCommandConfig = useMemo<DraftCommandConfig | undefined>(() => {
const cwd = (
isAttachWorktree && selectedWorktreePath ? selectedWorktreePath : workingDir
).trim();
if (!cwd) {
return undefined;
}
return {
provider: selectedProvider,
cwd,
...(modeOptions.length > 0 && selectedMode !== "" ? { modeId: selectedMode } : {}),
...(effectiveDraftModelId ? { model: effectiveDraftModelId } : {}),
...(effectiveDraftThinkingOptionId
? { thinkingOptionId: effectiveDraftThinkingOptionId }
: {}),
};
}, [
effectiveDraftModelId,
effectiveDraftThinkingOptionId,
isAttachWorktree,
modeOptions.length,
selectedMode,
selectedProvider,
selectedWorktreePath,
workingDir,
]);
const {
formErrorMessage,
@@ -791,7 +761,7 @@ function DraftAgentScreenContent({
optimisticStreamItems,
draftAgent,
handleCreateFromInput,
} = useDraftAgentCreateFlow<Agent, { id: string; cwd: string }>({
} = useDraftAgentCreateFlow<Agent, { id: string; workspaceId: string | null }>({
draftId: draftIdRef.current,
getPendingServerId: () => selectedServerId,
validateBeforeSubmit: ({ text }) => {
@@ -817,7 +787,7 @@ function DraftAgentScreenContent({
if (isModelLoading) {
return "Model defaults are still loading";
}
if (!effectiveDraftModelId) {
if (!effectiveModelId) {
return "No model is available for the selected provider";
}
if (isAttachWorktree && !selectedWorktreePath) {
@@ -850,15 +820,19 @@ function DraftAgentScreenContent({
const cwd =
(isAttachWorktree && selectedWorktreePath ? selectedWorktreePath : workingDir).trim() ||
".";
const provider = selectedProvider;
const model = effectiveDraftModelId || null;
const thinkingOptionId = effectiveDraftThinkingOptionId || null;
const modeId = modeOptions.length > 0 && selectedMode !== "" ? selectedMode : null;
const provider = composerState.selectedProvider;
const model = effectiveModelId || null;
const thinkingOptionId = effectiveThinkingOptionId || null;
const modeId =
composerState.modeOptions.length > 0 && composerState.selectedMode !== ""
? composerState.selectedMode
: null;
return {
serverId,
id: draftAgentIdRef.current,
provider,
terminal: false,
status: "running",
createdAt: now,
updatedAt: now,
@@ -887,14 +861,17 @@ function DraftAgentScreenContent({
const resolvedWorkingDir =
isAttachWorktree && selectedWorktreePath ? selectedWorktreePath : trimmedPath;
const modeId = modeOptions.length > 0 && selectedMode !== "" ? selectedMode : undefined;
const modeId =
composerState.modeOptions.length > 0 && composerState.selectedMode !== ""
? composerState.selectedMode
: undefined;
const config: AgentSessionConfig = {
provider: selectedProvider,
provider: composerState.selectedProvider,
cwd: resolvedWorkingDir,
...(modeId ? { modeId } : {}),
...(effectiveDraftModelId ? { model: effectiveDraftModelId } : {}),
...(effectiveDraftThinkingOptionId
? { thinkingOptionId: effectiveDraftThinkingOptionId }
...(effectiveModelId ? { model: effectiveModelId } : {}),
...(effectiveThinkingOptionId
? { thinkingOptionId: effectiveThinkingOptionId }
: {}),
};
@@ -942,20 +919,29 @@ function DraftAgentScreenContent({
const createdWorkingDir = typeof result.cwd === "string" ? result.cwd.trim() : "";
const configuredWorkingDir = config.cwd.trim();
const workspaceId = createdWorkingDir.length > 0 ? createdWorkingDir : configuredWorkingDir;
const workspaceId = resolveWorkspaceIdByExecutionDirectory({
workspaces: useSessionStore.getState().sessions[selectedServerId]?.workspaces?.values(),
workspaceDirectory: createdWorkingDir.length > 0 ? createdWorkingDir : configuredWorkingDir,
});
return {
agentId: result.id,
result: {
id: result.id,
cwd: workspaceId,
workspaceId,
},
};
},
onCreateSuccess: ({ result }) => {
if (!result.workspaceId) {
router.replace(
buildHostAgentDetailRoute(selectedServerId as string, result.id) as any,
);
return;
}
const route = prepareWorkspaceTab({
serverId: selectedServerId as string,
workspaceId: result.cwd,
workspaceId: result.workspaceId,
target: { kind: "agent", agentId: result.id },
});
router.replace(route as any);
@@ -1216,7 +1202,7 @@ function DraftAgentScreenContent({
)}
</Animated.View>
<View style={styles.inputAreaWrapper}>
<AgentInputArea
<Composer
agentId={draftAgentIdRef.current}
serverId={selectedServerId ?? ""}
isInputActive={isFocused}
@@ -1230,24 +1216,9 @@ function DraftAgentScreenContent({
clearDraft={draftInput.clear}
autoFocus={!isSubmitting}
onAddImages={handleAddImagesCallback}
commandDraftConfig={draftCommandConfig}
commandDraftConfig={commandDraftConfig}
statusControls={{
providerDefinitions,
selectedProvider,
onSelectProvider: setProviderFromUser,
modeOptions,
selectedMode,
onSelectMode: setModeFromUser,
models: availableModels,
selectedModel,
onSelectModel: setModelFromUser,
isModelLoading,
allProviderModels,
isAllModelsLoading,
onSelectProviderAndModel: setProviderAndModelFromUser,
thinkingOptions: availableThinkingOptions,
selectedThinkingOptionId,
onSelectThinkingOption: setThinkingOptionFromUser,
...statusControls,
disabled: isSubmitting,
}}
/>
@@ -1257,7 +1228,7 @@ function DraftAgentScreenContent({
{!isMobile && isExplorerOpen && explorerServerId && draftExplorerCheckout ? (
<ExplorerSidebar
serverId={explorerServerId}
workspaceId={draftExplorerCheckout.cwd}
workspaceId={draftExplorerWorkspaceId}
workspaceRoot={draftExplorerCheckout.cwd}
isGit={explorerIsGit}
/>
@@ -1280,7 +1251,7 @@ function DraftAgentScreenContent({
{isMobile && explorerServerId && draftExplorerCheckout ? (
<ExplorerSidebar
serverId={explorerServerId}
workspaceId={draftExplorerCheckout.cwd}
workspaceId={draftExplorerWorkspaceId}
workspaceRoot={draftExplorerCheckout.cwd}
isGit={explorerIsGit}
/>