diff --git a/src/web-server/routes/account-routes.ts b/src/web-server/routes/account-routes.ts index 6b512b3f..686c604f 100644 --- a/src/web-server/routes/account-routes.ts +++ b/src/web-server/routes/account-routes.ts @@ -82,4 +82,50 @@ router.post('/default', (req: Request, res: Response): void => { } }); +/** + * DELETE /api/accounts/reset-default - Reset to CCS default + */ +router.delete('/reset-default', (_req: Request, res: Response): void => { + try { + if (isUnifiedMode()) { + registry.clearDefaultUnified(); + } else { + registry.clearDefaultProfile(); + } + res.json({ success: true, default: null }); + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } +}); + +/** + * DELETE /api/accounts/:name - Delete an account + */ +router.delete('/:name', (req: Request, res: Response): void => { + try { + const { name } = req.params; + + if (!name) { + res.status(400).json({ error: 'Missing account name' }); + return; + } + + // Check if trying to delete default + const currentDefault = registry.getDefaultUnified() ?? registry.getDefaultProfile(); + if (name === currentDefault) { + res + .status(400) + .json({ error: 'Cannot delete the default account. Set a different default first.' }); + return; + } + + // Delete the profile + registry.deleteProfile(name); + + res.json({ success: true, deleted: name }); + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } +}); + export default router; diff --git a/ui/src/components/account/accounts-table.tsx b/ui/src/components/account/accounts-table.tsx index 65865f03..d90969c9 100644 --- a/ui/src/components/account/accounts-table.tsx +++ b/ui/src/components/account/accounts-table.tsx @@ -1,8 +1,9 @@ /** * Accounts Table Component - * Phase 03: REST API Routes & CRUD + * Dashboard parity: Full CRUD for auth profiles */ +import { useState } from 'react'; import { useReactTable, getCoreRowModel, flexRender, type ColumnDef } from '@tanstack/react-table'; import { Table, @@ -13,17 +14,35 @@ import { TableRow, } from '@/components/ui/table'; import { Button } from '@/components/ui/button'; -import { Check } from 'lucide-react'; -import { useSetDefaultAccount } from '@/hooks/use-accounts'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import { Check, Trash2, RotateCcw } from 'lucide-react'; +import { + useSetDefaultAccount, + useDeleteAccount, + useResetDefaultAccount, +} from '@/hooks/use-accounts'; import type { Account } from '@/lib/api-client'; interface AccountsTableProps { data: Account[]; defaultAccount: string | null; + onRefresh?: () => void; } export function AccountsTable({ data, defaultAccount }: AccountsTableProps) { const setDefaultMutation = useSetDefaultAccount(); + const deleteMutation = useDeleteAccount(); + const resetDefaultMutation = useResetDefaultAccount(); + const [deleteTarget, setDeleteTarget] = useState(null); const columns: ColumnDef[] = [ { @@ -71,20 +90,34 @@ export function AccountsTable({ data, defaultAccount }: AccountsTableProps) { { id: 'actions', header: 'Actions', - size: 100, + size: 180, cell: ({ row }) => { const isDefault = row.original.name === defaultAccount; + const isPending = setDefaultMutation.isPending || deleteMutation.isPending; + return ( - +
+ + +
); }, }, @@ -100,51 +133,97 @@ export function AccountsTable({ data, defaultAccount }: AccountsTableProps) { if (data.length === 0) { return (
- No accounts found. Use ccs login to - add accounts. + No accounts found. Use{' '} + ccs auth create to add accounts.
); } return ( -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - const widthClass = - { - name: 'w-[200px]', - type: 'w-[100px]', - created: 'w-[150px]', - last_used: 'w-[150px]', - actions: 'w-[100px]', - }[header.id] || 'w-auto'; + <> +
+
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const widthClass = + { + name: 'w-[200px]', + type: 'w-[100px]', + created: 'w-[150px]', + last_used: 'w-[150px]', + actions: 'w-[180px]', + }[header.id] || 'w-auto'; - return ( - - {header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext())} - - ); - })} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - + return ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + ); + })} + ))} - - ))} - -
-
+ + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + + + + + {/* Reset default button */} + {defaultAccount && ( +
+ +
+ )} + + + {/* Delete confirmation dialog */} + !open && setDeleteTarget(null)}> + + + Delete Account + + Are you sure you want to delete the account "{deleteTarget}"? This will + remove the profile and all its session data. This action cannot be undone. + + + + Cancel + { + if (deleteTarget) { + deleteMutation.mutate(deleteTarget); + setDeleteTarget(null); + } + }} + > + Delete + + + + + ); } diff --git a/ui/src/components/account/create-auth-profile-dialog.tsx b/ui/src/components/account/create-auth-profile-dialog.tsx new file mode 100644 index 00000000..2da65f82 --- /dev/null +++ b/ui/src/components/account/create-auth-profile-dialog.tsx @@ -0,0 +1,124 @@ +/** + * Create Auth Profile Dialog + * Shows CLI command for creating auth profiles (Dashboard cannot spawn Claude login directly) + */ + +import { useState } from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Copy, Check, Terminal } from 'lucide-react'; + +interface CreateAuthProfileDialogProps { + open: boolean; + onClose: () => void; +} + +export function CreateAuthProfileDialog({ open, onClose }: CreateAuthProfileDialogProps) { + const [profileName, setProfileName] = useState(''); + const [copied, setCopied] = useState(false); + + // Validate profile name: alphanumeric, dash, underscore only + const isValidName = /^[a-zA-Z][a-zA-Z0-9_-]*$/.test(profileName); + const command = profileName ? `ccs auth create ${profileName}` : 'ccs auth create '; + + const handleCopy = async () => { + if (!isValidName) return; + await navigator.clipboard.writeText(command); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + const handleClose = () => { + setProfileName(''); + setCopied(false); + onClose(); + }; + + return ( + !isOpen && handleClose()}> + + + Create New Account + + Auth profiles require Claude CLI login. Run the command below in your terminal. + + + +
+
+ + setProfileName(e.target.value)} + placeholder="e.g., work, personal, client" + autoComplete="off" + /> + {profileName && !isValidName && ( +

+ Name must start with a letter and contain only letters, numbers, dashes, or + underscores. +

+ )} +
+ +
+ +
+ + {command} + +
+
+ +
+

