From 9cf4d0de6401979c122065216c300a8f400fdf1c Mon Sep 17 00:00:00 2001 From: Raban von Spiegel Date: Fri, 12 Dec 2025 17:12:21 -0800 Subject: [PATCH 01/35] refactor: rename workspace to task in renderer --- src/renderer/App.tsx | 104 +++++++++--------- src/renderer/components/ActiveRuns.tsx | 6 +- src/renderer/components/ChatInterface.tsx | 94 ++++++++-------- .../components/CommandPaletteWrapper.tsx | 6 +- src/renderer/components/LeftSidebar.tsx | 48 ++++---- ...iAgentWorkspace.tsx => MultiAgentTask.tsx} | 38 +++---- src/renderer/components/ProjectMainView.tsx | 52 ++++----- src/renderer/components/RightSidebar.tsx | 28 ++--- .../{WorkspaceChanges.tsx => TaskChanges.tsx} | 0 ...eDeleteButton.tsx => TaskDeleteButton.tsx} | 0 .../{WorkspaceItem.tsx => TaskItem.tsx} | 42 +++---- .../{WorkspaceList.tsx => TaskList.tsx} | 50 ++++----- .../{WorkspaceModal.tsx => TaskModal.tsx} | 40 +++---- .../{WorkspacePorts.tsx => TaskPorts.tsx} | 0 ...erminalPanel.tsx => TaskTerminalPanel.tsx} | 18 +-- .../components/kanban/KanbanBoard.tsx | 8 +- src/renderer/components/kanban/KanbanCard.tsx | 6 +- ...Switch.ts => useAutoScrollOnTaskSwitch.ts} | 18 +-- src/renderer/hooks/useCreatePR.tsx | 2 +- .../{useWorkspaceBusy.ts => useTaskBusy.ts} | 4 +- ...eWorkspaceChanges.ts => useTaskChanges.ts} | 24 ++-- .../lib/{workspaceNames.ts => taskNames.ts} | 44 ++++---- .../lib/{workspaceStatus.ts => taskStatus.ts} | 0 ...erminalsStore.ts => taskTerminalsStore.ts} | 52 ++++----- src/renderer/types/app.ts | 11 +- src/renderer/types/chat.ts | 14 +-- src/renderer/types/index.ts | 2 +- 27 files changed, 355 insertions(+), 356 deletions(-) rename src/renderer/components/{MultiAgentWorkspace.tsx => MultiAgentTask.tsx} (94%) rename src/renderer/components/{WorkspaceChanges.tsx => TaskChanges.tsx} (100%) rename src/renderer/components/{WorkspaceDeleteButton.tsx => TaskDeleteButton.tsx} (100%) rename src/renderer/components/{WorkspaceItem.tsx => TaskItem.tsx} (76%) rename src/renderer/components/{WorkspaceList.tsx => TaskList.tsx} (70%) rename src/renderer/components/{WorkspaceModal.tsx => TaskModal.tsx} (96%) rename src/renderer/components/{WorkspacePorts.tsx => TaskPorts.tsx} (100%) rename src/renderer/components/{WorkspaceTerminalPanel.tsx => TaskTerminalPanel.tsx} (94%) rename src/renderer/hooks/{useAutoScrollOnWorkspaceSwitch.ts => useAutoScrollOnTaskSwitch.ts} (78%) rename src/renderer/hooks/{useWorkspaceBusy.ts => useTaskBusy.ts} (53%) rename src/renderer/hooks/{useWorkspaceChanges.ts => useTaskChanges.ts} (80%) rename src/renderer/lib/{workspaceNames.ts => taskNames.ts} (63%) rename src/renderer/lib/{workspaceStatus.ts => taskStatus.ts} (100%) rename src/renderer/lib/{workspaceTerminalsStore.ts => taskTerminalsStore.ts} (84%) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index a5a75be3..a53ad069 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -3,9 +3,9 @@ import { Button } from './components/ui/button'; import { FolderOpen } from 'lucide-react'; import LeftSidebar from './components/LeftSidebar'; import ProjectMainView from './components/ProjectMainView'; -import WorkspaceModal from './components/WorkspaceModal'; +import TaskModal from './components/TaskModal'; import ChatInterface from './components/ChatInterface'; -import MultiAgentWorkspace from './components/MultiAgentWorkspace'; +import MultiAgentTask from './components/MultiAgentTask'; import { Toaster } from './components/ui/toaster'; import useUpdateNotifier from './hooks/useUpdateNotifier'; import RequirementsNotice from './components/RequirementsNotice'; @@ -30,8 +30,8 @@ import type { ImperativePanelHandle } from 'react-resizable-panels'; import SettingsModal from './components/SettingsModal'; import CommandPaletteWrapper from './components/CommandPaletteWrapper'; import FirstLaunchModal from './components/FirstLaunchModal'; -import type { Project, Workspace } from './types/app'; -import type { WorkspaceMetadata as ChatWorkspaceMetadata } from './types/chat'; +import type { Project, Task } from './types/app'; +import type { TaskMetadata } from './types/chat'; import AppKeyboardShortcuts from './components/AppKeyboardShortcuts'; import { usePlanToasts } from './hooks/usePlanToasts'; import { terminalSessionRegistry } from './terminal/SessionRegistry'; @@ -78,8 +78,6 @@ const RightSidebarBridge: React.FC<{ return null; }; -// Shared types -type WorkspaceMetadata = ChatWorkspaceMetadata; const TITLEBAR_HEIGHT = '36px'; const PANEL_LAYOUT_STORAGE_KEY = 'emdash.layout.left-main-right.v2'; @@ -121,11 +119,11 @@ const AppContent: React.FC = () => { const [showDeviceFlowModal, setShowDeviceFlowModal] = useState(false); const [projects, setProjects] = useState([]); const [selectedProject, setSelectedProject] = useState(null); - const [showWorkspaceModal, setShowWorkspaceModal] = useState(false); + const [showTaskModal, setShowTaskModal] = useState(false); const [showHomeView, setShowHomeView] = useState(true); - const [isCreatingWorkspace, setIsCreatingWorkspace] = useState(false); - const [activeWorkspace, setActiveWorkspace] = useState(null); - const [activeWorkspaceProvider, setActiveWorkspaceProvider] = useState(null); + const [isCreatingTask, setIsCreatingWorkspace] = useState(false); + const [activeTask, setActiveWorkspace] = useState(null); + const [activeTaskProvider, setActiveWorkspaceProvider] = useState(null); const [installedProviders, setInstalledProviders] = useState>({}); const [showSettings, setShowSettings] = useState(false); const [showCommandPalette, setShowCommandPalette] = useState(false); @@ -135,7 +133,7 @@ const AppContent: React.FC = () => { const showAgentRequirement = Object.keys(installedProviders).length > 0 && Object.values(installedProviders).every((v) => v === false); - const deletingWorkspaceIdsRef = useRef>(new Set()); + const deletingTaskIdsRef = useRef>(new Set()); const normalizePathForComparison = useCallback( (input: string | null | undefined) => { @@ -811,7 +809,7 @@ const AppContent: React.FC = () => { preparedPrompt = parts.join('\n'); } - const workspaceMetadata: WorkspaceMetadata | null = + const workspaceMetadata: TaskMetadata | null = linkedLinearIssue || linkedJiraIssue || linkedGithubIssue || preparedPrompt || autoApprove ? { linearIssue: linkedLinearIssue ?? null, @@ -827,7 +825,7 @@ const AppContent: React.FC = () => { const isMultiAgent = totalRuns > 1; const primaryProvider = providerRuns[0]?.provider || 'claude'; - let newWorkspace: Workspace; + let newWorkspace: Task; if (isMultiAgent) { // Multi-agent workspace: create worktrees for each providerΓ—runs combo const variants: Array<{ @@ -867,7 +865,7 @@ const AppContent: React.FC = () => { } } - const multiMeta: WorkspaceMetadata = { + const multiMeta: TaskMetadata = { ...(workspaceMetadata || {}), multiAgent: { enabled: true, @@ -1154,7 +1152,7 @@ const AppContent: React.FC = () => { activateProjectView(project); }; - const handleSelectWorkspace = (workspace: Workspace) => { + const handleSelectWorkspace = (workspace: Task) => { setActiveWorkspace(workspace); // Load provider from workspace.agentId if it exists, otherwise default to null // This ensures the selected provider persists across app restarts @@ -1170,13 +1168,13 @@ const AppContent: React.FC = () => { (project: Project) => { const targetProject = projects.find((p) => p.id === project.id) || project; activateProjectView(targetProject); - setShowWorkspaceModal(true); + setShowTaskModal(true); }, [activateProjectView, projects] ); const removeWorkspaceFromState = (projectId: string, workspaceId: string, wasActive: boolean) => { - const filterWorkspaces = (list?: Workspace[]) => + const filterWorkspaces = (list?: Task[]) => (list || []).filter((w) => w.id !== workspaceId); setProjects((prev) => @@ -1201,10 +1199,10 @@ const AppContent: React.FC = () => { const handleDeleteWorkspace = async ( targetProject: Project, - workspace: Workspace, + workspace: Task, options?: { silent?: boolean } ): Promise => { - if (deletingWorkspaceIdsRef.current.has(workspace.id)) { + if (deletingTaskIdsRef.current.has(workspace.id)) { toast({ title: 'Deletion in progress', description: `"${workspace.name}" is already being removed.`, @@ -1212,9 +1210,9 @@ const AppContent: React.FC = () => { return false; } - const wasActive = activeWorkspace?.id === workspace.id; + const wasActive = activeTask?.id === workspace.id; const workspaceSnapshot = { ...workspace }; - deletingWorkspaceIdsRef.current.add(workspace.id); + deletingTaskIdsRef.current.add(workspace.id); removeWorkspaceFromState(targetProject.id, workspace.id, wasActive); const runDeletion = async (): Promise => { @@ -1359,7 +1357,7 @@ const AppContent: React.FC = () => { } return false; } finally { - deletingWorkspaceIdsRef.current.delete(workspace.id); + deletingTaskIdsRef.current.delete(workspace.id); } }; @@ -1492,7 +1490,7 @@ const AppContent: React.FC = () => { handleSelectWorkspace(ws); setShowKanban(false); }} - onCreateWorkspace={() => setShowWorkspaceModal(true)} + onCreateWorkspace={() => setShowTaskModal(true)} /> ); @@ -1561,29 +1559,29 @@ const AppContent: React.FC = () => { if (selectedProject) { return (
- {activeWorkspace ? ( - (activeWorkspace.metadata as any)?.multiAgent?.enabled ? ( - ) : ( ) ) : ( setShowWorkspaceModal(true)} - activeWorkspace={activeWorkspace} - onSelectWorkspace={handleSelectWorkspace} - onDeleteWorkspace={handleDeleteWorkspace} - isCreatingWorkspace={isCreatingWorkspace} + onCreateTask={() => setShowTaskModal(true)} + activeTask={activeTask} + onSelectTask={handleSelectWorkspace} + onDeleteTask={handleDeleteWorkspace} + isCreatingTask={isCreatingTask} onDeleteProject={handleDeleteProject} /> )} @@ -1654,19 +1652,19 @@ const AppContent: React.FC = () => { onToggleSettings={handleToggleSettings} isSettingsOpen={showSettings} currentPath={ - activeWorkspace?.metadata?.multiAgent?.enabled + activeTask?.metadata?.multiAgent?.enabled ? null - : activeWorkspace?.path || selectedProject?.path || null + : activeTask?.path || selectedProject?.path || null } defaultPreviewUrl={ - activeWorkspace?.id - ? getContainerRunState(activeWorkspace.id)?.previewUrl || null + activeTask?.id + ? getContainerRunState(activeTask.id)?.previewUrl || null : null } - workspaceId={activeWorkspace?.id || null} - workspacePath={activeWorkspace?.path || null} + workspaceId={activeTask?.id || null} + workspacePath={activeTask?.path || null} projectPath={selectedProject?.path || null} - isWorkspaceMultiAgent={Boolean(activeWorkspace?.metadata?.multiAgent?.enabled)} + isWorkspaceMultiAgent={Boolean(activeTask?.metadata?.multiAgent?.enabled)} githubUser={user} onToggleKanban={handleToggleKanban} isKanbanOpen={Boolean(showKanban)} @@ -1694,8 +1692,8 @@ const AppContent: React.FC = () => { onSelectProject={handleSelectProject} onGoHome={handleGoHome} onOpenProject={handleOpenProject} - onSelectWorkspace={handleSelectWorkspace} - activeWorkspace={activeWorkspace || undefined} + onSelectTask={handleSelectWorkspace} + activeTask={activeTask || undefined} onReorderProjects={handleReorderProjects} onReorderProjectsFull={handleReorderProjectsFull} githubInstalled={ghInstalled} @@ -1705,9 +1703,9 @@ const AppContent: React.FC = () => { githubLoading={githubLoading} githubStatusMessage={githubStatusMessage} onSidebarContextChange={handleSidebarContextChange} - onCreateWorkspaceForProject={handleStartCreateWorkspaceFromSidebar} - isCreatingWorkspace={isCreatingWorkspace} - onDeleteWorkspace={handleDeleteWorkspace} + onCreateTaskForProject={handleStartCreateWorkspaceFromSidebar} + isCreatingTask={isCreatingTask} + onDeleteTask={handleDeleteWorkspace} onDeleteProject={handleDeleteProject} isHomeView={showHomeView} /> @@ -1740,7 +1738,7 @@ const AppContent: React.FC = () => { collapsible order={3} > - +
@@ -1755,9 +1753,9 @@ const AppContent: React.FC = () => { handleOpenProject={handleOpenProject} handleOpenSettings={handleOpenSettings} /> - setShowWorkspaceModal(false)} + setShowTaskModal(false)} onCreateWorkspace={handleCreateWorkspace} projectName={selectedProject?.name || ''} defaultBranch={selectedProject?.gitInfo.branch || 'main'} @@ -1773,10 +1771,10 @@ const AppContent: React.FC = () => { /> diff --git a/src/renderer/components/ActiveRuns.tsx b/src/renderer/components/ActiveRuns.tsx index 9ce723bc..d51a69ab 100644 --- a/src/renderer/components/ActiveRuns.tsx +++ b/src/renderer/components/ActiveRuns.tsx @@ -17,10 +17,10 @@ import { interface Props { projects: any[]; onSelectProject?: (project: any) => void; - onSelectWorkspace?: (workspace: any) => void; + onSelectTask?: (task: any) => void; } -const ActiveRuns: React.FC = ({ projects, onSelectProject, onSelectWorkspace }) => { +const ActiveRuns: React.FC = ({ projects, onSelectProject, onSelectTask }) => { const [activeRuns, setActiveRuns] = React.useState(() => (getAllRunStates() || []).filter((s) => ['building', 'starting', 'ready'].includes(s.status)) ); @@ -71,7 +71,7 @@ const ActiveRuns: React.FC = ({ projects, onSelectProject, onSelectWorksp const onOpen = () => { if (project && ws) { onSelectProject?.(project); - onSelectWorkspace?.(ws); + onSelectTask?.(ws); } }; return ( diff --git a/src/renderer/components/ChatInterface.tsx b/src/renderer/components/ChatInterface.tsx index 5e1099a8..e56f3898 100644 --- a/src/renderer/components/ChatInterface.tsx +++ b/src/renderer/components/ChatInterface.tsx @@ -14,16 +14,16 @@ import { usePlanActivationTerminal } from '@/hooks/usePlanActivation'; import { log } from '@/lib/logger'; import { logPlanEvent } from '@/lib/planLogs'; import { type Provider } from '../types'; -import { Workspace } from '../types/chat'; +import { Task } from '../types/chat'; import { getContainerRunState, subscribeToWorkspaceRunState, type ContainerRunState, } from '@/lib/containerRuns'; import { useBrowser } from '@/providers/BrowserProvider'; -import { useWorkspaceTerminals } from '@/lib/workspaceTerminalsStore'; +import { useTaskTerminals } from '@/lib/taskTerminalsStore'; import { getInstallCommandForProvider } from '@shared/providers/registry'; -import { useAutoScrollOnWorkspaceSwitch } from '@/hooks/useAutoScrollOnWorkspaceSwitch'; +import { useAutoScrollOnTaskSwitch } from '@/hooks/useAutoScrollOnTaskSwitch'; declare const window: Window & { electronAPI: { @@ -32,14 +32,14 @@ declare const window: Window & { }; interface Props { - workspace: Workspace; + task: Task; projectName: string; className?: string; initialProvider?: Provider; } const ChatInterface: React.FC = ({ - workspace, + task, projectName: _projectName, className, initialProvider, @@ -55,33 +55,33 @@ const ChatInterface: React.FC = ({ const browser = useBrowser(); const [cliStartFailed, setCliStartFailed] = useState(false); const [containerState, setContainerState] = useState(() => - getContainerRunState(workspace.id) + getContainerRunState(task.id) ); const reduceMotion = useReducedMotion(); - const terminalId = useMemo(() => `${provider}-main-${workspace.id}`, [provider, workspace.id]); + const terminalId = useMemo(() => `${provider}-main-${task.id}`, [provider, task.id]); const [portsExpanded, setPortsExpanded] = useState(false); - const { activeTerminalId } = useWorkspaceTerminals(workspace.id, workspace.path); + const { activeTerminalId } = useTaskTerminals(task.id, task.path); // Auto-scroll to bottom when this workspace becomes active - useAutoScrollOnWorkspaceSwitch(true, workspace.id); + useAutoScrollOnTaskSwitch(true, task.id); // Unified Plan Mode (per workspace) const { enabled: planEnabled, setEnabled: setPlanEnabled } = usePlanMode( - workspace.id, - workspace.path + task.id, + task.path ); // Log transitions for visibility useEffect(() => { - log.info('[plan] state changed', { workspaceId: workspace.id, enabled: planEnabled }); - }, [planEnabled, workspace.id]); + log.info('[plan] state changed', { workspaceId: task.id, enabled: planEnabled }); + }, [planEnabled, task.id]); // For terminal providers with native plan activation commands usePlanActivationTerminal({ enabled: planEnabled, providerId: provider, - workspaceId: workspace.id, - workspacePath: workspace.path, + workspaceId: task.id, + workspacePath: task.path, }); useEffect(() => { @@ -126,8 +126,8 @@ const ChatInterface: React.FC = ({ useEffect(() => { setCliStartFailed(false); setIsProviderInstalled(null); - setContainerState(getContainerRunState(workspace.id)); - }, [workspace.id]); + setContainerState(getContainerRunState(task.id)); + }, [task.id]); const runInstallCommand = useCallback( (cmd: string) => { @@ -179,7 +179,7 @@ const ChatInterface: React.FC = ({ // If a locked provider exists (including Droid), prefer locked. useEffect(() => { try { - const lastKey = `provider:last:${workspace.id}`; + const lastKey = `provider:last:${task.id}`; const last = window.localStorage.getItem(lastKey) as Provider | null; if (initialProvider) { @@ -210,14 +210,14 @@ const ChatInterface: React.FC = ({ } catch { setProvider(initialProvider || 'codex'); } - }, [workspace.id, initialProvider]); + }, [task.id, initialProvider]); // Persist last-selected provider per workspace (including Droid) useEffect(() => { try { - window.localStorage.setItem(`provider:last:${workspace.id}`, provider); + window.localStorage.setItem(`provider:last:${task.id}`, provider); } catch {} - }, [provider, workspace.id]); + }, [provider, task.id]); // Track provider switching const prevProviderRef = React.useRef(null); @@ -303,7 +303,7 @@ const ChatInterface: React.FC = ({ cancelled = true; off?.(); }; - }, [provider, workspace.id]); + }, [provider, task.id]); // If we don't even have a cached status entry for the current provider, pessimistically // show the install banner and kick off a background refresh to populate it. @@ -349,13 +349,13 @@ const ChatInterface: React.FC = ({ try { } catch {} })(); - }, [provider, workspace.id]); + }, [provider, task.id]); const isTerminal = providerMeta[provider]?.terminalOnly === true; const initialInjection = useMemo(() => { if (!isTerminal) return null; - const md = workspace.metadata || null; + const md = task.metadata || null; const p = (md?.initialPrompt || '').trim(); if (p) return p; const issue = md?.linearIssue; @@ -442,12 +442,12 @@ const ChatInterface: React.FC = ({ return lines.join('\n'); } return null; - }, [isTerminal, workspace.metadata]); + }, [isTerminal, task.metadata]); // Only use keystroke injection for providers WITHOUT CLI flag support // Providers with initialPromptFlag use CLI arg injection via TerminalPane instead useInitialPromptInjection({ - workspaceId: workspace.id, + workspaceId: task.id, providerId: provider, prompt: initialInjection, enabled: isTerminal && providerMeta[provider]?.initialPromptFlag === undefined, @@ -456,18 +456,18 @@ const ChatInterface: React.FC = ({ // Ensure a provider is stored for this workspace so fallbacks can subscribe immediately useEffect(() => { try { - localStorage.setItem(`workspaceProvider:${workspace.id}`, provider); + localStorage.setItem(`workspaceProvider:${task.id}`, provider); } catch {} - }, [provider, workspace.id]); + }, [provider, task.id]); useEffect(() => { - const off = subscribeToWorkspaceRunState(workspace.id, (state) => { + const off = subscribeToWorkspaceRunState(task.id, (state) => { setContainerState(state); }); return () => { off?.(); }; - }, [workspace.id]); + }, [task.id]); const containerStatusNode = useMemo(() => { const state = containerState; @@ -502,7 +502,7 @@ const ChatInterface: React.FC = ({ const res = await api.resolveServiceIcon({ service: name, allowNetwork: true, - workspacePath: workspace.path, + workspacePath: task.path, }); if (!cancelled && res?.ok && typeof res.dataUrl === 'string') setSrc(res.dataUrl); } catch {} @@ -518,7 +518,7 @@ const ChatInterface: React.FC = ({ if (dbPorts.has(port)) return