From 8e01191c6810256013cc214daa6484e7d643804e Mon Sep 17 00:00:00 2001 From: jcfs Date: Mon, 12 Jan 2026 14:21:26 +0000 Subject: [PATCH] feat: enhance tool call display with payload details and typing indicator - Add typing indicator in TaskChatPanel that shows when agent is working - Enhanced ToolCallCard component with expandable details section - Backend now passes tool call args (kind, path, locations, raw_input) to comments - Add generateUUID fallback for non-secure contexts (HTTP non-localhost) - Configure Next.js allowedDevOrigins for cross-origin dev access - Bind dev server to 0.0.0.0 for network access Backend changes: - Extract Kind, Locations, RawInput from ACP ToolCall updates - Add Args field to ToolCallData in watcher - Pass args through CommentCreator interface to comment metadata Frontend changes: - ToolCallCard shows file path, status icons, expandable args - getToolIcon matches ACP ToolKind values (read, edit, search, execute) - Typing indicator with animated dots when awaiting agent response - Replace crypto.randomUUID with fallback utility across all files --- apps/backend/cmd/kandev/main.go | 24 +- .../internal/agent/lifecycle/manager.go | 35 ++- apps/backend/internal/orchestrator/service.go | 4 +- .../internal/orchestrator/watcher/watcher.go | 11 +- apps/web/app/settings/agents/page.tsx | 3 +- .../app/settings/environment/[id]/page.tsx | 5 +- apps/web/app/settings/environments/page.tsx | 3 +- .../workspace/workspace-edit-client.tsx | 17 +- apps/web/app/task/[id]/page-client.tsx | 1 + .../components/settings/key-value-input.tsx | 3 +- apps/web/components/task/task-chat-panel.tsx | 208 ++++++++++++++++-- apps/web/components/toast-provider.tsx | 4 +- apps/web/lib/config.ts | 2 +- apps/web/lib/utils.ts | 16 ++ apps/web/lib/ws/client.ts | 9 +- apps/web/next.config.ts | 9 +- apps/web/package.json | 2 +- 17 files changed, 303 insertions(+), 53 deletions(-) diff --git a/apps/backend/cmd/kandev/main.go b/apps/backend/cmd/kandev/main.go index def5c1c4a6..5c5839c0e1 100644 --- a/apps/backend/cmd/kandev/main.go +++ b/apps/backend/cmd/kandev/main.go @@ -685,18 +685,30 @@ func (a *commentCreatorAdapter) CreateAgentComment(ctx context.Context, taskID, } // CreateToolCallComment creates a comment for a tool call with type="tool_call" -func (a *commentCreatorAdapter) CreateToolCallComment(ctx context.Context, taskID, toolCallID, title, status, agentSessionID string) error { +func (a *commentCreatorAdapter) CreateToolCallComment(ctx context.Context, taskID, toolCallID, title, status, agentSessionID string, args map[string]interface{}) error { + metadata := map[string]interface{}{ + "tool_call_id": toolCallID, + "title": title, + "status": status, + } + + // Add args if provided (contains kind, path, locations, raw_input) + if args != nil && len(args) > 0 { + metadata["args"] = args + + // Extract kind as tool_name for icon selection in the frontend + if kind, ok := args["kind"].(string); ok && kind != "" { + metadata["tool_name"] = kind + } + } + _, err := a.svc.CreateComment(ctx, &taskservice.CreateCommentRequest{ TaskID: taskID, Content: title, AuthorType: "agent", AgentSessionID: agentSessionID, Type: "tool_call", - Metadata: map[string]interface{}{ - "tool_call_id": toolCallID, - "title": title, - "status": status, - }, + Metadata: metadata, }) return err } diff --git a/apps/backend/internal/agent/lifecycle/manager.go b/apps/backend/internal/agent/lifecycle/manager.go index f4ac2955e8..f01420682a 100644 --- a/apps/backend/internal/agent/lifecycle/manager.go +++ b/apps/backend/internal/agent/lifecycle/manager.go @@ -630,15 +630,46 @@ func (m *Manager) handleSessionNotification(instance *AgentInstance, notificatio toolCallID := string(update.ToolCall.ToolCallId) toolTitle := update.ToolCall.Title toolStatus := string(update.ToolCall.Status) + toolKind := string(update.ToolCall.Kind) m.logger.Info("tool call started", zap.String("instance_id", instance.ID), zap.String("tool_call_id", toolCallID), - zap.String("title", toolTitle)) + zap.String("title", toolTitle), + zap.String("kind", toolKind)) m.updateInstanceProgress(instance.ID, 60) + // Extract args from the tool call for display + args := map[string]interface{}{} + + // Add tool kind + if toolKind != "" { + args["kind"] = toolKind + } + + // Add locations (file paths) + if len(update.ToolCall.Locations) > 0 { + locations := make([]map[string]interface{}, len(update.ToolCall.Locations)) + for i, loc := range update.ToolCall.Locations { + locMap := map[string]interface{}{"path": loc.Path} + if loc.Line != nil { + locMap["line"] = *loc.Line + } + locations[i] = locMap + } + args["locations"] = locations + + // Also set primary path for convenience + args["path"] = update.ToolCall.Locations[0].Path + } + + // Add raw input if available + if update.ToolCall.RawInput != nil { + args["raw_input"] = update.ToolCall.RawInput + } + // Publish tool call as a comment so it appears in the chat - m.publishToolCall(instance, toolCallID, toolTitle, toolStatus, nil) + m.publishToolCall(instance, toolCallID, toolTitle, toolStatus, args) } if update.ToolCallUpdate != nil { diff --git a/apps/backend/internal/orchestrator/service.go b/apps/backend/internal/orchestrator/service.go index a3244d3127..cc8b1c4aab 100644 --- a/apps/backend/internal/orchestrator/service.go +++ b/apps/backend/internal/orchestrator/service.go @@ -48,7 +48,7 @@ type InputRequestHandler func(ctx context.Context, taskID, agentID, message stri // CommentCreator is an interface for creating comments on tasks type CommentCreator interface { CreateAgentComment(ctx context.Context, taskID, content, agentSessionID string) error - CreateToolCallComment(ctx context.Context, taskID, toolCallID, title, status, agentSessionID string) error + CreateToolCallComment(ctx context.Context, taskID, toolCallID, title, status, agentSessionID string, args map[string]interface{}) error } // Service is the main orchestrator service @@ -567,7 +567,7 @@ func (s *Service) handleToolCallStarted(ctx context.Context, data watcher.ToolCa // Get the active session ID for this task sessionID, _ := s.executor.GetActiveSessionID(ctx, data.TaskID) - if err := s.commentCreator.CreateToolCallComment(ctx, data.TaskID, data.ToolCallID, data.Title, data.Status, sessionID); err != nil { + if err := s.commentCreator.CreateToolCallComment(ctx, data.TaskID, data.ToolCallID, data.Title, data.Status, sessionID, data.Args); err != nil { s.logger.Error("failed to create tool call comment", zap.String("task_id", data.TaskID), zap.String("tool_call_id", data.ToolCallID), diff --git a/apps/backend/internal/orchestrator/watcher/watcher.go b/apps/backend/internal/orchestrator/watcher/watcher.go index c8947ca869..41462b9504 100644 --- a/apps/backend/internal/orchestrator/watcher/watcher.go +++ b/apps/backend/internal/orchestrator/watcher/watcher.go @@ -42,11 +42,12 @@ type PromptCompleteData struct { // ToolCallData contains data from tool_call events type ToolCallData struct { - TaskID string `json:"task_id"` - AgentID string `json:"agent_id"` - ToolCallID string `json:"tool_call_id"` - Title string `json:"title"` - Status string `json:"status"` + TaskID string `json:"task_id"` + AgentID string `json:"agent_id"` + ToolCallID string `json:"tool_call_id"` + Title string `json:"title"` + Status string `json:"status"` + Args map[string]interface{} `json:"args,omitempty"` // Tool call arguments (kind, path, locations, raw_input) } // EventHandlers contains callbacks for different event types diff --git a/apps/web/app/settings/agents/page.tsx b/apps/web/app/settings/agents/page.tsx index 871f0c3498..31523cc4bc 100644 --- a/apps/web/app/settings/agents/page.tsx +++ b/apps/web/app/settings/agents/page.tsx @@ -11,6 +11,7 @@ import { Separator } from '@/components/ui/separator'; import { AgentCard } from '@/components/settings/agent-card'; import { SETTINGS_DATA } from '@/lib/settings/dummy-data'; import type { AgentProfile, AgentType } from '@/lib/settings/types'; +import { generateUUID } from '@/lib/utils'; const AGENT_OPTIONS: { value: AgentType; label: string }[] = [ { value: 'claude-code', label: 'Claude Code' }, @@ -54,7 +55,7 @@ export default function AgentsSettingsPage() { const handleAdd = (e: React.FormEvent) => { e.preventDefault(); const profile: AgentProfile = { - id: crypto.randomUUID(), + id: generateUUID(), ...newAgent, }; setAgents([...agents, profile]); diff --git a/apps/web/app/settings/environment/[id]/page.tsx b/apps/web/app/settings/environment/[id]/page.tsx index 27076b7a23..ee21c57ef7 100644 --- a/apps/web/app/settings/environment/[id]/page.tsx +++ b/apps/web/app/settings/environment/[id]/page.tsx @@ -18,6 +18,7 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog'; +import { generateUUID } from '@/lib/utils'; import { SETTINGS_DATA } from '@/lib/settings/dummy-data'; import type { Environment, EnvironmentType, BaseDocker, AgentType } from '@/lib/settings/types'; @@ -55,7 +56,7 @@ export default function EnvironmentEditPage({ params }: { params: Promise<{ id: ...environment, envVariables: [ ...environment.envVariables, - { id: crypto.randomUUID(), key: newEnvKey, value: newEnvValue }, + { id: generateUUID(), key: newEnvKey, value: newEnvValue }, ], }); setNewEnvKey(''); @@ -76,7 +77,7 @@ export default function EnvironmentEditPage({ params }: { params: Promise<{ id: ...environment, secrets: [ ...environment.secrets, - { id: crypto.randomUUID(), key: newSecretKey, value: newSecretValue }, + { id: generateUUID(), key: newSecretKey, value: newSecretValue }, ], }); setNewSecretKey(''); diff --git a/apps/web/app/settings/environments/page.tsx b/apps/web/app/settings/environments/page.tsx index 701ef157b7..aa49840d0e 100644 --- a/apps/web/app/settings/environments/page.tsx +++ b/apps/web/app/settings/environments/page.tsx @@ -13,6 +13,7 @@ import { EnvironmentCard } from '@/components/settings/environment-card'; import { KeyValueInput } from '@/components/settings/key-value-input'; import { SETTINGS_DATA } from '@/lib/settings/dummy-data'; import type { Environment, EnvironmentType, BaseDocker, KeyValue, AgentType } from '@/lib/settings/types'; +import { generateUUID } from '@/lib/utils'; export default function EnvironmentsSettingsPage() { const [environments, setEnvironments] = useState(SETTINGS_DATA.environments); @@ -38,7 +39,7 @@ export default function EnvironmentsSettingsPage() { const handleAdd = (e: React.FormEvent) => { e.preventDefault(); const environment: Environment = { - id: crypto.randomUUID(), + id: generateUUID(), ...newEnv, }; setEnvironments([...environments, environment]); diff --git a/apps/web/app/settings/workspace/workspace-edit-client.tsx b/apps/web/app/settings/workspace/workspace-edit-client.tsx index f76e35c177..9799049756 100644 --- a/apps/web/app/settings/workspace/workspace-edit-client.tsx +++ b/apps/web/app/settings/workspace/workspace-edit-client.tsx @@ -19,6 +19,7 @@ import { 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, @@ -272,7 +273,7 @@ export function WorkspaceEditClient({ workspace, boards, repositories }: Workspa 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-${crypto.randomUUID()}`; + const draftId = `temp-repo-${generateUUID()}`; const draftRepo: RepositoryWithScripts = { id: draftId, workspace_id: currentWorkspace.id, @@ -296,7 +297,7 @@ export function WorkspaceEditClient({ workspace, boards, repositories }: Workspa const handleAddRepositoryScript = (repoId: string) => { const script: RepositoryScript = { - id: `temp-script-${crypto.randomUUID()}`, + id: `temp-script-${generateUUID()}`, repository_id: repoId, name: '', command: '', @@ -431,7 +432,7 @@ export function WorkspaceEditClient({ workspace, boards, repositories }: Workspa const handleAddBoard = () => { if (!currentWorkspace) return; - const draftBoardId = `temp-${crypto.randomUUID()}`; + const draftBoardId = `temp-${generateUUID()}`; const draftBoard: BoardWithColumns = { id: draftBoardId, workspace_id: currentWorkspace.id, @@ -441,7 +442,7 @@ export function WorkspaceEditClient({ workspace, boards, repositories }: Workspa updated_at: '', columns: [ { - id: `temp-col-${crypto.randomUUID()}`, + id: `temp-col-${generateUUID()}`, board_id: draftBoardId, name: 'Todo', position: 0, @@ -451,7 +452,7 @@ export function WorkspaceEditClient({ workspace, boards, repositories }: Workspa updated_at: '', }, { - id: `temp-col-${crypto.randomUUID()}`, + id: `temp-col-${generateUUID()}`, board_id: draftBoardId, name: 'In Progress', position: 1, @@ -461,7 +462,7 @@ export function WorkspaceEditClient({ workspace, boards, repositories }: Workspa updated_at: '', }, { - id: `temp-col-${crypto.randomUUID()}`, + id: `temp-col-${generateUUID()}`, board_id: draftBoardId, name: 'To Review', position: 2, @@ -471,7 +472,7 @@ export function WorkspaceEditClient({ workspace, boards, repositories }: Workspa updated_at: '', }, { - id: `temp-col-${crypto.randomUUID()}`, + id: `temp-col-${generateUUID()}`, board_id: draftBoardId, name: 'Done', position: 3, @@ -506,7 +507,7 @@ export function WorkspaceEditClient({ workspace, boards, repositories }: Workspa column: Omit ) => { const draftColumn: Column = { - id: `temp-col-${crypto.randomUUID()}`, + id: `temp-col-${generateUUID()}`, board_id: boardId, name: column.name, position: column.position, diff --git a/apps/web/app/task/[id]/page-client.tsx b/apps/web/app/task/[id]/page-client.tsx index c47ee7b8e2..d1b5fed37a 100644 --- a/apps/web/app/task/[id]/page-client.tsx +++ b/apps/web/app/task/[id]/page-client.tsx @@ -258,6 +258,7 @@ export default function TaskPage({ task }: TaskPageClientProps) { agents={AGENTS} onSend={handleSendMessage} isLoading={isLoadingComments} + isAgentWorking={isAgentRunning} /> ) : (
diff --git a/apps/web/components/settings/key-value-input.tsx b/apps/web/components/settings/key-value-input.tsx index a1b43ba296..220cb0e457 100644 --- a/apps/web/components/settings/key-value-input.tsx +++ b/apps/web/components/settings/key-value-input.tsx @@ -4,6 +4,7 @@ import { IconPlus, IconX } from '@tabler/icons-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import type { KeyValue } from '@/lib/settings/types'; +import { generateUUID } from '@/lib/utils'; type KeyValueInputProps = { items: KeyValue[]; @@ -23,7 +24,7 @@ export function KeyValueInput({ masked = false, }: KeyValueInputProps) { const handleAdd = () => { - onChange([...items, { id: crypto.randomUUID(), key: '', value: '' }]); + onChange([...items, { id: generateUUID(), key: '', value: '' }]); }; const handleRemove = (id: string) => { diff --git a/apps/web/components/task/task-chat-panel.tsx b/apps/web/components/task/task-chat-panel.tsx index d02b0a5191..c69b3bf176 100644 --- a/apps/web/components/task/task-chat-panel.tsx +++ b/apps/web/components/task/task-chat-panel.tsx @@ -1,7 +1,22 @@ 'use client'; import { FormEvent, useEffect, useRef, useState } from 'react'; -import { IconBrain, IconListCheck, IconLoader2, IconPaperclip, IconTerminal2 } from '@tabler/icons-react'; +import { + IconBrain, + IconCheck, + IconChevronDown, + IconChevronRight, + IconCode, + IconEdit, + IconEye, + IconFile, + IconListCheck, + IconLoader2, + IconPaperclip, + IconSearch, + IconTerminal2, + IconX, +} from '@tabler/icons-react'; import ReactMarkdown from 'react-markdown'; import remarkBreaks from 'remark-breaks'; import remarkGfm from 'remark-gfm'; @@ -29,14 +44,170 @@ type TaskChatPanelProps = { agents: AgentOption[]; onSend: (message: string) => void; isLoading?: boolean; + isAgentWorking?: boolean; }; -export function TaskChatPanel({ taskId, agents, onSend, isLoading }: TaskChatPanelProps) { +type ToolCallMetadata = { + tool_call_id?: string; + tool_name?: string; + title?: string; + status?: 'pending' | 'running' | 'complete' | 'error'; + args?: Record; + result?: string; +}; + +function getToolIcon(toolName?: 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; + } + if (name === 'read' || name.includes('view') || name.includes('read')) { + return IconEye; + } + if (name === 'search' || name.includes('search') || name.includes('find') || name.includes('retrieval')) { + return IconSearch; + } + if (name === 'execute' || name.includes('terminal') || name.includes('exec') || name.includes('launch') || name.includes('process')) { + return IconTerminal2; + } + if (name === 'delete' || name === 'move' || name.includes('file') || name.includes('create')) { + return IconFile; + } + return IconCode; +} + +function getStatusIcon(status?: string) { + switch (status) { + case 'complete': + return ; + case 'error': + return ; + case 'running': + return ; + default: + return null; + } +} + +function ToolCallCard({ comment }: { comment: Comment }) { + const [isExpanded, setIsExpanded] = useState(false); + const metadata = comment.metadata as ToolCallMetadata | undefined; + + const toolName = metadata?.tool_name ?? ''; + const title = metadata?.title ?? comment.content ?? 'Tool call'; + const status = metadata?.status; + const args = metadata?.args; + const result = metadata?.result; + + const ToolIcon = getToolIcon(toolName); + const hasDetails = args && Object.keys(args).length > 0; + + // Extract file path from various possible sources + let filePath: string | undefined; + const rawPath = args?.path ?? args?.file ?? args?.file_path; + if (typeof rawPath === 'string') { + filePath = rawPath; + } + // Also try to get path from locations array if available + if (!filePath && Array.isArray(args?.locations) && args.locations.length > 0) { + const firstLoc = args.locations[0] as { path?: string } | undefined; + if (firstLoc?.path) { + filePath = firstLoc.path; + } + } + + return ( +
+ + + {isExpanded && hasDetails && ( +
+ {args && Object.entries(args).map(([key, value]) => { + const strValue = typeof value === 'string' ? value : JSON.stringify(value, null, 2); + const isLongValue = strValue.length > 100 || strValue.includes('\n'); + + return ( +
+ {key}: + {isLongValue ? ( +
+                    {strValue}
+                  
+ ) : ( + {strValue} + )} +
+ ); + })} + {result && ( +
+ Result: +
+                {result}
+              
+
+ )} +
+ )} +
+ ); +} + +function TypingIndicator() { + return ( +
+
+ Agent + + + +
+
+ ); +} + +export function TaskChatPanel({ taskId, agents, onSend, isLoading, isAgentWorking }: TaskChatPanelProps) { const [messageInput, setMessageInput] = useState(''); const [selectedAgent, setSelectedAgent] = useState(agents[0]?.id ?? ''); const [planModeEnabled, setPlanModeEnabled] = useState(false); const [isSending, setIsSending] = useState(false); + const [isAwaitingResponse, setIsAwaitingResponse] = useState(false); const messagesEndRef = useRef(null); + const lastAgentMessageCountRef = useRef(0); const commentsState = useAppStore((state) => state.comments); const comments = commentsState?.items ?? []; @@ -47,16 +218,31 @@ export function TaskChatPanel({ taskId, agents, onSend, isLoading }: TaskChatPan (c) => c.type === 'message' || c.type === 'content' || c.type === 'tool_call' || !c.type ); - // Scroll to bottom when new comments arrive + // Count agent messages to detect new responses + const agentMessageCount = visibleComments.filter((c) => c.author_type !== 'user').length; + + // Clear awaiting state when a new agent message arrives + useEffect(() => { + if (agentMessageCount > lastAgentMessageCountRef.current) { + setIsAwaitingResponse(false); + } + lastAgentMessageCountRef.current = agentMessageCount; + }, [agentMessageCount]); + + // Show typing indicator when awaiting response AND agent session is active + const showTypingIndicator = isAwaitingResponse && isAgentWorking; + + // Scroll to bottom when new comments arrive or when typing indicator appears useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [visibleComments.length]); + }, [visibleComments.length, showTypingIndicator]); const handleSubmit = async (event: FormEvent) => { event.preventDefault(); const trimmed = messageInput.trim(); if (!trimmed || isSending) return; setIsSending(true); + setIsAwaitingResponse(true); setMessageInput(''); try { await onSend(trimmed); @@ -88,18 +274,7 @@ export function TaskChatPanel({ taskId, agents, onSend, isLoading }: TaskChatPan {visibleComments.map((comment) => { // Tool call comments have a special rendering if (comment.type === 'tool_call') { - const metadata = comment.metadata as { title?: string; status?: string } | undefined; - return ( -
- - - {metadata?.title || comment.content} - -
- ); + return ; } // Regular message or agent response - render as markdown @@ -129,6 +304,7 @@ export function TaskChatPanel({ taskId, agents, onSend, isLoading }: TaskChatPan
); })} + {showTypingIndicator && }
diff --git a/apps/web/components/toast-provider.tsx b/apps/web/components/toast-provider.tsx index 300716cba5..b119f8db77 100644 --- a/apps/web/components/toast-provider.tsx +++ b/apps/web/components/toast-provider.tsx @@ -1,7 +1,7 @@ 'use client'; import { createContext, useCallback, useContext, useMemo, useState } from 'react'; -import { cn } from '@/lib/utils'; +import { cn, generateUUID } from '@/lib/utils'; type ToastVariant = 'default' | 'error'; @@ -29,7 +29,7 @@ export function ToastProvider({ children }: { children: React.ReactNode }) { const toast = useCallback( (input: ToastInput) => { - const id = crypto.randomUUID(); + const id = generateUUID(); const nextToast: Toast = { id, title: input.title, diff --git a/apps/web/lib/config.ts b/apps/web/lib/config.ts index 03246ccd93..67abe8b2a0 100644 --- a/apps/web/lib/config.ts +++ b/apps/web/lib/config.ts @@ -2,7 +2,7 @@ export type AppConfig = { apiBaseUrl: string; }; -const DEFAULT_API_BASE_URL = 'http://localhost:8080'; +const DEFAULT_API_BASE_URL = 'http://nova:8080'; export function getBackendConfig(): AppConfig { if (typeof window === 'undefined') { diff --git a/apps/web/lib/utils.ts b/apps/web/lib/utils.ts index bd0c391ddd..b4bd4c3936 100644 --- a/apps/web/lib/utils.ts +++ b/apps/web/lib/utils.ts @@ -4,3 +4,19 @@ import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } + +/** + * Generate a UUID. Falls back to a custom implementation in non-secure contexts + * (e.g., HTTP on non-localhost where crypto.randomUUID is unavailable). + */ +export function generateUUID(): string { + if (typeof crypto !== 'undefined' && crypto.randomUUID) { + return crypto.randomUUID(); + } + // Fallback implementation + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} diff --git a/apps/web/lib/ws/client.ts b/apps/web/lib/ws/client.ts index 33596bbbc1..74886ec34b 100644 --- a/apps/web/lib/ws/client.ts +++ b/apps/web/lib/ws/client.ts @@ -1,4 +1,5 @@ import type { BackendMessageMap, BackendMessageType } from '@/lib/types/backend'; +import { generateUUID } from '@/lib/utils'; type MessageHandler = (message: BackendMessageMap[T]) => void; @@ -168,7 +169,7 @@ export class WebSocketClient { } request(action: string, payload: unknown, timeoutMs = 5000): Promise { - const id = crypto.randomUUID(); + const id = generateUUID(); return new Promise((resolve, reject) => { const timeout = setTimeout(() => { this.pendingRequests.delete(id); @@ -183,7 +184,7 @@ export class WebSocketClient { this.subscriptions.add(taskId); if (this.status === 'open') { this.send({ - id: crypto.randomUUID(), + id: generateUUID(), type: 'request', action: 'task.subscribe', payload: { task_id: taskId }, @@ -195,7 +196,7 @@ export class WebSocketClient { this.subscriptions.delete(taskId); if (this.status === 'open') { this.send({ - id: crypto.randomUUID(), + id: generateUUID(), type: 'request', action: 'task.unsubscribe', payload: { task_id: taskId }, @@ -279,7 +280,7 @@ export class WebSocketClient { // Re-subscribe to all tasks after reconnection this.subscriptions.forEach((taskId) => { this.send({ - id: crypto.randomUUID(), + id: generateUUID(), type: 'request', action: 'task.subscribe', payload: { task_id: taskId }, diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index e9ffa3083a..8b5f6224f8 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -1,7 +1,14 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + allowedDevOrigins: [ + "nova", + "nova:3000", + "localhost", + "localhost:3000", + "192.168.1.116", + "192.168.1.116:3000", + ], }; export default nextConfig; diff --git a/apps/web/package.json b/apps/web/package.json index 4ce0f38afe..684f278a35 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "next dev --hostname 0.0.0.0", "build": "NODE_ENV=production next build", "start": "NODE_ENV=production next start", "lint": "eslint",