diff --git a/apps/web/app/settings/workspace/[id]/boards/page.tsx b/apps/web/app/settings/workspace/[id]/boards/page.tsx new file mode 100644 index 0000000000..9ece56d2c1 --- /dev/null +++ b/apps/web/app/settings/workspace/[id]/boards/page.tsx @@ -0,0 +1,32 @@ +import { + getWorkspaceAction, + listBoardsAction, + listColumnsAction, +} from '@/app/actions/workspaces'; +import { WorkspaceBoardsClient } from '@/app/settings/workspace/workspace-boards-client'; + +export default async function WorkspaceBoardsPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + let workspace = null; + let boardsWithColumns: Array< + Awaited>['boards'][number] & { + columns: Awaited>['columns']; + } + > = []; + + try { + workspace = await getWorkspaceAction(id); + const boards = await listBoardsAction(id); + boardsWithColumns = await Promise.all( + boards.boards.map(async (board) => { + const columns = await listColumnsAction(board.id); + return { ...board, columns: columns.columns }; + }) + ); + } catch { + workspace = null; + boardsWithColumns = []; + } + + return ; +} diff --git a/apps/web/app/settings/workspace/[id]/page.tsx b/apps/web/app/settings/workspace/[id]/page.tsx index 254681afc7..dfe3993b99 100644 --- a/apps/web/app/settings/workspace/[id]/page.tsx +++ b/apps/web/app/settings/workspace/[id]/page.tsx @@ -1,45 +1,15 @@ -import { - getWorkspaceAction, - listBoardsAction, - listColumnsAction, - listRepositoriesAction, - listRepositoryScriptsAction, -} from '@/app/actions/workspaces'; +import { getWorkspaceAction } from '@/app/actions/workspaces'; import { WorkspaceEditClient } from '@/app/settings/workspace/workspace-edit-client'; export default async function WorkspaceEditPage({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; let workspace = null; - let boardsWithColumns: Array>['boards'][number] & { columns: Awaited>['columns'] }> = []; - let repositoriesWithScripts: Array>['repositories'][number] & { scripts: Awaited>['scripts'] }> = []; try { workspace = await getWorkspaceAction(id); - const boards = await listBoardsAction(id); - boardsWithColumns = await Promise.all( - boards.boards.map(async (board) => { - const columns = await listColumnsAction(board.id); - return { ...board, columns: columns.columns }; - }) - ); - const repositories = await listRepositoriesAction(id); - repositoriesWithScripts = await Promise.all( - repositories.repositories.map(async (repository) => { - const scripts = await listRepositoryScriptsAction(repository.id); - return { ...repository, scripts: scripts.scripts }; - }) - ); } catch { workspace = null; - boardsWithColumns = []; - repositoriesWithScripts = []; } - return ( - - ); + return ; } diff --git a/apps/web/app/settings/workspace/[id]/repositories/page.tsx b/apps/web/app/settings/workspace/[id]/repositories/page.tsx new file mode 100644 index 0000000000..97289811d4 --- /dev/null +++ b/apps/web/app/settings/workspace/[id]/repositories/page.tsx @@ -0,0 +1,38 @@ +import { + getWorkspaceAction, + listRepositoriesAction, + listRepositoryScriptsAction, +} from '@/app/actions/workspaces'; +import { WorkspaceRepositoriesClient } from '@/app/settings/workspace/workspace-repositories-client'; + +export default async function WorkspaceRepositoriesPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + let workspace = null; + let repositoriesWithScripts: Array< + Awaited>['repositories'][number] & { + scripts: Awaited>['scripts']; + } + > = []; + + try { + workspace = await getWorkspaceAction(id); + const repositories = await listRepositoriesAction(id); + repositoriesWithScripts = await Promise.all( + repositories.repositories.map(async (repository) => { + const scripts = await listRepositoryScriptsAction(repository.id); + return { ...repository, scripts: scripts.scripts }; + }) + ); + } catch { + workspace = null; + repositoriesWithScripts = []; + } + + return ( + + ); +} diff --git a/apps/web/app/settings/workspace/workspace-boards-client.tsx b/apps/web/app/settings/workspace/workspace-boards-client.tsx new file mode 100644 index 0000000000..9257a42f77 --- /dev/null +++ b/apps/web/app/settings/workspace/workspace-boards-client.tsx @@ -0,0 +1,339 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { IconLayoutColumns, IconPlus } from '@tabler/icons-react'; +import { Button } from '@kandev/ui/button'; +import { Card, CardContent } from '@kandev/ui/card'; +import { Separator } from '@kandev/ui/separator'; +import { SettingsSection } from '@/components/settings/settings-section'; +import { BoardCard } from '@/components/settings/board-card'; +import { generateUUID } from '@/lib/utils'; +import { + createBoardAction, + createColumnAction, + deleteBoardAction, + deleteColumnAction, + updateBoardAction, + updateColumnAction, +} from '@/app/actions/workspaces'; +import type { Board, Column, Workspace } from '@/lib/types/http'; + +type BoardWithColumns = Board & { columns: Column[] }; + +type WorkspaceBoardsClientProps = { + workspace: Workspace | null; + boards: BoardWithColumns[]; +}; + +export function WorkspaceBoardsClient({ workspace, boards }: WorkspaceBoardsClientProps) { + const router = useRouter(); + const [boardItems, setBoardItems] = useState(boards); + const [savedBoardItems, setSavedBoardItems] = useState(boards); + + const cloneBoard = (board: BoardWithColumns): BoardWithColumns => ({ + ...board, + columns: board.columns.map((column) => ({ ...column })), + }); + + const savedBoardsById = useMemo(() => { + return new Map(savedBoardItems.map((board) => [board.id, board])); + }, [savedBoardItems]); + + const isBoardNameDirty = (board: BoardWithColumns) => { + const saved = savedBoardsById.get(board.id); + if (!saved) return true; + if (board.name !== saved.name || board.description !== saved.description) return true; + return false; + }; + + const areColumnsDirty = (board: BoardWithColumns) => { + const saved = savedBoardsById.get(board.id); + if (!saved) { + return board.columns.length > 0; + } + if (board.columns.length !== saved.columns.length) return true; + const savedColumns = new Map(saved.columns.map((column) => [column.id, column])); + for (const column of board.columns) { + const savedColumn = savedColumns.get(column.id); + if (!savedColumn) return true; + if ( + column.name !== savedColumn.name || + column.color !== savedColumn.color || + column.position !== savedColumn.position || + column.state !== savedColumn.state + ) { + return true; + } + } + return false; + }; + + const handleAddBoard = () => { + if (!workspace) return; + const draftBoardId = `temp-${generateUUID()}`; + const draftBoard: BoardWithColumns = { + id: draftBoardId, + workspace_id: workspace.id, + name: 'New Board', + description: '', + created_at: '', + updated_at: '', + columns: [ + { + id: `temp-col-${generateUUID()}`, + board_id: draftBoardId, + name: 'Todo', + position: 0, + state: 'TODO', + color: 'bg-cyan-500', + created_at: '', + updated_at: '', + }, + { + id: `temp-col-${generateUUID()}`, + board_id: draftBoardId, + name: 'In Progress', + position: 1, + state: 'IN_PROGRESS', + color: 'bg-yellow-500', + created_at: '', + updated_at: '', + }, + { + id: `temp-col-${generateUUID()}`, + board_id: draftBoardId, + name: 'To Review', + position: 2, + state: 'REVIEW', + color: 'bg-green-500', + created_at: '', + updated_at: '', + }, + { + id: `temp-col-${generateUUID()}`, + board_id: draftBoardId, + name: 'Done', + position: 3, + state: 'COMPLETED', + color: 'bg-indigo-500', + created_at: '', + updated_at: '', + }, + ], + }; + setBoardItems((prev) => [draftBoard, ...prev]); + }; + + const handleUpdateBoard = (boardId: string, updates: { name?: string; description?: string }) => { + setBoardItems((prev) => + prev.map((board) => (board.id === boardId ? { ...board, ...updates } : board)) + ); + }; + + const handleDeleteBoard = async (boardId: string) => { + if (boardId.startsWith('temp-')) { + setBoardItems((prev) => prev.filter((board) => board.id !== boardId)); + return; + } + await deleteBoardAction(boardId); + setBoardItems((prev) => prev.filter((board) => board.id !== boardId)); + setSavedBoardItems((prev) => prev.filter((board) => board.id !== boardId)); + }; + + const handleCreateColumn = ( + boardId: string, + column: Omit + ) => { + const draftColumn: Column = { + id: `temp-col-${generateUUID()}`, + board_id: boardId, + name: column.name, + position: column.position, + state: column.state, + color: column.color, + created_at: '', + updated_at: '', + }; + setBoardItems((prev) => + prev.map((board) => + board.id === boardId ? { ...board, columns: [...board.columns, draftColumn] } : board + ) + ); + }; + + const handleUpdateColumn = (boardId: string, columnId: string, updates: Partial) => { + setBoardItems((prev) => + prev.map((board) => + board.id === boardId + ? { + ...board, + columns: board.columns.map((column) => + column.id === columnId ? { ...column, ...updates } : column + ), + } + : board + ) + ); + }; + + const handleDeleteColumn = async (boardId: string, columnId: string) => { + if (boardId.startsWith('temp-')) { + setBoardItems((prev) => + prev.map((board) => + board.id === boardId + ? { ...board, columns: board.columns.filter((column) => column.id !== columnId) } + : board + ) + ); + return; + } + await deleteColumnAction(columnId); + setBoardItems((prev) => + prev.map((board) => + board.id === boardId + ? { ...board, columns: board.columns.filter((column) => column.id !== columnId) } + : board + ) + ); + }; + + const handleReorderColumns = (boardId: string, columns: Column[]) => { + setBoardItems((prev) => + prev.map((board) => (board.id === boardId ? { ...board, columns } : board)) + ); + }; + + const handleSaveBoard = async (boardId: string) => { + const board = boardItems.find((item) => item.id === boardId); + if (!board) return; + if (boardId.startsWith('temp-')) { + const name = board.name.trim() || 'New Board'; + const createdBoard = await createBoardAction({ + workspace_id: workspace?.id ?? board.workspace_id, + name, + }); + const createdColumns = await Promise.all( + board.columns.map((column, index) => + createColumnAction({ + board_id: createdBoard.id, + name: column.name.trim() || 'New Column', + position: column.position ?? index, + state: column.state, + color: column.color, + }) + ) + ); + const nextBoard = { + ...createdBoard, + columns: createdColumns, + }; + setBoardItems((prev) => prev.map((item) => (item.id === boardId ? nextBoard : item))); + setSavedBoardItems((prev) => [cloneBoard(nextBoard), ...prev]); + return; + } + const updates: { name?: string; description?: string } = {}; + if (board.name.trim()) { + updates.name = board.name.trim(); + } + if (Object.keys(updates).length) { + await updateBoardAction(boardId, updates); + } + const nextColumns = await Promise.all( + board.columns.map((column, index) => { + if (column.id.startsWith('temp-col-')) { + return createColumnAction({ + board_id: boardId, + name: column.name.trim() || 'New Column', + position: column.position ?? index, + state: column.state, + color: column.color, + }); + } + return updateColumnAction(column.id, { + name: column.name, + color: column.color, + position: column.position ?? index, + state: column.state, + }); + }) + ); + setBoardItems((prev) => + prev.map((item) => (item.id === boardId ? { ...item, columns: nextColumns } : item)) + ); + setSavedBoardItems((prev) => + prev.some((item) => item.id === boardId) + ? prev.map((item) => + item.id === boardId ? cloneBoard({ ...board, columns: nextColumns }) : item + ) + : [...prev, cloneBoard({ ...board, columns: nextColumns })] + ); + }; + + if (!workspace) { + return ( +
+ + +

Workspace not found

+ +
+
+
+ ); + } + + return ( +
+
+
+

{workspace.name}

+

+ Manage boards and columns for this workspace. +

+
+ +
+ + + + } + title="Boards" + description="Boards and columns in this workspace" + action={ + + } + > +
+ {boardItems.map((board) => ( + handleUpdateBoard(board.id, updates)} + onDeleteBoard={() => handleDeleteBoard(board.id)} + onCreateColumn={(column) => handleCreateColumn(board.id, column)} + onUpdateColumn={(columnId, updates) => + handleUpdateColumn(board.id, columnId, updates) + } + onDeleteColumn={(columnId) => handleDeleteColumn(board.id, columnId)} + onReorderColumns={(columns) => handleReorderColumns(board.id, columns)} + onSaveBoard={() => handleSaveBoard(board.id)} + /> + ))} +
+
+
+ ); +} diff --git a/apps/web/app/settings/workspace/workspace-edit-client.tsx b/apps/web/app/settings/workspace/workspace-edit-client.tsx index af4ab29f31..5a6816aa41 100644 --- a/apps/web/app/settings/workspace/workspace-edit-client.tsx +++ b/apps/web/app/settings/workspace/workspace-edit-client.tsx @@ -1,11 +1,11 @@ 'use client'; -import { useMemo, useState } from 'react'; +import { useState } from 'react'; +import Link from 'next/link'; import { useRouter } from 'next/navigation'; -import { IconGitBranch, IconLoader2, IconPlus, IconTrash } from '@tabler/icons-react'; +import { IconGitBranch, IconLayoutColumns, IconTrash } from '@tabler/icons-react'; import { Button } from '@kandev/ui/button'; import { Input } from '@kandev/ui/input'; -import { Label } from '@kandev/ui/label'; import { Card, CardContent, CardHeader, CardTitle } from '@kandev/ui/card'; import { Separator } from '@kandev/ui/separator'; import { @@ -16,78 +16,27 @@ import { DialogHeader, DialogTitle, } from '@kandev/ui/dialog'; -import { SettingsSection } from '@/components/settings/settings-section'; -import { RepositoryCard } from '@/components/settings/repository-card'; -import { BoardCard } from '@/components/settings/board-card'; -import { generateUUID } from '@/lib/utils'; -import { - createBoardAction, - createColumnAction, - discoverRepositoriesAction, - createRepositoryAction, - createRepositoryScriptAction, - deleteBoardAction, - deleteColumnAction, - deleteRepositoryAction, - deleteRepositoryScriptAction, - deleteWorkspaceAction, - updateBoardAction, - updateColumnAction, - validateRepositoryPathAction, - updateRepositoryAction, - updateRepositoryScriptAction, - updateWorkspaceAction, -} from '@/app/actions/workspaces'; -import type { - Board, - Column, - LocalRepository, - Repository, - RepositoryScript, - Workspace, -} from '@/lib/types/http'; +import { updateWorkspaceAction, deleteWorkspaceAction } from '@/app/actions/workspaces'; +import type { Workspace } from '@/lib/types/http'; import { useRequest } from '@/lib/http/use-request'; import { useToast } from '@/components/toast-provider'; import { useAppStore } from '@/components/state-provider'; import { UnsavedChangesBadge, UnsavedSaveButton } from '@/components/settings/unsaved-indicator'; -type BoardWithColumns = Board & { columns: Column[] }; -type RepositoryWithScripts = Repository & { scripts: RepositoryScript[] }; - type WorkspaceEditClientProps = { workspace: Workspace | null; - boards: BoardWithColumns[]; - repositories: RepositoryWithScripts[]; }; -export function WorkspaceEditClient({ workspace, boards, repositories }: WorkspaceEditClientProps) { +export function WorkspaceEditClient({ workspace }: WorkspaceEditClientProps) { const router = useRouter(); const { toast } = useToast(); const [currentWorkspace, setCurrentWorkspace] = useState(workspace); const [workspaceNameDraft, setWorkspaceNameDraft] = useState(workspace?.name ?? ''); - const [boardItems, setBoardItems] = useState(boards); const [savedWorkspaceName, setSavedWorkspaceName] = useState(workspace?.name ?? ''); - const [savedBoardItems, setSavedBoardItems] = useState(boards); - const [repositoryItems, setRepositoryItems] = useState(repositories); - const [savedRepositoryItems, setSavedRepositoryItems] = useState(repositories); - const [discoveredRepositories, setDiscoveredRepositories] = useState([]); - const [localRepoDialogOpen, setLocalRepoDialogOpen] = useState(false); - const [remoteRepoDialogOpen, setRemoteRepoDialogOpen] = useState(false); - const [repoSearch, setRepoSearch] = useState(''); - const [selectedRepoPath, setSelectedRepoPath] = useState(null); - const [manualRepoPath, setManualRepoPath] = useState(''); - const [manualValidation, setManualValidation] = useState<{ - status: 'idle' | 'loading' | 'success' | 'error'; - message?: string; - isValid?: boolean; - path?: string; - }>({ status: 'idle' }); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteConfirmText, setDeleteConfirmText] = useState(''); const saveWorkspaceRequest = useRequest(updateWorkspaceAction); const deleteWorkspaceRequest = useRequest(deleteWorkspaceAction); - const discoverRepositoriesRequest = useRequest(discoverRepositoriesAction); - const validateRepositoryPathRequest = useRequest(validateRepositoryPathAction); const workspaces = useAppStore((state) => state.workspaces.items); const setWorkspaces = useAppStore((state) => state.setWorkspaces); @@ -113,520 +62,6 @@ export function WorkspaceEditClient({ workspace, boards, repositories }: Workspa } }; - const cloneBoard = (board: BoardWithColumns): BoardWithColumns => ({ - ...board, - columns: board.columns.map((column) => ({ ...column })), - }); - - const savedBoardsById = useMemo(() => { - return new Map(savedBoardItems.map((board) => [board.id, board])); - }, [savedBoardItems]); - - const isBoardNameDirty = (board: BoardWithColumns) => { - const saved = savedBoardsById.get(board.id); - if (!saved) return true; - if (board.name !== saved.name || board.description !== saved.description) return true; - return false; - }; - - const areColumnsDirty = (board: BoardWithColumns) => { - const saved = savedBoardsById.get(board.id); - if (!saved) { - return board.columns.length > 0; - } - if (board.columns.length !== saved.columns.length) return true; - const savedColumns = new Map(saved.columns.map((column) => [column.id, column])); - for (const column of board.columns) { - const savedColumn = savedColumns.get(column.id); - if (!savedColumn) return true; - if ( - column.name !== savedColumn.name || - column.color !== savedColumn.color || - column.position !== savedColumn.position || - column.state !== savedColumn.state - ) { - return true; - } - } - return false; - }; - - const isWorkspaceDirty = workspaceNameDraft.trim() !== savedWorkspaceName; - - const cloneRepository = (repo: RepositoryWithScripts): RepositoryWithScripts => ({ - ...repo, - scripts: repo.scripts.map((script) => ({ ...script })), - }); - - const savedRepositoriesById = useMemo(() => { - return new Map(savedRepositoryItems.map((repo) => [repo.id, repo])); - }, [savedRepositoryItems]); - - const filteredRepositories = useMemo(() => { - const query = repoSearch.trim().toLowerCase(); - if (!query) return discoveredRepositories; - return discoveredRepositories.filter( - (repo) => repo.name.toLowerCase().includes(query) || repo.path.toLowerCase().includes(query) - ); - }, [discoveredRepositories, repoSearch]); - - const canSaveLocalRepo = - Boolean(selectedRepoPath) || (manualValidation.status === 'success' && manualValidation.isValid); - - const isRepositoryDirty = (repo: RepositoryWithScripts) => { - const saved = savedRepositoriesById.get(repo.id); - if (!saved) return true; - return ( - repo.name !== saved.name || - repo.source_type !== saved.source_type || - repo.local_path !== saved.local_path || - repo.provider !== saved.provider || - repo.provider_repo_id !== saved.provider_repo_id || - repo.provider_owner !== saved.provider_owner || - repo.provider_name !== saved.provider_name || - repo.default_branch !== saved.default_branch || - repo.setup_script !== saved.setup_script || - repo.cleanup_script !== saved.cleanup_script - ); - }; - - const areRepositoryScriptsDirty = (repo: RepositoryWithScripts) => { - const saved = savedRepositoriesById.get(repo.id); - if (!saved) return repo.scripts.length > 0; - if (repo.scripts.length !== saved.scripts.length) return true; - const savedScripts = new Map(saved.scripts.map((script) => [script.id, script])); - for (const script of repo.scripts) { - const savedScript = savedScripts.get(script.id); - if (!savedScript) return true; - if ( - script.name !== savedScript.name || - script.command !== savedScript.command || - script.position !== savedScript.position - ) { - return true; - } - } - return false; - }; - - const handleUpdateRepository = (repoId: string, updates: Partial) => { - setRepositoryItems((prev) => - prev.map((repo) => (repo.id === repoId ? { ...repo, ...updates } : repo)) - ); - }; - - const handleDiscoverRepositories = async () => { - if (!currentWorkspace) return; - try { - const result = await discoverRepositoriesRequest.run(currentWorkspace.id); - setDiscoveredRepositories(result.repositories); - } catch (error) { - toast({ - title: 'Failed to discover repositories', - description: error instanceof Error ? error.message : 'Request failed', - variant: 'error', - }); - } - }; - - const openLocalRepositoryDialog = async () => { - setLocalRepoDialogOpen(true); - setRepoSearch(''); - setSelectedRepoPath(null); - setManualRepoPath(''); - setManualValidation({ status: 'idle' }); - await handleDiscoverRepositories(); - }; - - const handleValidateManualPath = async () => { - if (!currentWorkspace || !manualRepoPath.trim()) return; - setManualValidation({ status: 'loading' }); - try { - const result = await validateRepositoryPathRequest.run(currentWorkspace.id, manualRepoPath.trim()); - if (result.allowed && result.exists && result.is_git) { - setManualValidation({ - status: 'success', - isValid: true, - message: 'Valid git repository', - path: result.path, - }); - } else { - setManualValidation({ - status: 'error', - isValid: false, - message: result.message || 'Invalid repository path', - path: result.path, - }); - } - } catch (error) { - setManualValidation({ - status: 'error', - isValid: false, - message: error instanceof Error ? error.message : 'Request failed', - }); - } - }; - - const handleConfirmLocalRepository = () => { - if (!currentWorkspace) return; - const selectedRepo = discoveredRepositories.find((repo) => repo.path === selectedRepoPath); - const path = selectedRepo?.path || manualValidation.path || manualRepoPath.trim(); - if (!path) return; - const name = selectedRepo?.name || path.split('/').filter(Boolean).slice(-1)[0] || 'New Repository'; - const draftId = `temp-repo-${generateUUID()}`; - const draftRepo: RepositoryWithScripts = { - id: draftId, - workspace_id: currentWorkspace.id, - name, - source_type: 'local', - local_path: path, - provider: '', - provider_repo_id: '', - provider_owner: '', - provider_name: '', - default_branch: selectedRepo?.default_branch || '', - setup_script: '', - cleanup_script: '', - created_at: '', - updated_at: '', - scripts: [], - }; - setRepositoryItems((prev) => [draftRepo, ...prev]); - setLocalRepoDialogOpen(false); - }; - - const handleAddRepositoryScript = (repoId: string) => { - const script: RepositoryScript = { - id: `temp-script-${generateUUID()}`, - repository_id: repoId, - name: '', - command: '', - position: repositoryItems.find((repo) => repo.id === repoId)?.scripts.length ?? 0, - created_at: '', - updated_at: '', - }; - setRepositoryItems((prev) => - prev.map((repo) => - repo.id === repoId ? { ...repo, scripts: [...repo.scripts, script] } : repo - ) - ); - }; - - const handleUpdateRepositoryScript = (repoId: string, scriptId: string, updates: Partial) => { - setRepositoryItems((prev) => - prev.map((repo) => - repo.id === repoId - ? { - ...repo, - scripts: repo.scripts.map((script) => - script.id === scriptId ? { ...script, ...updates } : script - ), - } - : repo - ) - ); - }; - - const handleDeleteRepositoryScript = (repoId: string, scriptId: string) => { - setRepositoryItems((prev) => - prev.map((repo) => - repo.id === repoId - ? { ...repo, scripts: repo.scripts.filter((script) => script.id !== scriptId) } - : repo - ) - ); - }; - - const handleSaveRepository = async (repoId: string) => { - const repo = repositoryItems.find((item) => item.id === repoId); - if (!repo) return; - if (repoId.startsWith('temp-repo-')) { - const created = await createRepositoryAction({ - workspace_id: currentWorkspace?.id ?? repo.workspace_id, - name: repo.name.trim() || 'New Repository', - source_type: repo.source_type || 'local', - local_path: repo.local_path, - provider: repo.provider, - provider_repo_id: repo.provider_repo_id, - provider_owner: repo.provider_owner, - provider_name: repo.provider_name, - default_branch: repo.default_branch, - setup_script: repo.setup_script, - cleanup_script: repo.cleanup_script, - }); - const scripts = await Promise.all( - repo.scripts.map((script, index) => - createRepositoryScriptAction({ - repository_id: created.id, - name: script.name.trim() || 'New Script', - command: script.command.trim() || 'echo ""', - position: script.position ?? index, - }) - ) - ); - const nextRepo: RepositoryWithScripts = { ...created, scripts }; - setRepositoryItems((prev) => prev.map((item) => (item.id === repoId ? nextRepo : item))); - setSavedRepositoryItems((prev) => [cloneRepository(nextRepo), ...prev]); - return; - } - - const updated = await updateRepositoryAction(repoId, { - name: repo.name, - source_type: repo.source_type, - local_path: repo.local_path, - provider: repo.provider, - provider_repo_id: repo.provider_repo_id, - provider_owner: repo.provider_owner, - provider_name: repo.provider_name, - default_branch: repo.default_branch, - setup_script: repo.setup_script, - cleanup_script: repo.cleanup_script, - }); - - const saved = savedRepositoriesById.get(repoId); - const savedScripts = saved ? saved.scripts : []; - const currentScriptIds = new Set(repo.scripts.map((script) => script.id)); - - await Promise.all( - savedScripts - .filter((script) => !currentScriptIds.has(script.id)) - .map((script) => deleteRepositoryScriptAction(script.id)) - ); - - const nextScripts = await Promise.all( - repo.scripts.map((script, index) => { - if (script.id.startsWith('temp-script-')) { - return createRepositoryScriptAction({ - repository_id: repoId, - name: script.name.trim() || 'New Script', - command: script.command.trim() || 'echo ""', - position: script.position ?? index, - }); - } - return updateRepositoryScriptAction(script.id, { - name: script.name, - command: script.command, - position: script.position ?? index, - }); - }) - ); - - const nextRepo: RepositoryWithScripts = { ...updated, scripts: nextScripts }; - setRepositoryItems((prev) => prev.map((item) => (item.id === repoId ? nextRepo : item))); - setSavedRepositoryItems((prev) => - prev.some((item) => item.id === repoId) - ? prev.map((item) => (item.id === repoId ? cloneRepository(nextRepo) : item)) - : [...prev, cloneRepository(nextRepo)] - ); - }; - - const handleDeleteRepository = async (repoId: string) => { - if (repoId.startsWith('temp-repo-')) { - setRepositoryItems((prev) => prev.filter((repo) => repo.id !== repoId)); - return; - } - await deleteRepositoryAction(repoId); - setRepositoryItems((prev) => prev.filter((repo) => repo.id !== repoId)); - setSavedRepositoryItems((prev) => prev.filter((repo) => repo.id !== repoId)); - }; - - const handleAddBoard = () => { - if (!currentWorkspace) return; - const draftBoardId = `temp-${generateUUID()}`; - const draftBoard: BoardWithColumns = { - id: draftBoardId, - workspace_id: currentWorkspace.id, - name: 'New Board', - description: '', - created_at: '', - updated_at: '', - columns: [ - { - id: `temp-col-${generateUUID()}`, - board_id: draftBoardId, - name: 'Todo', - position: 0, - state: 'TODO', - color: 'bg-cyan-500', - created_at: '', - updated_at: '', - }, - { - id: `temp-col-${generateUUID()}`, - board_id: draftBoardId, - name: 'In Progress', - position: 1, - state: 'IN_PROGRESS', - color: 'bg-yellow-500', - created_at: '', - updated_at: '', - }, - { - id: `temp-col-${generateUUID()}`, - board_id: draftBoardId, - name: 'To Review', - position: 2, - state: 'REVIEW', - color: 'bg-green-500', - created_at: '', - updated_at: '', - }, - { - id: `temp-col-${generateUUID()}`, - board_id: draftBoardId, - name: 'Done', - position: 3, - state: 'COMPLETED', - color: 'bg-indigo-500', - created_at: '', - updated_at: '', - }, - ], - }; - setBoardItems((prev) => [draftBoard, ...prev]); - }; - - const handleUpdateBoard = (boardId: string, updates: { name?: string; description?: string }) => { - setBoardItems((prev) => - prev.map((board) => (board.id === boardId ? { ...board, ...updates } : board)) - ); - }; - - const handleDeleteBoard = async (boardId: string) => { - if (boardId.startsWith('temp-')) { - setBoardItems((prev) => prev.filter((board) => board.id !== boardId)); - return; - } - await deleteBoardAction(boardId); - setBoardItems((prev) => prev.filter((board) => board.id !== boardId)); - setSavedBoardItems((prev) => prev.filter((board) => board.id !== boardId)); - }; - - const handleCreateColumn = ( - boardId: string, - column: Omit - ) => { - const draftColumn: Column = { - id: `temp-col-${generateUUID()}`, - board_id: boardId, - name: column.name, - position: column.position, - state: column.state, - color: column.color, - created_at: '', - updated_at: '', - }; - setBoardItems((prev) => - prev.map((board) => - board.id === boardId ? { ...board, columns: [...board.columns, draftColumn] } : board - ) - ); - }; - - const handleUpdateColumn = (boardId: string, columnId: string, updates: Partial) => { - setBoardItems((prev) => - prev.map((board) => - board.id === boardId - ? { - ...board, - columns: board.columns.map((column) => (column.id === columnId ? { ...column, ...updates } : column)), - } - : board - ) - ); - }; - - const handleDeleteColumn = async (boardId: string, columnId: string) => { - if (boardId.startsWith('temp-')) { - setBoardItems((prev) => - prev.map((board) => - board.id === boardId ? { ...board, columns: board.columns.filter((column) => column.id !== columnId) } : board - ) - ); - return; - } - await deleteColumnAction(columnId); - setBoardItems((prev) => - prev.map((board) => - board.id === boardId ? { ...board, columns: board.columns.filter((column) => column.id !== columnId) } : board - ) - ); - }; - - const handleReorderColumns = (boardId: string, columns: Column[]) => { - setBoardItems((prev) => - prev.map((board) => (board.id === boardId ? { ...board, columns } : board)) - ); - }; - - const handleSaveBoard = async (boardId: string) => { - const board = boardItems.find((item) => item.id === boardId); - if (!board) return; - if (boardId.startsWith('temp-')) { - const name = board.name.trim() || 'New Board'; - const createdBoard = await createBoardAction({ - workspace_id: currentWorkspace?.id ?? board.workspace_id, - name, - }); - const createdColumns = await Promise.all( - board.columns.map((column, index) => - createColumnAction({ - board_id: createdBoard.id, - name: column.name.trim() || 'New Column', - position: column.position ?? index, - state: column.state, - color: column.color, - }) - ) - ); - const nextBoard = { - ...createdBoard, - columns: createdColumns, - }; - setBoardItems((prev) => - prev.map((item) => (item.id === boardId ? nextBoard : item)) - ); - setSavedBoardItems((prev) => [cloneBoard(nextBoard), ...prev]); - return; - } - const updates: { name?: string; description?: string } = {}; - if (board.name.trim()) { - updates.name = board.name.trim(); - } - if (Object.keys(updates).length) { - await updateBoardAction(boardId, updates); - } - const nextColumns = await Promise.all( - board.columns.map((column, index) => { - if (column.id.startsWith('temp-col-')) { - return createColumnAction({ - board_id: boardId, - name: column.name.trim() || 'New Column', - position: column.position ?? index, - state: column.state, - color: column.color, - }); - } - return updateColumnAction(column.id, { - name: column.name, - color: column.color, - position: column.position ?? index, - state: column.state, - }); - }) - ); - setBoardItems((prev) => - prev.map((item) => (item.id === boardId ? { ...item, columns: nextColumns } : item)) - ); - setSavedBoardItems((prev) => - prev.some((item) => item.id === boardId) - ? prev.map((item) => - item.id === boardId ? cloneBoard({ ...board, columns: nextColumns }) : item - ) - : [...prev, cloneBoard({ ...board, columns: nextColumns })] - ); - }; - const handleDeleteWorkspace = async () => { if (deleteConfirmText !== 'delete' || !currentWorkspace) return; try { @@ -662,7 +97,7 @@ export function WorkspaceEditClient({ workspace, boards, repositories }: Workspa

{currentWorkspace.name}

- Manage repositories and boards for this workspace + Manage workspace details and jump into boards or repositories.

@@ -672,7 +107,7 @@ export function WorkspaceEditClient({ workspace, boards, repositories }: Workspa Workspace Name - {isWorkspaceDirty && } + {workspaceNameDraft.trim() !== savedWorkspaceName && } @@ -683,7 +118,7 @@ export function WorkspaceEditClient({ workspace, boards, repositories }: Workspa />
- } - title="Repositories" - description="Repositories in this workspace" - action={ -
- -
- } - > -
- {repositoryItems.map((repo) => ( - - ))} -
-
- - - - - - - Add Local Repository - - Select a discovered repository or provide an absolute path to validate. - - -
-
- - setRepoSearch(e.target.value)} - /> -
- {discoverRepositoriesRequest.isLoading ? ( -
- - Scanning repositories... -
- ) : filteredRepositories.length === 0 ? ( -
No repositories found.
- ) : ( - filteredRepositories.map((repo) => ( - - )) - )} -
-
- -
- -
- { - setManualRepoPath(e.target.value); - setSelectedRepoPath(null); - setManualValidation({ status: 'idle' }); - }} - /> - -
- {manualValidation.status === 'error' && ( -

{manualValidation.message}

- )} - {manualValidation.status === 'success' && ( -

{manualValidation.message}

- )} -
-
- - - - -
-
- - - - - Add Remote Repository - Remote providers are coming soon. - -
- - - -
- - - -
-
- - } - title="Boards" - description="Boards and columns in this workspace" - action={ - - } - > -
- {boardItems.map((board) => ( - handleUpdateBoard(board.id, updates)} - onDeleteBoard={() => handleDeleteBoard(board.id)} - onCreateColumn={(column) => handleCreateColumn(board.id, column)} - onUpdateColumn={(columnId, updates) => handleUpdateColumn(board.id, columnId, updates)} - onDeleteColumn={(columnId) => handleDeleteColumn(board.id, columnId)} - onReorderColumns={(columns) => handleReorderColumns(board.id, columns)} - onSaveBoard={() => handleSaveBoard(board.id)} - /> - ))} -
-
+ + diff --git a/apps/web/app/settings/workspace/workspace-repositories-client.tsx b/apps/web/app/settings/workspace/workspace-repositories-client.tsx new file mode 100644 index 0000000000..14bcb99b3b --- /dev/null +++ b/apps/web/app/settings/workspace/workspace-repositories-client.tsx @@ -0,0 +1,524 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { IconGitBranch, IconLoader2 } from '@tabler/icons-react'; +import { Button } from '@kandev/ui/button'; +import { Input } from '@kandev/ui/input'; +import { Label } from '@kandev/ui/label'; +import { Card, CardContent } from '@kandev/ui/card'; +import { Separator } from '@kandev/ui/separator'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@kandev/ui/dialog'; +import { SettingsSection } from '@/components/settings/settings-section'; +import { RepositoryCard } from '@/components/settings/repository-card'; +import { generateUUID } from '@/lib/utils'; +import { + createRepositoryAction, + createRepositoryScriptAction, + deleteRepositoryAction, + deleteRepositoryScriptAction, + discoverRepositoriesAction, + updateRepositoryAction, + updateRepositoryScriptAction, + validateRepositoryPathAction, +} from '@/app/actions/workspaces'; +import type { LocalRepository, Repository, RepositoryScript, Workspace } from '@/lib/types/http'; +import { useRequest } from '@/lib/http/use-request'; +import { useToast } from '@/components/toast-provider'; + +type RepositoryWithScripts = Repository & { scripts: RepositoryScript[] }; + +type WorkspaceRepositoriesClientProps = { + workspace: Workspace | null; + repositories: RepositoryWithScripts[]; +}; + +export function WorkspaceRepositoriesClient({ + workspace, + repositories, +}: WorkspaceRepositoriesClientProps) { + const router = useRouter(); + const { toast } = useToast(); + const [repositoryItems, setRepositoryItems] = useState(repositories); + const [savedRepositoryItems, setSavedRepositoryItems] = useState(repositories); + const [discoveredRepositories, setDiscoveredRepositories] = useState([]); + const [localRepoDialogOpen, setLocalRepoDialogOpen] = useState(false); + const [remoteRepoDialogOpen, setRemoteRepoDialogOpen] = useState(false); + const [repoSearch, setRepoSearch] = useState(''); + const [selectedRepoPath, setSelectedRepoPath] = useState(null); + const [manualRepoPath, setManualRepoPath] = useState(''); + const [manualValidation, setManualValidation] = useState<{ + status: 'idle' | 'loading' | 'success' | 'error'; + message?: string; + isValid?: boolean; + path?: string; + }>({ status: 'idle' }); + const discoverRepositoriesRequest = useRequest(discoverRepositoriesAction); + const validateRepositoryPathRequest = useRequest(validateRepositoryPathAction); + + const cloneRepository = (repo: RepositoryWithScripts): RepositoryWithScripts => ({ + ...repo, + scripts: repo.scripts.map((script) => ({ ...script })), + }); + + const savedRepositoriesById = useMemo(() => { + return new Map(savedRepositoryItems.map((repo) => [repo.id, repo])); + }, [savedRepositoryItems]); + + const filteredRepositories = useMemo(() => { + const query = repoSearch.trim().toLowerCase(); + if (!query) return discoveredRepositories; + return discoveredRepositories.filter( + (repo) => repo.name.toLowerCase().includes(query) || repo.path.toLowerCase().includes(query) + ); + }, [discoveredRepositories, repoSearch]); + + const canSaveLocalRepo = + Boolean(selectedRepoPath) || (manualValidation.status === 'success' && manualValidation.isValid); + + const isRepositoryDirty = (repo: RepositoryWithScripts) => { + const saved = savedRepositoriesById.get(repo.id); + if (!saved) return true; + return ( + repo.name !== saved.name || + repo.source_type !== saved.source_type || + repo.local_path !== saved.local_path || + repo.provider !== saved.provider || + repo.provider_repo_id !== saved.provider_repo_id || + repo.provider_owner !== saved.provider_owner || + repo.provider_name !== saved.provider_name || + repo.default_branch !== saved.default_branch || + repo.setup_script !== saved.setup_script || + repo.cleanup_script !== saved.cleanup_script + ); + }; + + const areRepositoryScriptsDirty = (repo: RepositoryWithScripts) => { + const saved = savedRepositoriesById.get(repo.id); + if (!saved) return repo.scripts.length > 0; + if (repo.scripts.length !== saved.scripts.length) return true; + const savedScripts = new Map(saved.scripts.map((script) => [script.id, script])); + for (const script of repo.scripts) { + const savedScript = savedScripts.get(script.id); + if (!savedScript) return true; + if ( + script.name !== savedScript.name || + script.command !== savedScript.command || + script.position !== savedScript.position + ) { + return true; + } + } + return false; + }; + + const handleUpdateRepository = (repoId: string, updates: Partial) => { + setRepositoryItems((prev) => + prev.map((repo) => (repo.id === repoId ? { ...repo, ...updates } : repo)) + ); + }; + + const handleDiscoverRepositories = async () => { + if (!workspace) return; + try { + const result = await discoverRepositoriesRequest.run(workspace.id); + setDiscoveredRepositories(result.repositories); + } catch (error) { + toast({ + title: 'Failed to discover repositories', + description: error instanceof Error ? error.message : 'Request failed', + variant: 'error', + }); + } + }; + + const openLocalRepositoryDialog = async () => { + setLocalRepoDialogOpen(true); + setRepoSearch(''); + setSelectedRepoPath(null); + setManualRepoPath(''); + setManualValidation({ status: 'idle' }); + await handleDiscoverRepositories(); + }; + + const handleValidateManualPath = async () => { + if (!workspace || !manualRepoPath.trim()) return; + setManualValidation({ status: 'loading' }); + try { + const result = await validateRepositoryPathRequest.run(workspace.id, manualRepoPath.trim()); + if (result.allowed && result.exists && result.is_git) { + setManualValidation({ + status: 'success', + isValid: true, + message: 'Valid git repository', + path: result.path, + }); + } else { + setManualValidation({ + status: 'error', + isValid: false, + message: result.message || 'Invalid repository path', + path: result.path, + }); + } + } catch (error) { + setManualValidation({ + status: 'error', + isValid: false, + message: error instanceof Error ? error.message : 'Request failed', + }); + } + }; + + const handleConfirmLocalRepository = () => { + if (!workspace) return; + const selectedRepo = discoveredRepositories.find((repo) => repo.path === selectedRepoPath); + const path = selectedRepo?.path || manualValidation.path || manualRepoPath.trim(); + if (!path) return; + const name = selectedRepo?.name || path.split('/').filter(Boolean).slice(-1)[0] || 'New Repository'; + const draftId = `temp-repo-${generateUUID()}`; + const draftRepo: RepositoryWithScripts = { + id: draftId, + workspace_id: workspace.id, + name, + source_type: 'local', + local_path: path, + provider: '', + provider_repo_id: '', + provider_owner: '', + provider_name: '', + default_branch: selectedRepo?.default_branch || '', + setup_script: '', + cleanup_script: '', + created_at: '', + updated_at: '', + scripts: [], + }; + setRepositoryItems((prev) => [draftRepo, ...prev]); + setLocalRepoDialogOpen(false); + }; + + const handleAddRepositoryScript = (repoId: string) => { + const script: RepositoryScript = { + id: `temp-script-${generateUUID()}`, + repository_id: repoId, + name: '', + command: '', + position: repositoryItems.find((repo) => repo.id === repoId)?.scripts.length ?? 0, + created_at: '', + updated_at: '', + }; + setRepositoryItems((prev) => + prev.map((repo) => + repo.id === repoId ? { ...repo, scripts: [...repo.scripts, script] } : repo + ) + ); + }; + + const handleUpdateRepositoryScript = ( + repoId: string, + scriptId: string, + updates: Partial + ) => { + setRepositoryItems((prev) => + prev.map((repo) => + repo.id === repoId + ? { + ...repo, + scripts: repo.scripts.map((script) => + script.id === scriptId ? { ...script, ...updates } : script + ), + } + : repo + ) + ); + }; + + const handleDeleteRepositoryScript = (repoId: string, scriptId: string) => { + setRepositoryItems((prev) => + prev.map((repo) => + repo.id === repoId + ? { ...repo, scripts: repo.scripts.filter((script) => script.id !== scriptId) } + : repo + ) + ); + }; + + const handleSaveRepository = async (repoId: string) => { + const repo = repositoryItems.find((item) => item.id === repoId); + if (!repo) return; + if (repoId.startsWith('temp-repo-')) { + const created = await createRepositoryAction({ + workspace_id: workspace?.id ?? repo.workspace_id, + name: repo.name.trim() || 'New Repository', + source_type: repo.source_type || 'local', + local_path: repo.local_path, + provider: repo.provider, + provider_repo_id: repo.provider_repo_id, + provider_owner: repo.provider_owner, + provider_name: repo.provider_name, + default_branch: repo.default_branch, + setup_script: repo.setup_script, + cleanup_script: repo.cleanup_script, + }); + const scripts = await Promise.all( + repo.scripts.map((script, index) => + createRepositoryScriptAction({ + repository_id: created.id, + name: script.name.trim() || 'New Script', + command: script.command.trim() || 'echo ""', + position: script.position ?? index, + }) + ) + ); + const nextRepo: RepositoryWithScripts = { ...created, scripts }; + setRepositoryItems((prev) => prev.map((item) => (item.id === repoId ? nextRepo : item))); + setSavedRepositoryItems((prev) => [cloneRepository(nextRepo), ...prev]); + return; + } + + const updated = await updateRepositoryAction(repoId, { + name: repo.name, + source_type: repo.source_type, + local_path: repo.local_path, + provider: repo.provider, + provider_repo_id: repo.provider_repo_id, + provider_owner: repo.provider_owner, + provider_name: repo.provider_name, + default_branch: repo.default_branch, + setup_script: repo.setup_script, + cleanup_script: repo.cleanup_script, + }); + + const saved = savedRepositoriesById.get(repoId); + const savedScripts = saved ? saved.scripts : []; + const currentScriptIds = new Set(repo.scripts.map((script) => script.id)); + + await Promise.all( + savedScripts + .filter((script) => !currentScriptIds.has(script.id)) + .map((script) => deleteRepositoryScriptAction(script.id)) + ); + + const nextScripts = await Promise.all( + repo.scripts.map((script, index) => { + if (script.id.startsWith('temp-script-')) { + return createRepositoryScriptAction({ + repository_id: repoId, + name: script.name.trim() || 'New Script', + command: script.command.trim() || 'echo ""', + position: script.position ?? index, + }); + } + return updateRepositoryScriptAction(script.id, { + name: script.name, + command: script.command, + position: script.position ?? index, + }); + }) + ); + + const nextRepo: RepositoryWithScripts = { ...updated, scripts: nextScripts }; + setRepositoryItems((prev) => prev.map((item) => (item.id === repoId ? nextRepo : item))); + setSavedRepositoryItems((prev) => + prev.some((item) => item.id === repoId) + ? prev.map((item) => (item.id === repoId ? cloneRepository(nextRepo) : item)) + : [...prev, cloneRepository(nextRepo)] + ); + }; + + const handleDeleteRepository = async (repoId: string) => { + if (repoId.startsWith('temp-repo-')) { + setRepositoryItems((prev) => prev.filter((repo) => repo.id !== repoId)); + return; + } + await deleteRepositoryAction(repoId); + setRepositoryItems((prev) => prev.filter((repo) => repo.id !== repoId)); + setSavedRepositoryItems((prev) => prev.filter((repo) => repo.id !== repoId)); + }; + + if (!workspace) { + return ( +
+ + +

Workspace not found

+ +
+
+
+ ); + } + + return ( +
+
+
+

{workspace.name}

+

+ Manage repositories connected to this workspace. +

+
+ +
+ + + + } + title="Repositories" + description="Repositories in this workspace" + action={ +
+ + +
+ } + > +
+ {repositoryItems.map((repo) => ( + + ))} +
+
+ + + + + Add Local Repository + + Select a discovered repository or provide an absolute path to validate. + + +
+
+ + setRepoSearch(e.target.value)} + /> +
+ {discoverRepositoriesRequest.isLoading ? ( +
+ + Scanning repositories... +
+ ) : filteredRepositories.length === 0 ? ( +
No repositories found.
+ ) : ( + filteredRepositories.map((repo) => ( + + )) + )} +
+
+ +
+ +
+ { + setManualRepoPath(e.target.value); + setSelectedRepoPath(null); + setManualValidation({ status: 'idle' }); + }} + /> + +
+ {manualValidation.status === 'error' && ( +

{manualValidation.message}

+ )} + {manualValidation.status === 'success' && ( +

{manualValidation.message}

+ )} +
+
+ + + + +
+
+ + + + + Add Remote Repository + Remote providers are coming soon. + +
+ + + +
+ + + +
+
+
+ ); +} diff --git a/apps/web/app/task/[id]/page-client.tsx b/apps/web/app/task/[id]/page-client.tsx index d2619e71ab..d9e83b9ac9 100644 --- a/apps/web/app/task/[id]/page-client.tsx +++ b/apps/web/app/task/[id]/page-client.tsx @@ -18,7 +18,6 @@ import { listRepositories, listRepositoryBranches } from '@/lib/http/client'; import { useRequest } from '@/lib/http/use-request'; import { getWebSocketClient } from '@/lib/ws/connection'; import { useAppStoreApi } from '@/components/state-provider'; -import { useAppStore } from '@/components/state-provider'; const AGENTS = [ { id: 'codex', label: 'Codex' }, @@ -49,11 +48,6 @@ export default function TaskPage({ task: initialTask }: TaskPageClientProps) { // Use task from props but allow updates const task = initialTask; - // Track task state from store (kanban.tasks) to determine if agent is running - const taskFromStore = useAppStore((state) => - state.kanban.tasks.find((t) => t.id === task?.id) - ); - useEffect(() => { setIsMounted(true); }, []); @@ -92,7 +86,8 @@ export default function TaskPage({ task: initialTask }: TaskPageClientProps) { const branchResponse = await listRepositoryBranches(getBackendConfig().apiBaseUrl, repo.id); return branchResponse.branches; }, []); - const branchesRequest = useRequest(fetchBranches); + const { run: runBranches, data: branchesData, isLoading: branchesLoading } = + useRequest(fetchBranches); // Fetch comments on mount and when task changes useEffect(() => { @@ -215,8 +210,8 @@ export default function TaskPage({ task: initialTask }: TaskPageClientProps) { useEffect(() => { if (!task?.workspace_id || !task.repository_url) return; - branchesRequest.run(task.workspace_id, task.repository_url).catch(() => {}); - }, [branchesRequest.run, task?.repository_url, task?.workspace_id]); + runBranches(task.workspace_id, task.repository_url).catch(() => {}); + }, [runBranches, task?.repository_url, task?.workspace_id]); if (!isMounted) { return
; @@ -228,8 +223,8 @@ export default function TaskPage({ task: initialTask }: TaskPageClientProps) { {task?.id ? ( - {workspaces.map((workspace) => ( - - - - {workspace.name} - - - - ))} + {workspaces.map((workspace) => { + const workspacePath = `/settings/workspace/${workspace.id}`; + const boardsPath = `${workspacePath}/boards`; + const repositoriesPath = `${workspacePath}/repositories`; + + return ( + + + + {workspace.name} + + + + + + + Repositories + + + + + + + Boards + + + + + + ); + })} diff --git a/apps/web/components/settings/settings-mobile-nav.tsx b/apps/web/components/settings/settings-mobile-nav.tsx deleted file mode 100644 index 975513ce57..0000000000 --- a/apps/web/components/settings/settings-mobile-nav.tsx +++ /dev/null @@ -1,24 +0,0 @@ -'use client'; - -import { Sheet, SheetContent } from '@kandev/ui/sheet'; -import { SettingsSidebar } from './settings-sidebar'; - -type SettingsMobileNavProps = { - open: boolean; - onOpenChange: (open: boolean) => void; -}; - -export function SettingsMobileNav({ open, onOpenChange }: SettingsMobileNavProps) { - return ( - - -
- onOpenChange(false)} - /> -
-
-
- ); -} diff --git a/apps/web/components/settings/settings-sidebar.tsx b/apps/web/components/settings/settings-sidebar.tsx deleted file mode 100644 index a63b3753f0..0000000000 --- a/apps/web/components/settings/settings-sidebar.tsx +++ /dev/null @@ -1,66 +0,0 @@ -'use client'; - -import Link from 'next/link'; -import { usePathname } from 'next/navigation'; -import { IconSettings, IconFolder, IconServer, IconRobot } from '@tabler/icons-react'; -import { cn } from '@/lib/utils'; - -const NAV_ITEMS = [ - { - href: '/settings/general', - label: 'General', - icon: IconSettings, - }, - { - href: '/settings/workspace', - label: 'Workspace', - icon: IconFolder, - }, - { - href: '/settings/environments', - label: 'Environments', - icon: IconServer, - }, - { - href: '/settings/agents', - label: 'Agents', - icon: IconRobot, - }, -]; - -type SettingsSidebarProps = { - className?: string; - onNavigate?: () => void; -}; - -export function SettingsSidebar({ className, onNavigate }: SettingsSidebarProps) { - const pathname = usePathname(); - - return ( - - ); -} diff --git a/apps/web/components/task/task-changes-panel.tsx b/apps/web/components/task/task-changes-panel.tsx index c0ad8f05d7..34ea6822c4 100644 --- a/apps/web/components/task/task-changes-panel.tsx +++ b/apps/web/components/task/task-changes-panel.tsx @@ -1,6 +1,6 @@ 'use client'; -import { memo, useEffect, useMemo, useState } from 'react'; +import { memo, useMemo, useState } from 'react'; import { DiffModeEnum, DiffView } from '@git-diff-view/react'; import { IconArrowBackUp, IconCopy, IconLayoutColumns, IconLayoutRows } from '@tabler/icons-react'; import { useTheme } from 'next-themes'; @@ -22,13 +22,11 @@ const TaskChangesPanel = memo(function TaskChangesPanel({ selectedDiffPath, onClearSelected, }: TaskChangesPanelProps) { - const [diffViewMode, setDiffViewMode] = useState<'unified' | 'split'>(DEFAULT_DIFF_MODE); + const [diffViewMode, setDiffViewMode] = useState<'unified' | 'split'>(() => + getLocalStorage('task-diff-view-mode', DEFAULT_DIFF_MODE) + ); const { resolvedTheme } = useTheme(); - useEffect(() => { - setDiffViewMode(getLocalStorage('task-diff-view-mode', DEFAULT_DIFF_MODE)); - }, []); - const diffTargets = useMemo( () => (selectedDiffPath ? [selectedDiffPath] : CHANGED_FILES.map((file) => file.path)), [selectedDiffPath] diff --git a/apps/web/components/task/task-chat-panel.tsx b/apps/web/components/task/task-chat-panel.tsx index e69b4f4f70..ff5ed43cc1 100644 --- a/apps/web/components/task/task-chat-panel.tsx +++ b/apps/web/components/task/task-chat-panel.tsx @@ -40,7 +40,6 @@ type AgentOption = { }; type TaskChatPanelProps = { - taskId: string; agents: AgentOption[]; onSend: (message: string) => void; isLoading?: boolean; @@ -56,25 +55,25 @@ type ToolCallMetadata = { result?: string; }; -function getToolIcon(toolName?: string) { +function getToolIconElement(toolName?: string, className?: string) { const name = toolName?.toLowerCase() ?? ''; // Match ACP ToolKind values: read, edit, delete, move, search, execute if (name === 'edit' || name.includes('edit') || name.includes('replace') || name.includes('write') || name.includes('save')) { - return IconEdit; + return ; } if (name === 'read' || name.includes('view') || name.includes('read')) { - return IconEye; + return ; } if (name === 'search' || name.includes('search') || name.includes('find') || name.includes('retrieval')) { - return IconSearch; + return ; } if (name === 'execute' || name.includes('terminal') || name.includes('exec') || name.includes('launch') || name.includes('process')) { - return IconTerminal2; + return ; } if (name === 'delete' || name === 'move' || name.includes('file') || name.includes('create')) { - return IconFile; + return ; } - return IconCode; + return ; } function getStatusIcon(status?: string) { @@ -100,7 +99,10 @@ function ToolCallCard({ comment }: { comment: Comment }) { const args = metadata?.args; const result = metadata?.result; - const ToolIcon = getToolIcon(toolName); + const toolIcon = getToolIconElement( + toolName, + 'h-4 w-4 text-amber-600 dark:text-amber-400 flex-shrink-0' + ); const hasDetails = args && Object.keys(args).length > 0; // Extract file path from various possible sources @@ -128,7 +130,7 @@ function ToolCallCard({ comment }: { comment: Comment }) { )} disabled={!hasDetails} > - + {toolIcon} {title} @@ -200,7 +202,7 @@ function TypingIndicator() { ); } -export function TaskChatPanel({ taskId, agents, onSend, isLoading, isAgentWorking }: TaskChatPanelProps) { +export function TaskChatPanel({ agents, onSend, isLoading, isAgentWorking }: TaskChatPanelProps) { const [messageInput, setMessageInput] = useState(''); const [selectedAgent, setSelectedAgent] = useState(agents[0]?.id ?? ''); const [planModeEnabled, setPlanModeEnabled] = useState(false); diff --git a/apps/web/lib/ws/client.ts b/apps/web/lib/ws/client.ts index 74886ec34b..f986a303dc 100644 --- a/apps/web/lib/ws/client.ts +++ b/apps/web/lib/ws/client.ts @@ -29,10 +29,6 @@ const DEFAULT_RECONNECT_OPTIONS: Required = { backoffMultiplier: 1.5, }; -// Heartbeat configuration -const HEARTBEAT_INTERVAL = 30000; // 30 seconds -const HEARTBEAT_TIMEOUT = 5000; // 5 seconds - export class WebSocketClient { private socket: WebSocket | null = null; private status: WebSocketStatus = 'idle';