After running the command:

+
    +
  1. Complete the Claude login in your browser
  2. +
  3. Return here and refresh to see the new account
  4. +
+
+ +
+ + +
+
+
+
+ ); +} diff --git a/ui/src/components/account/index.ts b/ui/src/components/account/index.ts index ba0cf9b4..0b75ccb8 100644 --- a/ui/src/components/account/index.ts +++ b/ui/src/components/account/index.ts @@ -5,6 +5,7 @@ // Main components export { AccountsTable } from './accounts-table'; export { AddAccountDialog } from './add-account-dialog'; +export { CreateAuthProfileDialog } from './create-auth-profile-dialog'; // Flow visualization (from subdirectory) export { AccountFlowViz } from './flow-viz'; diff --git a/ui/src/hooks/use-accounts.ts b/ui/src/hooks/use-accounts.ts index a74422f2..b46691af 100644 --- a/ui/src/hooks/use-accounts.ts +++ b/ui/src/hooks/use-accounts.ts @@ -1,6 +1,6 @@ /** * React Query hooks for accounts (profiles.json) - * Phase 03: REST API Routes & CRUD + * Dashboard parity: Full CRUD operations for auth profiles */ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; @@ -28,3 +28,33 @@ export function useSetDefaultAccount() { }, }); } + +export function useResetDefaultAccount() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: () => api.accounts.resetDefault(), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['accounts'] }); + toast.success('Default account reset to CCS'); + }, + onError: (error: Error) => { + toast.error(error.message); + }, + }); +} + +export function useDeleteAccount() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (name: string) => api.accounts.delete(name), + onSuccess: (_data, name) => { + queryClient.invalidateQueries({ queryKey: ['accounts'] }); + toast.success(`Account "${name}" deleted`); + }, + onError: (error: Error) => { + toast.error(error.message); + }, + }); +} diff --git a/ui/src/lib/api-client.ts b/ui/src/lib/api-client.ts index 57cdb9bd..10b5ba44 100644 --- a/ui/src/lib/api-client.ts +++ b/ui/src/lib/api-client.ts @@ -351,6 +351,8 @@ export const api = { method: 'POST', body: JSON.stringify({ name }), }), + resetDefault: () => request('/accounts/reset-default', { method: 'DELETE' }), + delete: (name: string) => request(`/accounts/${name}`, { method: 'DELETE' }), }, // Unified config API config: { diff --git a/ui/src/pages/accounts.tsx b/ui/src/pages/accounts.tsx index 231dd545..bbed9c89 100644 --- a/ui/src/pages/accounts.tsx +++ b/ui/src/pages/accounts.tsx @@ -1,13 +1,18 @@ /** * Accounts Page - * Phase 03: REST API Routes & CRUD + * Dashboard parity: Auth profile CRUD operations */ +import { useState } from 'react'; +import { Plus } from 'lucide-react'; import { AccountsTable } from '@/components/account/accounts-table'; +import { CreateAuthProfileDialog } from '@/components/account/create-auth-profile-dialog'; +import { Button } from '@/components/ui/button'; import { useAccounts } from '@/hooks/use-accounts'; export function AccountsPage() { - const { data, isLoading } = useAccounts(); + const { data, isLoading, refetch } = useAccounts(); + const [createDialogOpen, setCreateDialogOpen] = useState(false); return (
@@ -18,22 +23,23 @@ export function AccountsPage() { Manage multi-account Claude sessions (profiles.json)

+ {isLoading ? (
Loading accounts...
) : ( - + )} -
-

- Accounts are isolated Claude instances with separate sessions. -
- Use ccs auth create <name> to add new - accounts via CLI. -

-
+ setCreateDialogOpen(false)} /> ); }