From 53ae8d57e3e9f24410be59c7c9f02d07f6ac4e11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Wed, 18 Feb 2026 18:24:41 +0000 Subject: [PATCH] fix: resolve all web app ESLint linter warnings (#246) --- .../app/demo/messages/demo-messages-data.ts | 675 +++++++ apps/web/app/demo/messages/page.tsx | 733 +------ apps/web/app/page.tsx | 201 +- apps/web/app/s/[sessionId]/page.tsx | 469 ++--- .../agents/[agentId]/agent-save-helpers.ts | 245 +++ .../app/settings/agents/[agentId]/page.tsx | 564 ++---- .../[agentId]/profile-mcp-config-card.tsx | 486 +++-- .../[profileId]/command-preview-card.tsx | 152 +- .../[agentId]/use-profile-mcp-config.ts | 188 +- apps/web/app/settings/agents/page.tsx | 124 +- .../app/settings/environment/[id]/page.tsx | 385 ++-- .../web/app/settings/environment/new/page.tsx | 247 ++- apps/web/app/settings/executor/[id]/page.tsx | 631 +++---- apps/web/app/settings/executor/new/page.tsx | 296 +-- apps/web/app/settings/executors/page.tsx | 240 ++- .../workspace/workspace-edit-client.tsx | 452 +++-- .../workspace-repositories-client.tsx | 655 +++---- .../workspace/workspace-workflows-client.tsx | 619 +++--- .../workspace/workspaces-page-client.tsx | 124 +- apps/web/app/stats/loading.tsx | 262 +-- apps/web/app/stats/stats-charts.tsx | 368 ++++ apps/web/app/stats/stats-page-client.tsx | 1217 ++---------- apps/web/app/stats/stats-sections.tsx | 453 +++++ apps/web/app/tasks/columns.tsx | 177 +- apps/web/app/tasks/tasks-page-client.tsx | 231 +-- apps/web/components/command-panel.tsx | 411 ++-- .../components/diff/diff-viewer-resolver.tsx | 3 +- apps/web/components/diff/diff-viewer.tsx | 487 +---- .../diff/use-diff-annotation-renderer.tsx | 96 + apps/web/components/diff/use-diff-metadata.ts | 43 + apps/web/components/diff/use-diff-options.tsx | 119 ++ .../components/diff/use-diff-viewer-state.ts | 215 +++ apps/web/components/diff/use-hunk-hover.ts | 78 + .../codemirror/codemirror-code-editor.tsx | 673 +------ .../codemirror/use-codemirror-editor-state.ts | 399 ++++ .../editors/monaco/diff-viewer-helpers.ts | 82 + .../editors/monaco/monaco-code-editor.tsx | 834 +------- .../editors/monaco/monaco-diff-viewer.tsx | 430 +---- .../editors/monaco/monaco-editor-toolbar.tsx | 129 ++ .../editors/monaco/monaco-inline-diff.tsx | 67 +- .../editors/monaco/use-diff-view-zones.ts | 22 +- .../monaco/use-diff-viewer-comments.ts | 214 +++ .../editors/monaco/use-global-folding.ts | 33 + .../editors/monaco/use-monaco-editor-lsp.ts | 193 ++ .../editors/monaco/use-monaco-editor-state.ts | 302 +++ .../tiptap/tiptap-comment-highlight.ts | 93 +- .../editors/tiptap/tiptap-plan-editor.tsx | 246 +-- apps/web/components/global-commands.tsx | 125 +- apps/web/components/kanban-board-grid.tsx | 369 ++-- apps/web/components/kanban-board.tsx | 391 ++-- apps/web/components/kanban-card.tsx | 236 ++- .../components/kanban-display-dropdown.tsx | 159 +- apps/web/components/kanban-with-preview.tsx | 358 ++-- .../components/kanban/graph2-step-node.tsx | 140 +- .../kanban/graph2-task-pipeline.tsx | 115 +- apps/web/components/kanban/kanban-header.tsx | 367 ++-- .../components/kanban/mobile-drop-targets.tsx | 10 +- .../components/kanban/mobile-menu-sheet.tsx | 223 ++- .../components/kanban/swimlane-container.tsx | 77 +- .../kanban/swimlane-graph-content.tsx | 266 +-- .../kanban/swimlane-kanban-content.tsx | 144 +- apps/web/components/onboarding-dialog.tsx | 207 +- apps/web/components/review/review-dialog.tsx | 279 +-- .../components/review/review-diff-list.tsx | 231 +-- .../components/review/review-file-tree.tsx | 126 +- apps/web/components/review/review-top-bar.tsx | 206 +- apps/web/components/session-commands.tsx | 232 +-- apps/web/components/session-tabs.tsx | 164 +- .../settings/agent-profile-page.tsx | 299 +-- apps/web/components/settings/editor-form.tsx | 223 +++ .../components/settings/editors-settings.tsx | 1224 +++++------- .../components/settings/general-settings.tsx | 177 +- .../components/settings/model-combobox.tsx | 381 ++-- .../settings/notifications-settings.tsx | 807 ++++---- .../settings/profile-form-fields.tsx | 180 +- .../components/settings/prompts-settings.tsx | 519 +++-- .../components/settings/repository-card.tsx | 778 +++++--- .../settings/settings-app-sidebar.tsx | 391 ++-- .../settings/shell-settings-card.tsx | 148 +- .../web/components/settings/workflow-card.tsx | 797 +++----- .../workflow-pipeline-editor-helpers.tsx | 22 + .../workflow-pipeline-editor-panels.tsx | 489 +++++ .../settings/workflow-pipeline-editor.tsx | 712 +------ apps/web/components/step-capability-icons.tsx | 127 +- .../components/task-create-dialog-footer.tsx | 220 +++ .../task-create-dialog-form-body.tsx | 172 ++ .../components/task-create-dialog-options.tsx | 162 ++ .../task-create-dialog-selectors.tsx | 264 +++ .../components/task-create-dialog-submit.tsx | 389 ++++ apps/web/components/task-create-dialog.tsx | 1681 +++++------------ apps/web/components/task/browser-panel.tsx | 54 +- .../components/task/changes-panel-dialogs.tsx | 305 +++ .../task/changes-panel-file-row.tsx | 183 ++ .../task/changes-panel-timeline.tsx | 283 +++ apps/web/components/task/changes-panel.tsx | 1254 ++++-------- .../task/chat/chat-input-container.tsx | 723 +++---- .../task/chat/chat-input-toolbar.tsx | 103 +- .../task/chat/clarification-input-overlay.tsx | 216 +-- .../components/task/chat/context-popover.tsx | 215 +-- .../web/components/task/chat/mention-menu.tsx | 6 +- .../task/chat/messages/agent-status.tsx | 127 +- .../task/chat/messages/chat-message.tsx | 465 +++-- .../task/chat/messages/message-actions.tsx | 188 +- .../messages/permission-request-message.tsx | 60 +- .../messages/script-execution-message.tsx | 282 +-- .../task/chat/messages/status-message.tsx | 269 +-- .../task/chat/messages/tool-call-message.tsx | 248 ++- .../task/chat/messages/tool-edit-message.tsx | 200 +- .../chat/messages/tool-execute-message.tsx | 115 +- .../task/chat/messages/tool-read-message.tsx | 81 +- .../chat/messages/tool-search-message.tsx | 118 +- .../chat/messages/tool-subagent-message.tsx | 124 +- .../task/chat/messages/turn-group-message.tsx | 200 +- .../task/chat/messages/use-expand-state.ts | 33 + .../task/chat/queued-message-indicator.tsx | 187 +- .../components/task/chat/rich-text-input.tsx | 210 +- .../task/chat/tiptap-code-block-view.tsx | 46 + .../components/task/chat/tiptap-helpers.ts | 185 ++ .../web/components/task/chat/tiptap-input.tsx | 793 ++------ .../task/chat/tiptap-suggestion.tsx | 16 +- .../task/chat/use-chat-input-state.ts | 145 ++ .../components/task/chat/use-tiptap-editor.ts | 340 ++++ .../task/chat/virtualized-message-list.tsx | 408 ++-- .../components/task/comment-view-popover.tsx | 133 +- .../components/task/commit-detail-panel.tsx | 119 +- .../task/dockview-desktop-layout.tsx | 648 ++----- .../task/dockview-header-actions.tsx | 331 ++++ .../task/editor-comment-popover.tsx | 181 +- .../components/task/file-browser-parts.tsx | 490 +++++ apps/web/components/task/file-browser.tsx | 1148 ++++------- .../web/components/task/file-editor-panel.tsx | 96 +- .../task/mobile/session-mobile-layout.tsx | 156 +- .../task/mobile/session-mobile-top-bar.tsx | 618 +++--- .../task/mobile/session-tablet-layout.tsx | 92 +- .../mobile/session-task-switcher-sheet.tsx | 307 +-- apps/web/components/task/model-selector.tsx | 121 +- .../components/task/passthrough-terminal.tsx | 511 ++--- .../task/plan-selection-popover.tsx | 101 +- .../task/preview/preview-controls.tsx | 203 +- .../components/task/preview/preview-panel.tsx | 423 ++--- .../web/components/task/sessions-dropdown.tsx | 325 ++-- apps/web/components/task/shell-terminal.tsx | 288 ++- .../web/components/task/task-center-panel.tsx | 963 ++++------ .../components/task/task-changes-panel.tsx | 608 +++--- apps/web/components/task/task-chat-panel.tsx | 737 ++++---- apps/web/components/task/task-files-panel.tsx | 832 ++++---- apps/web/components/task/task-item.tsx | 91 +- .../web/components/task/task-page-content.tsx | 519 ++--- apps/web/components/task/task-plan-panel.tsx | 664 +++---- apps/web/components/task/task-right-panel.tsx | 239 +-- .../components/task/task-session-sidebar.tsx | 275 +-- apps/web/components/task/task-top-bar.tsx | 457 ++--- apps/web/components/task/workflow-stepper.tsx | 249 ++- .../components/ui/data-table-pagination.tsx | 155 +- apps/web/components/ui/data-table.tsx | 76 +- apps/web/components/ui/file-icon.tsx | 70 +- apps/web/components/vcs-split-button.tsx | 269 +-- apps/web/components/vcs/vcs-dialogs.tsx | 321 ++-- .../domains/kanban/use-kanban-actions.ts | 126 +- .../hooks/domains/kanban/use-swimlane-move.ts | 33 +- .../domains/session/use-session-messages.ts | 186 +- .../domains/session/use-session-resumption.ts | 231 +-- .../hooks/domains/session/use-terminals.ts | 350 ++-- .../web/hooks/domains/settings/use-editors.ts | 62 +- apps/web/hooks/use-capture-keydown.ts | 39 + apps/web/hooks/use-drag-and-drop.ts | 163 +- apps/web/hooks/use-file-editors.ts | 337 ++-- apps/web/hooks/use-git-operations.ts | 45 +- apps/web/hooks/use-inline-mention.ts | 344 ++-- apps/web/hooks/use-inline-slash.ts | 235 +-- apps/web/hooks/use-kanban-display-settings.ts | 50 +- apps/web/hooks/use-kanban-layout.ts | 9 +- apps/web/hooks/use-message-handler.ts | 135 +- apps/web/hooks/use-preview-panel.ts | 317 ++-- apps/web/hooks/use-processed-messages.ts | 361 ++-- apps/web/hooks/use-session-file-reviews.ts | 148 +- apps/web/hooks/use-session-layout-state.ts | 66 +- apps/web/hooks/use-task-removal.ts | 81 +- apps/web/hooks/use-user-display-settings.ts | 206 +- apps/web/lib/api/domains/plan-api.ts | 10 +- apps/web/lib/api/domains/queue-api.ts | 10 +- apps/web/lib/diff/adapter.ts | 85 +- apps/web/lib/executor-icons.ts | 2 +- apps/web/lib/keyboard/utils.test.ts | 175 +- apps/web/lib/keyboard/utils.ts | 87 +- apps/web/lib/languages.ts | 91 +- apps/web/lib/lsp/lsp-client-manager.ts | 472 +---- apps/web/lib/lsp/lsp-providers.ts | 373 ++++ apps/web/lib/preview-url-detector.test.ts | 280 ++- apps/web/lib/preview-url-detector.ts | 71 +- apps/web/lib/state/dockview-store.ts | 468 ++--- apps/web/lib/state/editor-resolver-store.ts | 21 +- apps/web/lib/state/hydration/hydrator.ts | 195 +- apps/web/lib/state/layout-store.ts | 258 +-- .../diff-comments/diff-comments-slice.ts | 123 +- .../session-runtime/session-runtime-slice.ts | 237 +-- .../lib/state/slices/session/session-slice.ts | 347 ++-- apps/web/lib/types/http-agents.ts | 272 +++ apps/web/lib/types/http.ts | 271 +-- apps/web/lib/ui/state-icons.tsx | 32 +- apps/web/lib/utils.test.ts | 10 +- apps/web/lib/utils.ts | 7 +- apps/web/lib/ws/client.ts | 79 +- apps/web/lib/ws/handlers/agent-session.ts | 257 ++- apps/web/lib/ws/handlers/agents.ts | 180 +- apps/web/lib/ws/handlers/tasks.ts | 171 +- 206 files changed, 28728 insertions(+), 30328 deletions(-) create mode 100644 apps/web/app/demo/messages/demo-messages-data.ts create mode 100644 apps/web/app/settings/agents/[agentId]/agent-save-helpers.ts create mode 100644 apps/web/app/stats/stats-charts.tsx create mode 100644 apps/web/app/stats/stats-sections.tsx create mode 100644 apps/web/components/diff/use-diff-annotation-renderer.tsx create mode 100644 apps/web/components/diff/use-diff-metadata.ts create mode 100644 apps/web/components/diff/use-diff-options.tsx create mode 100644 apps/web/components/diff/use-diff-viewer-state.ts create mode 100644 apps/web/components/diff/use-hunk-hover.ts create mode 100644 apps/web/components/editors/codemirror/use-codemirror-editor-state.ts create mode 100644 apps/web/components/editors/monaco/diff-viewer-helpers.ts create mode 100644 apps/web/components/editors/monaco/monaco-editor-toolbar.tsx create mode 100644 apps/web/components/editors/monaco/use-diff-viewer-comments.ts create mode 100644 apps/web/components/editors/monaco/use-global-folding.ts create mode 100644 apps/web/components/editors/monaco/use-monaco-editor-lsp.ts create mode 100644 apps/web/components/editors/monaco/use-monaco-editor-state.ts create mode 100644 apps/web/components/settings/editor-form.tsx create mode 100644 apps/web/components/settings/workflow-pipeline-editor-helpers.tsx create mode 100644 apps/web/components/settings/workflow-pipeline-editor-panels.tsx create mode 100644 apps/web/components/task-create-dialog-footer.tsx create mode 100644 apps/web/components/task-create-dialog-form-body.tsx create mode 100644 apps/web/components/task-create-dialog-options.tsx create mode 100644 apps/web/components/task-create-dialog-selectors.tsx create mode 100644 apps/web/components/task-create-dialog-submit.tsx create mode 100644 apps/web/components/task/changes-panel-dialogs.tsx create mode 100644 apps/web/components/task/changes-panel-file-row.tsx create mode 100644 apps/web/components/task/changes-panel-timeline.tsx create mode 100644 apps/web/components/task/chat/messages/use-expand-state.ts create mode 100644 apps/web/components/task/chat/tiptap-code-block-view.tsx create mode 100644 apps/web/components/task/chat/tiptap-helpers.ts create mode 100644 apps/web/components/task/chat/use-chat-input-state.ts create mode 100644 apps/web/components/task/chat/use-tiptap-editor.ts create mode 100644 apps/web/components/task/dockview-header-actions.tsx create mode 100644 apps/web/components/task/file-browser-parts.tsx create mode 100644 apps/web/hooks/use-capture-keydown.ts create mode 100644 apps/web/lib/lsp/lsp-providers.ts create mode 100644 apps/web/lib/types/http-agents.ts diff --git a/apps/web/app/demo/messages/demo-messages-data.ts b/apps/web/app/demo/messages/demo-messages-data.ts new file mode 100644 index 0000000000..a1b2132960 --- /dev/null +++ b/apps/web/app/demo/messages/demo-messages-data.ts @@ -0,0 +1,675 @@ +import type { Message } from '@/lib/types/http'; + +const AUTH_CONFIG_PATH = 'src/auth/config.ts'; +const B = { session_id: 'demo' as const, task_id: 'demo-task' }; + +export const dummyMessages: Message[] = [ + // Task description + { ...B, id: 'task-description', content: '[TASK DESCRIPTION - AMBER BANNER] Implement a new feature to handle user authentication with OAuth 2.0', author_type: 'user', type: 'message', created_at: '2024-01-20T10:00:00Z' }, + + // User message - short + { ...B, id: 'user-1', content: '[USER MESSAGE - SHORT] Can you help me with this?', author_type: 'user', type: 'message', created_at: '2024-01-20T10:01:00Z' }, + + // User message - long + { ...B, id: 'user-2', content: '[USER MESSAGE - LONG] I need to implement a complex feature that involves multiple components. The requirements are:\n1. User authentication\n2. Role-based access control\n3. Session management\n4. Token refresh mechanism\n\nCan you help me design this system?', author_type: 'user', type: 'message', created_at: '2024-01-20T10:02:00Z' }, + + // Agent message - plain text + { ...B, id: 'agent-1', content: '[AGENT MESSAGE - PLAIN TEXT] This is a simple paragraph with no markdown formatting. Just plain text content.', author_type: 'agent', type: 'message', created_at: '2024-01-20T10:03:00Z' }, + + // Agent message - with markdown + { + id: 'agent-2', + ...B, + content: `**[MARKDOWN DEMO]** Here's a message showing all markdown elements: + +## Heading 2 (h2 element) + +This is a **paragraph (p element)** with some **bold text (strong element)** to test styling. + +### Heading 3 (h3 element) + +**Ordered List (ol element):** + +1. First item (li element in ol) +2. Second item with **bold text** +3. Third item with inline code: \`const example = true\` +4. Fourth item (li element) + +**Unordered List (ul element):** + +- Bullet point one (li element in ul) +- Bullet point two +- Bullet point with \`inline code\` (code element) +- Another bullet point + +**Inline code examples (code element, inline):** +- Variable: \`const token = jwt.sign(payload)\` +- Function call: \`getUserData(userId)\` +- Path: \`/src/auth/config.ts\` + +**Code block example (code element, block):** + +\`\`\`typescript +// This is a code block (pre > code) +interface AuthConfig { + clientId: string; + clientSecret: string; + redirectUri: string; +} + +function authenticate(config: AuthConfig) { + return jwt.sign(config); +} +\`\`\` + +**Another code block (JavaScript):** + +\`\`\`javascript +// Testing syntax highlighting +const data = { name: "test", value: 123 }; +console.log(data); +\`\`\``, + author_type: 'agent', + type: 'message', + created_at: '2024-01-20T10:04:00Z', + }, + + // Thinking message + { + id: 'thinking-1', + ...B, + type: 'thinking', + content: '', + author_type: 'agent', + created_at: '2024-01-20T10:05:00Z', + metadata: { + thinking: '[THINKING MESSAGE] This shows the agent\'s internal reasoning process. I need to check the current authentication implementation before suggesting changes. Let me search for existing auth files and understand the current architecture.', + }, + }, + + // Tool call - Read (complete) + { + id: 'tool-1', + ...B, + type: 'tool_call', + content: 'Read file', + author_type: 'agent', + created_at: '2024-01-20T10:06:00Z', + metadata: { + tool_call_id: 'tool-1', + tool_name: 'Read', + title: '[TOOL - READ - COMPLETE \u2713] Read src/auth/config.ts', + status: 'complete', + args: { + file_path: '/Users/demo/project/src/auth/config.ts', + line_count: 50, + }, + result: 'File content loaded successfully (50 lines)', + }, + }, + + // Tool call - Bash (running) + { + id: 'tool-2', + ...B, + type: 'tool_call', + content: 'Run command', + author_type: 'agent', + created_at: '2024-01-20T10:07:00Z', + metadata: { + tool_call_id: 'tool-2', + tool_name: 'Bash', + title: '[TOOL - BASH - RUNNING \u231B] npm test', + status: 'running', + args: { + command: 'npm test -- --coverage', + description: 'Run tests with coverage', + timeout: 30000, + }, + }, + }, + + // Tool call - Edit (error) + { + id: 'tool-3', + ...B, + type: 'tool_call', + content: 'Edit file', + author_type: 'agent', + created_at: '2024-01-20T10:08:00Z', + metadata: { + tool_call_id: 'tool-3', + tool_name: 'Edit', + title: '[TOOL - EDIT - ERROR \u2717] Edit src/components/Login.tsx', + status: 'error', + args: { + file_path: '/Users/demo/project/src/components/Login.tsx', + old_string: 'const handleLogin = () => {', + new_string: 'const handleLogin = async () => {', + }, + result: 'Error: old_string not found in file. Make sure the old_string exactly matches the content in the file.', + }, + }, + + // Tool call - Edit with complex metadata (complete) + { + id: 'tool-3b', + ...B, + type: 'tool_call', + content: 'Auggie Read `drizzle.config.ts`', + author_type: 'agent', + created_at: '2024-01-20T10:08:30Z', + metadata: { + tool_call_id: 'toolu_vrtx_01W75yrLi1WLgLCz8P3D8jjh', + tool_name: 'edit', + title: 'Edit drizzle.config.ts', + status: 'complete', + args: { + kind: 'edit', + locations: [ + { + path: '/Users/cfl/.kandev/worktrees/1_c28de20d/drizzle.config.ts', + }, + ], + path: '/Users/cfl/.kandev/worktrees/1_c28de20d/drizzle.config.ts', + raw_input: { + command: 'str_replace', + instruction_reminder: 'ALWAYS BREAK DOWN EDITS INTO SMALLER CHUNKS OF AT MOST 150 LINES EACH.', + new_str_1: 'export default defineConfig({\n schema: "./src/main/lib/db/schema/index.ts",\n dialect: "sqlite",\n})', + old_str_1: 'export default defineConfig({\n schema: "./src/main/lib/db/schema/index.ts",\n out: "./drizzle",\n dialect: "sqlite",\n})', + old_str_end_line_number_1: 7, + old_str_start_line_number_1: 3, + path: 'drizzle.config.ts', + }, + }, + }, + }, + + // Tool call - Search (pending, with permission) + { + id: 'tool-4', + ...B, + type: 'tool_call', + content: 'Search files', + author_type: 'agent', + created_at: '2024-01-20T10:09:00Z', + metadata: { + tool_call_id: 'tool-4', + tool_name: 'Grep', + title: '[TOOL - GREP - PENDING \uD83D\uDD12] Search for "authentication" in codebase', + status: 'pending', + args: { + pattern: 'authentication', + path: '/Users/demo/project/src', + output_mode: 'files_with_matches', + }, + }, + }, + + // Permission request (for tool-4) + { + id: 'permission-1', + ...B, + type: 'permission_request', + content: '[PERMISSION REQUEST] Approve/Reject action', + author_type: 'user', + created_at: '2024-01-20T10:09:01Z', + metadata: { + pending_id: 'perm-1', + tool_call_id: 'tool-4', + action_type: 'file_search', + action_details: { + path: '/Users/demo/project/src', + command: 'grep -r "authentication"', + }, + options: [ + { option_id: 'allow-1', name: 'Allow Once', kind: 'allow_once' }, + { option_id: 'allow-2', name: 'Allow Always', kind: 'allow_always' }, + { option_id: 'reject-1', name: 'Reject', kind: 'reject_once' }, + ], + status: 'pending', + }, + }, + + // Status message - simple (separator style) + { + id: 'status-0', + ...B, + type: 'status', + content: 'Agent initialized', + author_type: 'agent', + created_at: '2024-01-20T10:09:30Z', + }, + + // Status message - success + { + id: 'status-1', + ...B, + type: 'status', + content: '[STATUS MESSAGE - SUCCESS \u2713] Tests passed successfully! All 42 tests completed.', + author_type: 'agent', + created_at: '2024-01-20T10:10:00Z', + metadata: { + level: 'success', + }, + }, + + // Status message - error + { + id: 'status-2', + ...B, + type: 'error', + content: '[STATUS MESSAGE - ERROR \u2717] Build failed: TypeScript compilation error in auth.ts:45', + author_type: 'agent', + created_at: '2024-01-20T10:11:00Z', + metadata: { + level: 'error', + }, + }, + + // Todo message + { + id: 'todo-1', + ...B, + type: 'todo', + content: '[TODO MESSAGE] Task list', + author_type: 'agent', + created_at: '2024-01-20T10:12:00Z', + metadata: { + todos: [ + { id: '1', text: '[TODO ITEM - COMPLETED] Set up OAuth provider', completed: true }, + { id: '2', text: '[TODO ITEM - PENDING] Implement token validation middleware', completed: false }, + { id: '3', text: '[TODO ITEM - PENDING] Add refresh token endpoint', completed: false }, + { id: '4', text: '[TODO ITEM - PENDING] Write integration tests', completed: false }, + ], + }, + }, + + // Script execution message - running + { + id: 'script-1', + ...B, + type: 'script_execution', + content: '[SCRIPT EXECUTION MESSAGE - RUNNING] Setting up environment...\nInstalling dependencies...', + author_type: 'agent', + created_at: '2024-01-20T10:13:00Z', + metadata: { + script_type: 'setup', + command: './scripts/setup-env.sh', + status: 'running', + }, + }, + + // Script execution message - completed + { + id: 'script-2', + ...B, + type: 'script_execution', + content: '[SCRIPT EXECUTION MESSAGE - COMPLETED] Build successful!\nAll tests passed.\nDeployment ready.', + author_type: 'agent', + created_at: '2024-01-20T10:14:00Z', + metadata: { + script_type: 'cleanup', + command: 'npm run build', + status: 'exited', + exit_code: 0, + started_at: '2024-01-20T10:13:00Z', + completed_at: '2024-01-20T10:14:00Z', + }, + }, + + // Agent message - mixed content + { + id: 'agent-3', + ...B, + content: `**[MIXED CONTENT DEMO]** Testing nested and mixed elements: + +#### Heading 4 (h4 element) + +Regular paragraph with inline elements: \`code\`, **bold**, and more text. + +**Nested list structure:** + +1. **First level item** (ol > li with strong) + - Nested bullet (this should indent) + - Another nested item +2. Second level item with \`inline code\` +3. Third item + +**Technical details:** + +- File path: \`/src/components/Auth.tsx\` +- Function: \`authenticateUser(credentials)\` +- Status: **Active** (strong element) + +\`\`\`bash +# Code block with bash syntax +npm install +npm run dev +\`\`\` + +Final paragraph (p element) to wrap up.`, + author_type: 'agent', + type: 'message', + created_at: '2024-01-20T10:14:00Z', + }, + + // Agent message with rich blocks - thinking + { + id: 'agent-rich-1', + ...B, + content: `**[RICH BLOCK - THINKING]** Here's my analysis of the authentication system.`, + author_type: 'agent', + type: 'message', + created_at: '2024-01-20T10:15:00Z', + metadata: { + thinking: 'I need to carefully consider the security implications here. The current implementation uses JWT tokens, but we should add refresh token rotation to prevent token theft. We also need to implement proper CSRF protection for the authentication endpoints.', + }, + }, + + // Agent message with rich blocks - todos + { + id: 'agent-rich-2', + ...B, + content: `**[RICH BLOCK - TODOS]** Here are the next steps we need to take.`, + author_type: 'agent', + type: 'message', + created_at: '2024-01-20T10:16:00Z', + metadata: { + todos: [ + { text: 'Update the authentication middleware', done: true }, + { text: 'Add unit tests for token validation', done: false }, + { text: 'Implement rate limiting on login endpoint', done: false }, + 'Update documentation for new OAuth flow', + ], + }, + }, + + // Agent message with rich blocks - diff payload (structured) + { + id: 'agent-rich-3', + ...B, + content: `**[RICH BLOCK - DIFF PAYLOAD]** I've made the following changes to the authentication config:`, + author_type: 'agent', + type: 'message', + created_at: '2024-01-20T10:17:00Z', + metadata: { + diff: { + oldFile: { fileName: AUTH_CONFIG_PATH, fileLang: 'typescript' }, + newFile: { fileName: AUTH_CONFIG_PATH, fileLang: 'typescript' }, + hunks: [ + `diff --git a/src/auth/config.ts b/src/auth/config.ts +index 1234567..abcdefg 100644 +--- a/src/auth/config.ts ++++ b/src/auth/config.ts +@@ -1,7 +1,10 @@ + export const authConfig = { + clientId: process.env.OAUTH_CLIENT_ID, +- clientSecret: process.env.OAUTH_CLIENT_SECRET, ++ clientSecret: process.env.OAUTH_CLIENT_SECRET || '', + redirectUri: process.env.OAUTH_REDIRECT_URI, ++ tokenExpiry: '1h', ++ refreshTokenExpiry: '7d', ++ enableCsrf: true, + }; + + export function validateToken(token: string) {`, + ], + }, + }, + }, + + // Agent message with rich blocks - diff string (plain text) + { + id: 'agent-rich-4', + ...B, + content: `**[RICH BLOCK - DIFF STRING]** Here's the diff for the middleware changes:`, + author_type: 'agent', + type: 'message', + created_at: '2024-01-20T10:18:00Z', + metadata: { + diff: `--- a/src/middleware/auth.ts ++++ b/src/middleware/auth.ts +@@ -12,6 +12,11 @@ export async function authMiddleware(req: Request, res: Response, next: NextFun + const token = req.headers.authorization?.split(' ')[1]; + + if (!token) { +- return res.status(401).json({ error: 'No token provided' }); ++ return res.status(401).json({ ++ error: 'No token provided', ++ code: 'AUTH_TOKEN_MISSING', ++ timestamp: new Date().toISOString() ++ }); + } + + try {`, + }, + }, + + // Agent message with multiple rich blocks + { + id: 'agent-rich-5', + ...B, + content: `**[RICH BLOCK - ALL TYPES]** Here's my complete analysis with all the changes:`, + author_type: 'agent', + type: 'message', + created_at: '2024-01-20T10:19:00Z', + metadata: { + thinking: 'After reviewing the code, I found several areas that need improvement. The error handling is inconsistent and we need better type safety.', + todos: [ + { text: 'Add TypeScript strict mode', done: true }, + { text: 'Implement error boundaries', done: false }, + { text: 'Add logging middleware', done: false }, + ], + diff: { + oldFile: { fileName: 'src/types/auth.ts', fileLang: 'typescript' }, + newFile: { fileName: 'src/types/auth.ts', fileLang: 'typescript' }, + hunks: [ + `diff --git a/src/types/auth.ts b/src/types/auth.ts +index 9876543..fedcba9 100644 +--- a/src/types/auth.ts ++++ b/src/types/auth.ts +@@ -1,4 +1,12 @@ +-export type AuthToken = string; ++export interface AuthToken { ++ value: string; ++ expiresAt: Date; ++ type: 'access' | 'refresh'; ++} + +-export type User = { ++export interface User { + id: string; + email: string; ++ role: 'admin' | 'user'; ++ createdAt: Date; ++}`, + ], + }, + }, + }, + + // === Normalized Tool Messages (tool_edit, tool_read, tool_execute) === + + // Tool Edit - complete with diff + { + id: 'tool-edit-1', + ...B, + type: 'tool_edit', + content: 'Edit src/auth/config.ts', + author_type: 'agent', + created_at: '2024-01-20T10:20:00Z', + metadata: { + tool_call_id: 'tool-edit-1', + status: 'complete', + file_path: AUTH_CONFIG_PATH, + old_content: " clientSecret: process.env.OAUTH_CLIENT_SECRET,", + new_content: " clientSecret: process.env.OAUTH_CLIENT_SECRET || '',\n tokenExpiry: '1h',", + diff: { + hunks: [ + `diff --git a/src/auth/config.ts b/src/auth/config.ts +index 1234567..abcdefg 100644 +--- a/src/auth/config.ts ++++ b/src/auth/config.ts +@@ -2,3 +2,4 @@ + clientId: process.env.OAUTH_CLIENT_ID, +- clientSecret: process.env.OAUTH_CLIENT_SECRET, ++ clientSecret: process.env.OAUTH_CLIENT_SECRET || '', ++ tokenExpiry: '1h', + redirectUri: process.env.OAUTH_REDIRECT_URI,`, + ], + oldFile: { fileName: AUTH_CONFIG_PATH, fileLang: 'typescript' }, + newFile: { fileName: AUTH_CONFIG_PATH, fileLang: 'typescript' }, + }, + result: 'File edited successfully.', + }, + }, + + // Tool Edit - running (expanded) + { + id: 'tool-edit-2', + ...B, + type: 'tool_edit', + content: 'Edit src/middleware/auth.ts', + author_type: 'agent', + created_at: '2024-01-20T10:20:30Z', + metadata: { + tool_call_id: 'tool-edit-2', + status: 'running', + file_path: 'src/middleware/auth.ts', + old_content: "return res.status(401).json({ error: 'No token' });", + new_content: "return res.status(401).json({\n error: 'No token',\n code: 'AUTH_MISSING'\n});", + diff: { + hunks: [ + `diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts +--- a/src/middleware/auth.ts ++++ b/src/middleware/auth.ts +@@ -12,1 +12,4 @@ +-return res.status(401).json({ error: 'No token' }); ++return res.status(401).json({ ++ error: 'No token', ++ code: 'AUTH_MISSING' ++});`, + ], + oldFile: { fileName: 'src/middleware/auth.ts', fileLang: 'typescript' }, + newFile: { fileName: 'src/middleware/auth.ts', fileLang: 'typescript' }, + }, + }, + }, + + // Tool Edit - error + { + id: 'tool-edit-3', + ...B, + type: 'tool_edit', + content: 'Edit src/utils/format.ts', + author_type: 'agent', + created_at: '2024-01-20T10:21:00Z', + metadata: { + tool_call_id: 'tool-edit-3', + status: 'error', + file_path: 'src/utils/format.ts', + result: 'Error: old_string not found in file.', + }, + }, + + // Tool Read - complete + { + id: 'tool-read-1', + ...B, + type: 'tool_read', + content: 'Read src/auth/config.ts', + author_type: 'agent', + created_at: '2024-01-20T10:22:00Z', + metadata: { + tool_call_id: 'tool-read-1', + status: 'complete', + file_path: AUTH_CONFIG_PATH, + line_count: 50, + }, + }, + + // Tool Read - running + { + id: 'tool-read-2', + ...B, + type: 'tool_read', + content: 'Read package.json', + author_type: 'agent', + created_at: '2024-01-20T10:22:30Z', + metadata: { + tool_call_id: 'tool-read-2', + status: 'running', + file_path: 'package.json', + }, + }, + + // Tool Read - error + { + id: 'tool-read-3', + ...B, + type: 'tool_read', + content: 'Read /nonexistent/file.ts', + author_type: 'agent', + created_at: '2024-01-20T10:22:45Z', + metadata: { + tool_call_id: 'tool-read-3', + status: 'error', + file_path: '/nonexistent/file.ts', + }, + }, + + // Tool Execute - complete (success) + { + id: 'tool-exec-1', + ...B, + type: 'tool_execute', + content: 'Run `npm test -- --coverage`', + author_type: 'agent', + created_at: '2024-01-20T10:23:00Z', + metadata: { + tool_call_id: 'tool-exec-1', + status: 'complete', + command: 'npm test -- --coverage', + cwd: '/workspace', + exit_code: 0, + stdout: 'PASS src/auth/config.test.ts\nPASS src/middleware/auth.test.ts\n\nTest Suites: 2 passed, 2 total\nTests: 12 passed, 12 total\nCoverage: 87.5%', + result: 'Tests passed successfully.', + }, + }, + + // Tool Execute - running + { + id: 'tool-exec-2', + ...B, + type: 'tool_execute', + content: 'Run `go build ./...`', + author_type: 'agent', + created_at: '2024-01-20T10:23:30Z', + metadata: { + tool_call_id: 'tool-exec-2', + status: 'running', + command: 'go build ./...', + cwd: '/workspace', + }, + }, + + // Tool Execute - error with stderr + { + id: 'tool-exec-3', + ...B, + type: 'tool_execute', + content: 'Run `npm run lint`', + author_type: 'agent', + created_at: '2024-01-20T10:24:00Z', + metadata: { + tool_call_id: 'tool-exec-3', + status: 'complete', + command: 'npm run lint', + exit_code: 1, + stdout: '', + stderr: 'Error: src/auth/config.ts(5,1): Missing semicolon.\nError: src/middleware/auth.ts(12,3): Unexpected token.', + result: 'Lint failed with 2 errors.', + }, + }, +]; diff --git a/apps/web/app/demo/messages/page.tsx b/apps/web/app/demo/messages/page.tsx index 9c3db752bd..1647269ca5 100644 --- a/apps/web/app/demo/messages/page.tsx +++ b/apps/web/app/demo/messages/page.tsx @@ -2,738 +2,7 @@ import type { Message } from '@/lib/types/http'; import { MessageRenderer } from '@/components/task/chat/message-renderer'; - -const dummyMessages: Message[] = [ - // Task description - { - id: 'task-description', - session_id: 'demo', - task_id: 'demo-task', - content: '[TASK DESCRIPTION - AMBER BANNER] Implement a new feature to handle user authentication with OAuth 2.0', - author_type: 'user', - type: 'message', - created_at: '2024-01-20T10:00:00Z', - }, - - // User message - short - { - id: 'user-1', - session_id: 'demo', - task_id: 'demo-task', - content: '[USER MESSAGE - SHORT] Can you help me with this?', - author_type: 'user', - type: 'message', - created_at: '2024-01-20T10:01:00Z', - }, - - // User message - long - { - id: 'user-2', - session_id: 'demo', - task_id: 'demo-task', - content: '[USER MESSAGE - LONG] I need to implement a complex feature that involves multiple components. The requirements are:\n1. User authentication\n2. Role-based access control\n3. Session management\n4. Token refresh mechanism\n\nCan you help me design this system?', - author_type: 'user', - type: 'message', - created_at: '2024-01-20T10:02:00Z', - }, - - // Agent message - plain text - { - id: 'agent-1', - session_id: 'demo', - task_id: 'demo-task', - content: '[AGENT MESSAGE - PLAIN TEXT] This is a simple paragraph with no markdown formatting. Just plain text content.', - author_type: 'agent', - type: 'message', - created_at: '2024-01-20T10:03:00Z', - }, - - // Agent message - with markdown - { - id: 'agent-2', - session_id: 'demo', - task_id: 'demo-task', - content: `**[MARKDOWN DEMO]** Here's a message showing all markdown elements: - -## Heading 2 (h2 element) - -This is a **paragraph (p element)** with some **bold text (strong element)** to test styling. - -### Heading 3 (h3 element) - -**Ordered List (ol element):** - -1. First item (li element in ol) -2. Second item with **bold text** -3. Third item with inline code: \`const example = true\` -4. Fourth item (li element) - -**Unordered List (ul element):** - -- Bullet point one (li element in ul) -- Bullet point two -- Bullet point with \`inline code\` (code element) -- Another bullet point - -**Inline code examples (code element, inline):** -- Variable: \`const token = jwt.sign(payload)\` -- Function call: \`getUserData(userId)\` -- Path: \`/src/auth/config.ts\` - -**Code block example (code element, block):** - -\`\`\`typescript -// This is a code block (pre > code) -interface AuthConfig { - clientId: string; - clientSecret: string; - redirectUri: string; -} - -function authenticate(config: AuthConfig) { - return jwt.sign(config); -} -\`\`\` - -**Another code block (JavaScript):** - -\`\`\`javascript -// Testing syntax highlighting -const data = { name: "test", value: 123 }; -console.log(data); -\`\`\``, - author_type: 'agent', - type: 'message', - created_at: '2024-01-20T10:04:00Z', - }, - - // Thinking message - { - id: 'thinking-1', - session_id: 'demo', - task_id: 'demo-task', - type: 'thinking', - content: '', - author_type: 'agent', - created_at: '2024-01-20T10:05:00Z', - metadata: { - thinking: '[THINKING MESSAGE] This shows the agent\'s internal reasoning process. I need to check the current authentication implementation before suggesting changes. Let me search for existing auth files and understand the current architecture.', - }, - }, - - // Tool call - Read (complete) - { - id: 'tool-1', - session_id: 'demo', - task_id: 'demo-task', - type: 'tool_call', - content: 'Read file', - author_type: 'agent', - created_at: '2024-01-20T10:06:00Z', - metadata: { - tool_call_id: 'tool-1', - tool_name: 'Read', - title: '[TOOL - READ - COMPLETE ✓] Read src/auth/config.ts', - status: 'complete', - args: { - file_path: '/Users/demo/project/src/auth/config.ts', - line_count: 50, - }, - result: 'File content loaded successfully (50 lines)', - }, - }, - - // Tool call - Bash (running) - { - id: 'tool-2', - session_id: 'demo', - task_id: 'demo-task', - type: 'tool_call', - content: 'Run command', - author_type: 'agent', - created_at: '2024-01-20T10:07:00Z', - metadata: { - tool_call_id: 'tool-2', - tool_name: 'Bash', - title: '[TOOL - BASH - RUNNING ⏳] npm test', - status: 'running', - args: { - command: 'npm test -- --coverage', - description: 'Run tests with coverage', - timeout: 30000, - }, - }, - }, - - // Tool call - Edit (error) - { - id: 'tool-3', - session_id: 'demo', - task_id: 'demo-task', - type: 'tool_call', - content: 'Edit file', - author_type: 'agent', - created_at: '2024-01-20T10:08:00Z', - metadata: { - tool_call_id: 'tool-3', - tool_name: 'Edit', - title: '[TOOL - EDIT - ERROR ✗] Edit src/components/Login.tsx', - status: 'error', - args: { - file_path: '/Users/demo/project/src/components/Login.tsx', - old_string: 'const handleLogin = () => {', - new_string: 'const handleLogin = async () => {', - }, - result: 'Error: old_string not found in file. Make sure the old_string exactly matches the content in the file.', - }, - }, - - // Tool call - Edit with complex metadata (complete) - { - id: 'tool-3b', - session_id: 'demo', - task_id: 'demo-task', - type: 'tool_call', - content: 'Auggie Read `drizzle.config.ts`', - author_type: 'agent', - created_at: '2024-01-20T10:08:30Z', - metadata: { - tool_call_id: 'toolu_vrtx_01W75yrLi1WLgLCz8P3D8jjh', - tool_name: 'edit', - title: 'Edit drizzle.config.ts', - status: 'complete', - args: { - kind: 'edit', - locations: [ - { - path: '/Users/cfl/.kandev/worktrees/1_c28de20d/drizzle.config.ts', - }, - ], - path: '/Users/cfl/.kandev/worktrees/1_c28de20d/drizzle.config.ts', - raw_input: { - command: 'str_replace', - instruction_reminder: 'ALWAYS BREAK DOWN EDITS INTO SMALLER CHUNKS OF AT MOST 150 LINES EACH.', - new_str_1: 'export default defineConfig({\n schema: "./src/main/lib/db/schema/index.ts",\n dialect: "sqlite",\n})', - old_str_1: 'export default defineConfig({\n schema: "./src/main/lib/db/schema/index.ts",\n out: "./drizzle",\n dialect: "sqlite",\n})', - old_str_end_line_number_1: 7, - old_str_start_line_number_1: 3, - path: 'drizzle.config.ts', - }, - }, - }, - }, - - // Tool call - Search (pending, with permission) - { - id: 'tool-4', - session_id: 'demo', - task_id: 'demo-task', - type: 'tool_call', - content: 'Search files', - author_type: 'agent', - created_at: '2024-01-20T10:09:00Z', - metadata: { - tool_call_id: 'tool-4', - tool_name: 'Grep', - title: '[TOOL - GREP - PENDING 🔒] Search for "authentication" in codebase', - status: 'pending', - args: { - pattern: 'authentication', - path: '/Users/demo/project/src', - output_mode: 'files_with_matches', - }, - }, - }, - - // Permission request (for tool-4) - { - id: 'permission-1', - session_id: 'demo', - task_id: 'demo-task', - type: 'permission_request', - content: '[PERMISSION REQUEST] Approve/Reject action', - author_type: 'user', - created_at: '2024-01-20T10:09:01Z', - metadata: { - pending_id: 'perm-1', - tool_call_id: 'tool-4', - action_type: 'file_search', - action_details: { - path: '/Users/demo/project/src', - command: 'grep -r "authentication"', - }, - options: [ - { option_id: 'allow-1', name: 'Allow Once', kind: 'allow_once' }, - { option_id: 'allow-2', name: 'Allow Always', kind: 'allow_always' }, - { option_id: 'reject-1', name: 'Reject', kind: 'reject_once' }, - ], - status: 'pending', - }, - }, - - // Status message - simple (separator style) - { - id: 'status-0', - session_id: 'demo', - task_id: 'demo-task', - type: 'status', - content: 'Agent initialized', - author_type: 'agent', - created_at: '2024-01-20T10:09:30Z', - }, - - // Status message - success - { - id: 'status-1', - session_id: 'demo', - task_id: 'demo-task', - type: 'status', - content: '[STATUS MESSAGE - SUCCESS ✓] Tests passed successfully! All 42 tests completed.', - author_type: 'agent', - created_at: '2024-01-20T10:10:00Z', - metadata: { - level: 'success', - }, - }, - - // Status message - error - { - id: 'status-2', - session_id: 'demo', - task_id: 'demo-task', - type: 'error', - content: '[STATUS MESSAGE - ERROR ✗] Build failed: TypeScript compilation error in auth.ts:45', - author_type: 'agent', - created_at: '2024-01-20T10:11:00Z', - metadata: { - level: 'error', - }, - }, - - // Todo message - { - id: 'todo-1', - session_id: 'demo', - task_id: 'demo-task', - type: 'todo', - content: '[TODO MESSAGE] Task list', - author_type: 'agent', - created_at: '2024-01-20T10:12:00Z', - metadata: { - todos: [ - { id: '1', text: '[TODO ITEM - COMPLETED] Set up OAuth provider', completed: true }, - { id: '2', text: '[TODO ITEM - PENDING] Implement token validation middleware', completed: false }, - { id: '3', text: '[TODO ITEM - PENDING] Add refresh token endpoint', completed: false }, - { id: '4', text: '[TODO ITEM - PENDING] Write integration tests', completed: false }, - ], - }, - }, - - // Script execution message - running - { - id: 'script-1', - session_id: 'demo', - task_id: 'demo-task', - type: 'script_execution', - content: '[SCRIPT EXECUTION MESSAGE - RUNNING] Setting up environment...\nInstalling dependencies...', - author_type: 'agent', - created_at: '2024-01-20T10:13:00Z', - metadata: { - script_type: 'setup', - command: './scripts/setup-env.sh', - status: 'running', - }, - }, - - // Script execution message - completed - { - id: 'script-2', - session_id: 'demo', - task_id: 'demo-task', - type: 'script_execution', - content: '[SCRIPT EXECUTION MESSAGE - COMPLETED] Build successful!\nAll tests passed.\nDeployment ready.', - author_type: 'agent', - created_at: '2024-01-20T10:14:00Z', - metadata: { - script_type: 'cleanup', - command: 'npm run build', - status: 'exited', - exit_code: 0, - started_at: '2024-01-20T10:13:00Z', - completed_at: '2024-01-20T10:14:00Z', - }, - }, - - // Agent message - mixed content - { - id: 'agent-3', - session_id: 'demo', - task_id: 'demo-task', - content: `**[MIXED CONTENT DEMO]** Testing nested and mixed elements: - -#### Heading 4 (h4 element) - -Regular paragraph with inline elements: \`code\`, **bold**, and more text. - -**Nested list structure:** - -1. **First level item** (ol > li with strong) - - Nested bullet (this should indent) - - Another nested item -2. Second level item with \`inline code\` -3. Third item - -**Technical details:** - -- File path: \`/src/components/Auth.tsx\` -- Function: \`authenticateUser(credentials)\` -- Status: **Active** (strong element) - -\`\`\`bash -# Code block with bash syntax -npm install -npm run dev -\`\`\` - -Final paragraph (p element) to wrap up.`, - author_type: 'agent', - type: 'message', - created_at: '2024-01-20T10:14:00Z', - }, - - // Agent message with rich blocks - thinking - { - id: 'agent-rich-1', - session_id: 'demo', - task_id: 'demo-task', - content: `**[RICH BLOCK - THINKING]** Here's my analysis of the authentication system.`, - author_type: 'agent', - type: 'message', - created_at: '2024-01-20T10:15:00Z', - metadata: { - thinking: 'I need to carefully consider the security implications here. The current implementation uses JWT tokens, but we should add refresh token rotation to prevent token theft. We also need to implement proper CSRF protection for the authentication endpoints.', - }, - }, - - // Agent message with rich blocks - todos - { - id: 'agent-rich-2', - session_id: 'demo', - task_id: 'demo-task', - content: `**[RICH BLOCK - TODOS]** Here are the next steps we need to take.`, - author_type: 'agent', - type: 'message', - created_at: '2024-01-20T10:16:00Z', - metadata: { - todos: [ - { text: 'Update the authentication middleware', done: true }, - { text: 'Add unit tests for token validation', done: false }, - { text: 'Implement rate limiting on login endpoint', done: false }, - 'Update documentation for new OAuth flow', - ], - }, - }, - - // Agent message with rich blocks - diff payload (structured) - { - id: 'agent-rich-3', - session_id: 'demo', - task_id: 'demo-task', - content: `**[RICH BLOCK - DIFF PAYLOAD]** I've made the following changes to the authentication config:`, - author_type: 'agent', - type: 'message', - created_at: '2024-01-20T10:17:00Z', - metadata: { - diff: { - oldFile: { fileName: 'src/auth/config.ts', fileLang: 'typescript' }, - newFile: { fileName: 'src/auth/config.ts', fileLang: 'typescript' }, - hunks: [ - `diff --git a/src/auth/config.ts b/src/auth/config.ts -index 1234567..abcdefg 100644 ---- a/src/auth/config.ts -+++ b/src/auth/config.ts -@@ -1,7 +1,10 @@ - export const authConfig = { - clientId: process.env.OAUTH_CLIENT_ID, -- clientSecret: process.env.OAUTH_CLIENT_SECRET, -+ clientSecret: process.env.OAUTH_CLIENT_SECRET || '', - redirectUri: process.env.OAUTH_REDIRECT_URI, -+ tokenExpiry: '1h', -+ refreshTokenExpiry: '7d', -+ enableCsrf: true, - }; - - export function validateToken(token: string) {`, - ], - }, - }, - }, - - // Agent message with rich blocks - diff string (plain text) - { - id: 'agent-rich-4', - session_id: 'demo', - task_id: 'demo-task', - content: `**[RICH BLOCK - DIFF STRING]** Here's the diff for the middleware changes:`, - author_type: 'agent', - type: 'message', - created_at: '2024-01-20T10:18:00Z', - metadata: { - diff: `--- a/src/middleware/auth.ts -+++ b/src/middleware/auth.ts -@@ -12,6 +12,11 @@ export async function authMiddleware(req: Request, res: Response, next: NextFun - const token = req.headers.authorization?.split(' ')[1]; - - if (!token) { -- return res.status(401).json({ error: 'No token provided' }); -+ return res.status(401).json({ -+ error: 'No token provided', -+ code: 'AUTH_TOKEN_MISSING', -+ timestamp: new Date().toISOString() -+ }); - } - - try {`, - }, - }, - - // Agent message with multiple rich blocks - { - id: 'agent-rich-5', - session_id: 'demo', - task_id: 'demo-task', - content: `**[RICH BLOCK - ALL TYPES]** Here's my complete analysis with all the changes:`, - author_type: 'agent', - type: 'message', - created_at: '2024-01-20T10:19:00Z', - metadata: { - thinking: 'After reviewing the code, I found several areas that need improvement. The error handling is inconsistent and we need better type safety.', - todos: [ - { text: 'Add TypeScript strict mode', done: true }, - { text: 'Implement error boundaries', done: false }, - { text: 'Add logging middleware', done: false }, - ], - diff: { - oldFile: { fileName: 'src/types/auth.ts', fileLang: 'typescript' }, - newFile: { fileName: 'src/types/auth.ts', fileLang: 'typescript' }, - hunks: [ - `diff --git a/src/types/auth.ts b/src/types/auth.ts -index 9876543..fedcba9 100644 ---- a/src/types/auth.ts -+++ b/src/types/auth.ts -@@ -1,4 +1,12 @@ --export type AuthToken = string; -+export interface AuthToken { -+ value: string; -+ expiresAt: Date; -+ type: 'access' | 'refresh'; -+} - --export type User = { -+export interface User { - id: string; - email: string; -+ role: 'admin' | 'user'; -+ createdAt: Date; -+}`, - ], - }, - }, - }, - - // === Normalized Tool Messages (tool_edit, tool_read, tool_execute) === - - // Tool Edit - complete with diff - { - id: 'tool-edit-1', - session_id: 'demo', - task_id: 'demo-task', - type: 'tool_edit', - content: 'Edit src/auth/config.ts', - author_type: 'agent', - created_at: '2024-01-20T10:20:00Z', - metadata: { - tool_call_id: 'tool-edit-1', - status: 'complete', - file_path: 'src/auth/config.ts', - old_content: " clientSecret: process.env.OAUTH_CLIENT_SECRET,", - new_content: " clientSecret: process.env.OAUTH_CLIENT_SECRET || '',\n tokenExpiry: '1h',", - diff: { - hunks: [ - `diff --git a/src/auth/config.ts b/src/auth/config.ts -index 1234567..abcdefg 100644 ---- a/src/auth/config.ts -+++ b/src/auth/config.ts -@@ -2,3 +2,4 @@ - clientId: process.env.OAUTH_CLIENT_ID, -- clientSecret: process.env.OAUTH_CLIENT_SECRET, -+ clientSecret: process.env.OAUTH_CLIENT_SECRET || '', -+ tokenExpiry: '1h', - redirectUri: process.env.OAUTH_REDIRECT_URI,`, - ], - oldFile: { fileName: 'src/auth/config.ts', fileLang: 'typescript' }, - newFile: { fileName: 'src/auth/config.ts', fileLang: 'typescript' }, - }, - result: 'File edited successfully.', - }, - }, - - // Tool Edit - running (expanded) - { - id: 'tool-edit-2', - session_id: 'demo', - task_id: 'demo-task', - type: 'tool_edit', - content: 'Edit src/middleware/auth.ts', - author_type: 'agent', - created_at: '2024-01-20T10:20:30Z', - metadata: { - tool_call_id: 'tool-edit-2', - status: 'running', - file_path: 'src/middleware/auth.ts', - old_content: "return res.status(401).json({ error: 'No token' });", - new_content: "return res.status(401).json({\n error: 'No token',\n code: 'AUTH_MISSING'\n});", - diff: { - hunks: [ - `diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts ---- a/src/middleware/auth.ts -+++ b/src/middleware/auth.ts -@@ -12,1 +12,4 @@ --return res.status(401).json({ error: 'No token' }); -+return res.status(401).json({ -+ error: 'No token', -+ code: 'AUTH_MISSING' -+});`, - ], - oldFile: { fileName: 'src/middleware/auth.ts', fileLang: 'typescript' }, - newFile: { fileName: 'src/middleware/auth.ts', fileLang: 'typescript' }, - }, - }, - }, - - // Tool Edit - error - { - id: 'tool-edit-3', - session_id: 'demo', - task_id: 'demo-task', - type: 'tool_edit', - content: 'Edit src/utils/format.ts', - author_type: 'agent', - created_at: '2024-01-20T10:21:00Z', - metadata: { - tool_call_id: 'tool-edit-3', - status: 'error', - file_path: 'src/utils/format.ts', - result: 'Error: old_string not found in file.', - }, - }, - - // Tool Read - complete - { - id: 'tool-read-1', - session_id: 'demo', - task_id: 'demo-task', - type: 'tool_read', - content: 'Read src/auth/config.ts', - author_type: 'agent', - created_at: '2024-01-20T10:22:00Z', - metadata: { - tool_call_id: 'tool-read-1', - status: 'complete', - file_path: 'src/auth/config.ts', - line_count: 50, - }, - }, - - // Tool Read - running - { - id: 'tool-read-2', - session_id: 'demo', - task_id: 'demo-task', - type: 'tool_read', - content: 'Read package.json', - author_type: 'agent', - created_at: '2024-01-20T10:22:30Z', - metadata: { - tool_call_id: 'tool-read-2', - status: 'running', - file_path: 'package.json', - }, - }, - - // Tool Read - error - { - id: 'tool-read-3', - session_id: 'demo', - task_id: 'demo-task', - type: 'tool_read', - content: 'Read /nonexistent/file.ts', - author_type: 'agent', - created_at: '2024-01-20T10:22:45Z', - metadata: { - tool_call_id: 'tool-read-3', - status: 'error', - file_path: '/nonexistent/file.ts', - }, - }, - - // Tool Execute - complete (success) - { - id: 'tool-exec-1', - session_id: 'demo', - task_id: 'demo-task', - type: 'tool_execute', - content: 'Run `npm test -- --coverage`', - author_type: 'agent', - created_at: '2024-01-20T10:23:00Z', - metadata: { - tool_call_id: 'tool-exec-1', - status: 'complete', - command: 'npm test -- --coverage', - cwd: '/workspace', - exit_code: 0, - stdout: 'PASS src/auth/config.test.ts\nPASS src/middleware/auth.test.ts\n\nTest Suites: 2 passed, 2 total\nTests: 12 passed, 12 total\nCoverage: 87.5%', - result: 'Tests passed successfully.', - }, - }, - - // Tool Execute - running - { - id: 'tool-exec-2', - session_id: 'demo', - task_id: 'demo-task', - type: 'tool_execute', - content: 'Run `go build ./...`', - author_type: 'agent', - created_at: '2024-01-20T10:23:30Z', - metadata: { - tool_call_id: 'tool-exec-2', - status: 'running', - command: 'go build ./...', - cwd: '/workspace', - }, - }, - - // Tool Execute - error with stderr - { - id: 'tool-exec-3', - session_id: 'demo', - task_id: 'demo-task', - type: 'tool_execute', - content: 'Run `npm run lint`', - author_type: 'agent', - created_at: '2024-01-20T10:24:00Z', - metadata: { - tool_call_id: 'tool-exec-3', - status: 'complete', - command: 'npm run lint', - exit_code: 1, - stdout: '', - stderr: 'Error: src/auth/config.ts(5,1): Missing semicolon.\nError: src/middleware/auth.ts(12,3): Unexpected token.', - result: 'Lint failed with 2 errors.', - }, - }, -]; +import { dummyMessages } from './demo-messages-data'; export default function MessagesDemoPage() { const permissionsByToolCallId = new Map(); diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 484679da2f..e206d16f20 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -3,70 +3,127 @@ import { StateHydrator } from '@/components/state-hydrator'; import { fetchWorkflowSnapshot, fetchUserSettings, listWorkflows, listRepositories, listWorkspaces, listTaskSessionMessages } from '@/lib/api'; import { snapshotToState } from '@/lib/ssr/mapper'; import type { AppState } from '@/lib/state/store'; +import type { ListWorkspacesResponse, UserSettings, UserSettingsResponse } from '@/lib/types/http'; // Server Component: runs on the server for SSR and data hydration. type PageProps = { searchParams?: Promise>; }; +function resolveParam(value: string | string[] | undefined): string | undefined { + return Array.isArray(value) ? value[0] : value; +} + +type WorkspaceItem = ListWorkspacesResponse['workspaces'][number]; +function mapWorkspaceItem(ws: WorkspaceItem) { + return { + id: ws.id, + name: ws.name, + description: ws.description ?? null, + owner_id: ws.owner_id, + default_executor_id: ws.default_executor_id ?? null, + default_environment_id: ws.default_environment_id ?? null, + default_agent_profile_id: ws.default_agent_profile_id ?? null, + created_at: ws.created_at, + updated_at: ws.updated_at, + }; +} + +function buildLspConfig(s: UserSettings) { + return { + lspAutoStartLanguages: s.lsp_auto_start_languages ?? [], + lspAutoInstallLanguages: s.lsp_auto_install_languages ?? [], + lspServerConfigs: s.lsp_server_configs ?? {}, + }; +} + +function buildUserSettingsState(resp: UserSettingsResponse | null, workspaceId: string | null): AppState['userSettings'] { + const s = resp?.settings; + const shellOptions = resp ? (resp.shell_options ?? []) : []; + if (!s) { + return { + workspaceId, workflowId: null, kanbanViewMode: null, repositoryIds: [], preferredShell: null, + shellOptions, defaultEditorId: null, enablePreviewOnClick: false, chatSubmitKey: 'cmd_enter', + reviewAutoMarkOnScroll: true, lspAutoStartLanguages: [], lspAutoInstallLanguages: [], lspServerConfigs: {}, loaded: false, + }; + } + return { + workspaceId, + workflowId: s.workflow_filter_id || null, + kanbanViewMode: s.kanban_view_mode || null, + repositoryIds: Array.from(new Set(s.repository_ids ?? [])).sort(), + preferredShell: s.preferred_shell || null, + shellOptions, + defaultEditorId: s.default_editor_id || null, + enablePreviewOnClick: s.enable_preview_on_click ?? false, + chatSubmitKey: s.chat_submit_key ?? 'cmd_enter', + reviewAutoMarkOnScroll: s.review_auto_mark_on_scroll ?? true, + ...buildLspConfig(s), + loaded: true, + }; +} + +function resolveActiveId(items: T[], preferredId?: string, fallbackId?: string | null): string | null { + return items.find((i) => i.id === preferredId)?.id ?? items.find((i) => i.id === fallbackId)?.id ?? items[0]?.id ?? null; +} + +function buildBaseState( + workspaces: ListWorkspacesResponse, + userSettingsResponse: UserSettingsResponse | null, + activeWorkspaceId: string | null, +): Partial { + return { + workspaces: { + items: workspaces.workspaces.map(mapWorkspaceItem), + activeId: activeWorkspaceId, + }, + userSettings: buildUserSettingsState(userSettingsResponse, activeWorkspaceId), + }; +} + +async function loadSessionMessages( + sessionId: string, + state: Partial, +): Promise> { + try { + const messagesResponse = await listTaskSessionMessages(sessionId, { limit: 50, sort: 'desc' }, { cache: 'no-store' }); + const messages = [...(messagesResponse.messages ?? [])].reverse(); + return { + ...state, + messages: { + bySession: { [sessionId]: messages }, + metaBySession: { + [sessionId]: { + isLoading: false, + hasMore: messagesResponse.has_more ?? false, + oldestCursor: messages[0]?.id ?? null, + }, + }, + }, + }; + } catch (error) { + console.warn('Could not SSR messages (client will load via WebSocket):', error instanceof Error ? error.message : String(error)); + return state; + } +} + export default async function Page({ searchParams }: PageProps) { try { const resolvedParams = searchParams ? await searchParams : {}; - const workspaceParam = resolvedParams.workspaceId; - const workflowParam = resolvedParams.workflowId; - const taskIdParam = resolvedParams.taskId; - const sessionIdParam = resolvedParams.sessionId; - const workspaceId = Array.isArray(workspaceParam) ? workspaceParam[0] : workspaceParam; - const workflowIdParam = Array.isArray(workflowParam) ? workflowParam[0] : workflowParam; - const taskId = Array.isArray(taskIdParam) ? taskIdParam[0] : taskIdParam; - const sessionId = Array.isArray(sessionIdParam) ? sessionIdParam[0] : sessionIdParam; + const workspaceId = resolveParam(resolvedParams.workspaceId); + const workflowIdParam = resolveParam(resolvedParams.workflowId); + const taskId = resolveParam(resolvedParams.taskId); + const sessionId = resolveParam(resolvedParams.sessionId); const [workspaces, userSettingsResponse] = await Promise.all([ listWorkspaces({ cache: 'no-store' }), fetchUserSettings({ cache: 'no-store' }).catch(() => null), ]); - const userSettings = userSettingsResponse?.settings; - const settingsWorkspaceId = userSettings?.workspace_id || null; - const settingsWorkflowId = userSettings?.workflow_filter_id || null; - const settingsRepositoryIds = Array.from(new Set(userSettings?.repository_ids ?? [])).sort(); - const settingsEnablePreviewOnClick = userSettings?.enable_preview_on_click ?? false; - const activeWorkspaceId = - workspaces.workspaces.find((workspace) => workspace.id === workspaceId)?.id ?? - workspaces.workspaces.find((workspace) => workspace.id === settingsWorkspaceId)?.id ?? - workspaces.workspaces[0]?.id ?? - null; - let initialState: Partial = { - workspaces: { - items: workspaces.workspaces.map((workspace) => ({ - id: workspace.id, - name: workspace.name, - description: workspace.description ?? null, - owner_id: workspace.owner_id, - default_executor_id: workspace.default_executor_id ?? null, - default_environment_id: workspace.default_environment_id ?? null, - default_agent_profile_id: workspace.default_agent_profile_id ?? null, - created_at: workspace.created_at, - updated_at: workspace.updated_at, - })), - activeId: activeWorkspaceId, - }, - userSettings: { - workspaceId: activeWorkspaceId, - workflowId: settingsWorkflowId, - kanbanViewMode: userSettings?.kanban_view_mode || null, - repositoryIds: settingsRepositoryIds, - preferredShell: userSettings?.preferred_shell || null, - shellOptions: userSettingsResponse?.shell_options ?? [], - defaultEditorId: userSettings?.default_editor_id || null, - enablePreviewOnClick: settingsEnablePreviewOnClick, - chatSubmitKey: userSettings?.chat_submit_key ?? 'cmd_enter', - reviewAutoMarkOnScroll: userSettings?.review_auto_mark_on_scroll ?? true, - lspAutoStartLanguages: userSettings?.lsp_auto_start_languages ?? [], - lspAutoInstallLanguages: userSettings?.lsp_auto_install_languages ?? [], - lspServerConfigs: userSettings?.lsp_server_configs ?? {}, - loaded: Boolean(userSettings), - }, - }; + const settingsWorkspaceId = userSettingsResponse?.settings?.workspace_id || null; + const settingsWorkflowId = userSettingsResponse?.settings?.workflow_filter_id || null; + const activeWorkspaceId = resolveActiveId(workspaces.workspaces, workspaceId, settingsWorkspaceId); + + let initialState = buildBaseState(workspaces, userSettingsResponse, activeWorkspaceId); if (!activeWorkspaceId) { return ( @@ -82,25 +139,17 @@ export default async function Page({ searchParams }: PageProps) { listRepositories(activeWorkspaceId, undefined, { cache: 'no-store' }).catch(() => ({ repositories: [] })), ]); - // Use workflowIdParam if it exists in this workspace, otherwise fall back - // Client-side code will handle task selection even if workflow doesn't match - const workflowId = - workflowList.workflows.find((workflow) => workflow.id === workflowIdParam)?.id ?? - workflowList.workflows.find((workflow) => workflow.id === settingsWorkflowId)?.id ?? - workflowList.workflows[0]?.id; + const workflowId = resolveActiveId(workflowList.workflows, workflowIdParam, settingsWorkflowId); + initialState = { ...initialState, userSettings: { ...(initialState.userSettings as AppState['userSettings']), - workflowId: workflowId ?? null, + workflowId, }, workflows: { - items: workflowList.workflows.map((workflow) => ({ - id: workflow.id, - workspaceId: workflow.workspace_id, - name: workflow.name, - })), - activeId: workflowId ?? null, + items: workflowList.workflows.map((w) => ({ id: w.id, workspaceId: w.workspace_id, name: w.name })), + activeId: workflowId, }, repositories: { itemsByWorkspaceId: { [activeWorkspaceId]: repositoriesResponse.repositories }, @@ -121,32 +170,8 @@ export default async function Page({ searchParams }: PageProps) { const snapshot = await fetchWorkflowSnapshot(workflowId, { cache: 'no-store' }); initialState = { ...initialState, ...snapshotToState(snapshot) }; - // Load messages for selected task session if provided (SSR optimization) if (taskId && sessionId) { - try { - // Load most recent messages in descending order, then reverse to show oldest-to-newest - const messagesResponse = await listTaskSessionMessages(sessionId, { limit: 50, sort: 'desc' }, { cache: 'no-store' }); - const messages = [...(messagesResponse.messages ?? [])].reverse(); - initialState = { - ...initialState, - messages: { - bySession: { - [sessionId]: messages, - }, - metaBySession: { - [sessionId]: { - isLoading: false, - hasMore: messagesResponse.has_more ?? false, - // oldestCursor should be the first (oldest) message ID for lazy loading older messages - oldestCursor: messages[0]?.id ?? null, - }, - }, - }, - }; - } catch (error) { - // SSR failed - client will load messages via WebSocket - console.warn('Could not SSR messages (client will load via WebSocket):', error instanceof Error ? error.message : String(error)); - } + initialState = await loadSessionMessages(sessionId, initialState); } return ( diff --git a/apps/web/app/s/[sessionId]/page.tsx b/apps/web/app/s/[sessionId]/page.tsx index adda5b20ac..2f8bafdd53 100644 --- a/apps/web/app/s/[sessionId]/page.tsx +++ b/apps/web/app/s/[sessionId]/page.tsx @@ -16,240 +16,275 @@ import { import { toAgentProfileOption } from '@/lib/state/slices/settings/types'; import { listSessionTurns } from '@/lib/api/domains/session-api'; import { fetchTerminals } from '@/lib/api/domains/user-shell-api'; -import type { ListMessagesResponse, Task } from '@/lib/types/http'; +import type { ListMessagesResponse, Task, TaskSession, UserSettingsResponse } from '@/lib/types/http'; import type { Terminal } from '@/hooks/domains/session/use-terminals'; import { snapshotToState, taskToState } from '@/lib/ssr/mapper'; import { TaskPageContent } from '@/components/task/task-page-content'; -export default async function SessionPage({ - params, -}: { - params: Promise<{ sessionId: string }>; -}) { - let initialState: ReturnType | null = null; - let task: Task | null = null; - let sessionId: string | null = null; - let initialTerminals: Terminal[] = []; +function buildWorktreeState(allSessions: TaskSession[]) { + const sessionsWithWorktrees = allSessions.filter((s) => s.worktree_id); + return { + worktrees: { + items: Object.fromEntries( + sessionsWithWorktrees.map((s) => [ + s.worktree_id, + { + id: s.worktree_id!, + sessionId: s.id, + repositoryId: s.repository_id ?? undefined, + path: s.worktree_path ?? undefined, + branch: s.worktree_branch ?? undefined, + }, + ]) + ), + }, + sessionWorktreesBySessionId: { + itemsBySessionId: Object.fromEntries( + sessionsWithWorktrees.map((s) => [s.id, [s.worktree_id!]]) + ), + }, + }; +} + +type UserSettingsSettings = NonNullable; + +function buildLspSettings(s: UserSettingsSettings | undefined) { + return { + lspAutoStartLanguages: s?.lsp_auto_start_languages ?? [], + lspAutoInstallLanguages: s?.lsp_auto_install_languages ?? [], + lspServerConfigs: s?.lsp_server_configs ?? {}, + }; +} + +function buildUserSettingsCore(s: UserSettingsSettings) { + return { + workspaceId: s.workspace_id || null, + workflowId: s.workflow_filter_id || null, + kanbanViewMode: s.kanban_view_mode || null, + repositoryIds: Array.from(new Set(s.repository_ids ?? [])).sort(), + preferredShell: s.preferred_shell || null, + defaultEditorId: s.default_editor_id || null, + enablePreviewOnClick: s.enable_preview_on_click ?? false, + chatSubmitKey: s.chat_submit_key ?? 'cmd_enter', + reviewAutoMarkOnScroll: s.review_auto_mark_on_scroll ?? true, + }; +} + +function buildUserSettingsFromResponse(userSettingsResponse: UserSettingsResponse | null) { + const s = userSettingsResponse?.settings; + const shellOptions = userSettingsResponse?.shell_options ?? []; + if (!s) return { workspaceId: null, workflowId: null, kanbanViewMode: null, repositoryIds: [], preferredShell: null, shellOptions, defaultEditorId: null, enablePreviewOnClick: false, chatSubmitKey: 'cmd_enter' as const, reviewAutoMarkOnScroll: true, loaded: false, ...buildLspSettings(undefined) }; + return { ...buildUserSettingsCore(s), shellOptions, loaded: true, ...buildLspSettings(s) }; +} + +type BuildSessionPageStateParams = { + task: Task; + sessionId: string; + snapshot: Awaited>; + agents: Awaited>; + repositories: Awaited>['repositories']; + allSessions: TaskSession[]; + availableAgents: Awaited>['agents']; + workspaces: Awaited>['workspaces']; + workflows: Awaited>['workflows']; + turns: Awaited>['turns']; + userSettingsResponse: UserSettingsResponse | null; + messagesResponse: ListMessagesResponse | null; +}; + +function buildSessionPageState(p: BuildSessionPageStateParams) { + const { + task, sessionId, snapshot, agents, repositories, + allSessions, availableAgents, workspaces, workflows, + turns, userSettingsResponse, messagesResponse, + } = p; + + const repositoryId = task.repositories?.[0]?.repository_id; + const repository = repositories.find((r) => r.id === repositoryId); + const scripts = repository?.scripts ?? []; + const messages = messagesResponse?.messages ? [...messagesResponse.messages].reverse() : []; + + const taskState = taskToState(task, sessionId, { + items: messages, + hasMore: messagesResponse?.has_more ?? false, + oldestCursor: messages[0]?.id ?? null, + }); + + const repositoryScripts = repositoryId + ? { + itemsByRepositoryId: { [repositoryId]: scripts }, + loadingByRepositoryId: { [repositoryId]: false }, + loadedByRepositoryId: { [repositoryId]: true }, + } + : { + itemsByRepositoryId: {}, + loadingByRepositoryId: {}, + loadedByRepositoryId: {}, + }; + + return { + ...snapshotToState(snapshot), + ...taskState, + workspaces: { + items: workspaces.map((workspace) => ({ + id: workspace.id, + name: workspace.name, + description: workspace.description ?? null, + owner_id: workspace.owner_id, + default_executor_id: workspace.default_executor_id ?? null, + default_environment_id: workspace.default_environment_id ?? null, + default_agent_profile_id: workspace.default_agent_profile_id ?? null, + created_at: workspace.created_at, + updated_at: workspace.updated_at, + })), + activeId: task.workspace_id, + }, + workflows: { + items: workflows.map((workflow) => ({ + id: workflow.id, + workspaceId: workflow.workspace_id, + name: workflow.name, + })), + activeId: task.workflow_id, + }, + repositories: { + itemsByWorkspaceId: { [task.workspace_id]: repositories }, + loadingByWorkspaceId: { [task.workspace_id]: false }, + loadedByWorkspaceId: { [task.workspace_id]: true }, + }, + repositoryScripts, + agentProfiles: { + items: agents.agents.flatMap((agent) => + agent.profiles.map((profile) => toAgentProfileOption(agent, profile)) + ), + version: 0, + }, + taskSessions: { + items: Object.fromEntries(allSessions.map((s) => [s.id, s])), + }, + taskSessionsByTask: { + itemsByTaskId: { [task.id]: allSessions }, + loadingByTaskId: { [task.id]: false }, + loadedByTaskId: { [task.id]: true }, + }, + turns: { + bySession: { [sessionId]: turns }, + activeBySession: { + [sessionId]: turns.filter((t) => !t.completed_at).pop()?.id ?? null, + }, + }, + ...buildWorktreeState(allSessions), + settingsAgents: { items: agents.agents }, + settingsData: { + agentsLoaded: true, + executorsLoaded: false, + environmentsLoaded: false, + }, + availableAgents: { items: availableAgents, loaded: true, loading: false }, + userSettings: buildUserSettingsFromResponse(userSettingsResponse), + }; +} + +type FetchedSessionData = { + task: Task; + sessionId: string; + initialState: ReturnType; + initialTerminals: Terminal[]; +}; + +async function fetchSessionData(paramSessionId: string): Promise { + const sessionResponse = await fetchTaskSession(paramSessionId, { cache: 'no-store' }); + const session = sessionResponse.session; + if (!session?.task_id) { + throw new Error('No task_id found for session'); + } + + const task = await fetchTask(session.task_id, { cache: 'no-store' }); + const [ + snapshot, agents, repositoriesResponse, allSessionsResponse, + availableAgentsResponse, workspacesResponse, workflowsResponse, + turnsResponse, userSettingsResponse, terminalsResponse, + ] = await Promise.all([ + fetchWorkflowSnapshot(task.workflow_id, { cache: 'no-store' }), + listAgents({ cache: 'no-store' }), + listRepositories(task.workspace_id, { includeScripts: true }, { cache: 'no-store' }), + listTaskSessions(session.task_id, { cache: 'no-store' }), + listAvailableAgents({ cache: 'no-store' }).catch(() => ({ agents: [] })), + listWorkspaces({ cache: 'no-store' }).catch(() => ({ workspaces: [] })), + listWorkflows(task.workspace_id, { cache: 'no-store' }).catch(() => ({ workflows: [] })), + listSessionTurns(paramSessionId, { cache: 'no-store' }).catch(() => ({ turns: [], total: 0 })), + fetchUserSettings({ cache: 'no-store' }).catch(() => null), + fetchTerminals(paramSessionId).catch(() => []), + ]); + + const repositories = repositoriesResponse.repositories ?? []; + const allSessions = allSessionsResponse.sessions ?? [session]; + const availableAgents = availableAgentsResponse.agents ?? []; + const workspaces = workspacesResponse.workspaces ?? []; + const workflows = workflowsResponse.workflows ?? []; + const turns = turnsResponse.turns ?? []; + + const initialTerminals: Terminal[] = terminalsResponse.map((t) => ({ + id: t.terminal_id, + type: t.initial_command ? 'script' as const : 'shell' as const, + label: t.label, + closable: t.closable, + })); + + let messagesResponse: ListMessagesResponse | null = null; + try { + messagesResponse = await listTaskSessionMessages( + paramSessionId, + { limit: 50, sort: 'desc' }, + { cache: 'no-store' } + ); + } catch (error) { + console.warn( + 'Could not load session messages for SSR:', + error instanceof Error ? error.message : String(error) + ); + } + + const initialState = buildSessionPageState({ + task, sessionId: paramSessionId, snapshot, agents, + repositories, allSessions, availableAgents, + workspaces, workflows, turns, + userSettingsResponse, messagesResponse, + }); + + return { task, sessionId: paramSessionId, initialState, initialTerminals }; +} + +function extractInitialRepositories(initialState: FetchedSessionData['initialState'] | null, task: Task | null) { + return initialState?.repositories?.itemsByWorkspaceId?.[task?.workspace_id ?? ''] ?? []; +} + +function extractInitialScripts(initialState: FetchedSessionData['initialState'] | null, task: Task | null) { + const repoId = task?.repositories?.[0]?.repository_id ?? ''; + return initialState?.repositoryScripts?.itemsByRepositoryId?.[repoId] ?? []; +} + +export default async function SessionPage({ params }: { params: Promise<{ sessionId: string }> }) { + let fetchedData: FetchedSessionData | null = null; const defaultLayouts = await readLayoutDefaults(); try { const { sessionId: paramSessionId } = await params; - sessionId = paramSessionId; - - const sessionResponse = await fetchTaskSession(sessionId, { cache: 'no-store' }); - const session = sessionResponse.session; - if (!session?.task_id) { - throw new Error('No task_id found for session'); - } - - task = await fetchTask(session.task_id, { cache: 'no-store' }); - const [snapshot, agents, repositoriesResponse, allSessionsResponse, availableAgentsResponse, workspacesResponse, workflowsResponse, turnsResponse, userSettingsResponse, terminalsResponse] = await Promise.all([ - fetchWorkflowSnapshot(task.workflow_id, { cache: 'no-store' }), - listAgents({ cache: 'no-store' }), - listRepositories(task.workspace_id, { includeScripts: true }, { cache: 'no-store' }), - listTaskSessions(session.task_id, { cache: 'no-store' }), - listAvailableAgents({ cache: 'no-store' }).catch(() => ({ agents: [] })), - listWorkspaces({ cache: 'no-store' }).catch(() => ({ workspaces: [] })), - listWorkflows(task.workspace_id, { cache: 'no-store' }).catch(() => ({ workflows: [] })), - listSessionTurns(sessionId, { cache: 'no-store' }).catch(() => ({ turns: [], total: 0 })), - fetchUserSettings({ cache: 'no-store' }).catch(() => null), - fetchTerminals(sessionId).catch(() => []), - ]); - const repositories = repositoriesResponse.repositories ?? []; - const allSessions = allSessionsResponse.sessions ?? [session]; - const availableAgents = availableAgentsResponse.agents ?? []; - const workspaces = workspacesResponse.workspaces ?? []; - const workflows = workflowsResponse.workflows ?? []; - const turns = turnsResponse.turns ?? []; - const userSettings = userSettingsResponse?.settings; - - // Transform terminals to frontend format - initialTerminals = terminalsResponse.map((t) => ({ - id: t.terminal_id, - type: t.initial_command ? 'script' as const : 'shell' as const, - label: t.label, - closable: t.closable, - })); - - // Get repository scripts from the repository (already fetched with includeScripts) - const repositoryId = task.repositories?.[0]?.repository_id; - const repository = repositories.find(r => r.id === repositoryId); - const scripts = repository?.scripts ?? []; - - let messagesResponse: ListMessagesResponse | null = null; - try { - // Load most recent messages in descending order, then reverse to show oldest-to-newest - messagesResponse = await listTaskSessionMessages( - sessionId, - { limit: 50, sort: 'desc' }, - { cache: 'no-store' } - ); - } catch (error) { - console.warn( - 'Could not load session messages for SSR:', - error instanceof Error ? error.message : String(error) - ); - } - - const messages = messagesResponse?.messages ? [...messagesResponse.messages].reverse() : []; - const taskState = taskToState(task, sessionId, { - items: messages, - hasMore: messagesResponse?.has_more ?? false, - // oldestCursor should be the first (oldest) message ID for lazy loading older messages - oldestCursor: messages[0]?.id ?? null, - }); - - initialState = { - ...snapshotToState(snapshot), - ...taskState, - workspaces: { - items: workspaces.map((workspace) => ({ - id: workspace.id, - name: workspace.name, - description: workspace.description ?? null, - owner_id: workspace.owner_id, - default_executor_id: workspace.default_executor_id ?? null, - default_environment_id: workspace.default_environment_id ?? null, - default_agent_profile_id: workspace.default_agent_profile_id ?? null, - created_at: workspace.created_at, - updated_at: workspace.updated_at, - })), - activeId: task.workspace_id, - }, - workflows: { - items: workflows.map((workflow) => ({ - id: workflow.id, - workspaceId: workflow.workspace_id, - name: workflow.name, - })), - activeId: task.workflow_id, - }, - repositories: { - itemsByWorkspaceId: { - [task.workspace_id]: repositories, - }, - loadingByWorkspaceId: { - [task.workspace_id]: false, - }, - loadedByWorkspaceId: { - [task.workspace_id]: true, - }, - }, - repositoryScripts: repositoryId ? { - itemsByRepositoryId: { - [repositoryId]: scripts, - }, - loadingByRepositoryId: { - [repositoryId]: false, - }, - loadedByRepositoryId: { - [repositoryId]: true, - }, - } : { - itemsByRepositoryId: {}, - loadingByRepositoryId: {}, - loadedByRepositoryId: {}, - }, - agentProfiles: { - items: agents.agents.flatMap((agent) => - agent.profiles.map((profile) => toAgentProfileOption(agent, profile)) - ), - version: 0, - }, - taskSessions: { - items: Object.fromEntries(allSessions.map((s) => [s.id, s])), - }, - taskSessionsByTask: { - itemsByTaskId: { - [task.id]: allSessions, - }, - loadingByTaskId: { - [task.id]: false, - }, - loadedByTaskId: { - [task.id]: true, - }, - }, - turns: { - bySession: { - [sessionId]: turns, - }, - activeBySession: { - // Find the most recent (last) incomplete turn - [sessionId]: turns.filter((t) => !t.completed_at).pop()?.id ?? null, - }, - }, - worktrees: { - items: Object.fromEntries( - allSessions - .filter((s) => s.worktree_id) - .map((s) => [ - s.worktree_id, - { - id: s.worktree_id!, - sessionId: s.id, - repositoryId: s.repository_id ?? undefined, - path: s.worktree_path ?? undefined, - branch: s.worktree_branch ?? undefined, - }, - ]) - ), - }, - sessionWorktreesBySessionId: { - itemsBySessionId: Object.fromEntries( - allSessions - .filter((s) => s.worktree_id) - .map((s) => [s.id, [s.worktree_id!]]) - ), - }, - settingsAgents: { - items: agents.agents, - }, - settingsData: { - agentsLoaded: true, - executorsLoaded: false, - environmentsLoaded: false, - }, - availableAgents: { - items: availableAgents, - loaded: true, - loading: false, - }, - userSettings: { - workspaceId: userSettings?.workspace_id || null, - workflowId: userSettings?.workflow_filter_id || null, - kanbanViewMode: userSettings?.kanban_view_mode || null, - repositoryIds: Array.from(new Set(userSettings?.repository_ids ?? [])).sort(), - preferredShell: userSettings?.preferred_shell || null, - shellOptions: userSettingsResponse?.shell_options ?? [], - defaultEditorId: userSettings?.default_editor_id || null, - enablePreviewOnClick: userSettings?.enable_preview_on_click ?? false, - chatSubmitKey: userSettings?.chat_submit_key ?? 'cmd_enter', - reviewAutoMarkOnScroll: userSettings?.review_auto_mark_on_scroll ?? true, - lspAutoStartLanguages: userSettings?.lsp_auto_start_languages ?? [], - lspAutoInstallLanguages: userSettings?.lsp_auto_install_languages ?? [], - lspServerConfigs: userSettings?.lsp_server_configs ?? {}, - loaded: Boolean(userSettings), - }, - }; + fetchedData = await fetchSessionData(paramSessionId); } catch (error) { - console.warn( - 'Could not SSR session (client will load via WebSocket):', - error instanceof Error ? error.message : String(error) - ); - initialState = null; - task = null; - sessionId = null; + console.warn('Could not SSR session (client will load via WebSocket):', error instanceof Error ? error.message : String(error)); } + const { task, sessionId, initialState, initialTerminals } = fetchedData ?? { task: null, sessionId: null, initialState: null, initialTerminals: [] }; + return ( <> {initialState ? : null} diff --git a/apps/web/app/settings/agents/[agentId]/agent-save-helpers.ts b/apps/web/app/settings/agents/[agentId]/agent-save-helpers.ts new file mode 100644 index 0000000000..77bb9681a5 --- /dev/null +++ b/apps/web/app/settings/agents/[agentId]/agent-save-helpers.ts @@ -0,0 +1,245 @@ +import { + createAgentAction, + createAgentProfileAction, + deleteAgentProfileAction, + updateAgentAction, + updateAgentProfileAction, + updateAgentProfileMcpConfigAction, +} from '@/app/actions/agents'; +import type { Agent, AgentProfile, McpServerDef, PermissionSetting, ModelConfig } from '@/lib/types/http'; +import { permissionsToProfilePatch } from '@/lib/agent-permissions'; + +type DraftMcpConfig = { + enabled: boolean; + servers: string; + dirty: boolean; + error: string | null; +}; + +export type DraftProfile = Omit & { + allow_indexing?: boolean; + isNew?: boolean; + mcp_config?: DraftMcpConfig; +}; + +export type DraftAgent = Omit & { profiles: DraftProfile[]; isNew?: boolean }; + +export const parseProfileMcpServers = (raw: string): Record => { + if (!raw.trim()) return {}; + const parsed = JSON.parse(raw) as unknown; + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + throw new Error('MCP servers config must be a JSON object'); + } + if ('mcpServers' in parsed) { + const nested = (parsed as { mcpServers?: unknown }).mcpServers; + if (!nested || typeof nested !== 'object' || Array.isArray(nested)) { + throw new Error('mcpServers must be a JSON object'); + } + return nested as Record; + } + return parsed as Record; +}; + +type SaveMcpForProfileParams = { + draftProfile: DraftProfile; + targetProfileId: string; + onToastError: (error: unknown) => void; +}; + +async function saveMcpForProfile({ draftProfile, targetProfileId, onToastError }: SaveMcpForProfileParams) { + if (!draftProfile.mcp_config?.dirty || !draftProfile.mcp_config.servers.trim()) return; + try { + const servers = parseProfileMcpServers(draftProfile.mcp_config.servers); + await updateAgentProfileMcpConfigAction(targetProfileId, { + enabled: draftProfile.mcp_config.enabled, + mcpServers: servers, + }); + } catch (error) { + onToastError(error); + } +} + +async function saveMcpForCreatedProfiles( + draftAgent: DraftAgent, + created: Agent, + onToastError: (error: unknown) => void +) { + if (created.profiles.length === draftAgent.profiles.length) { + for (let index = 0; index < draftAgent.profiles.length; index += 1) { + await saveMcpForProfile({ + draftProfile: draftAgent.profiles[index], + targetProfileId: created.profiles[index].id, + onToastError, + }); + } + return; + } + for (const draftProfile of draftAgent.profiles) { + const createdProfile = created.profiles.find((profile) => profile.name === draftProfile.name); + if (!createdProfile) continue; + await saveMcpForProfile({ + draftProfile, + targetProfileId: createdProfile.id, + onToastError, + }); + } +} + +export type EnsureProfilesFn = ( + agent: DraftAgent, + displayName: string, + defaultModel: string, + permissions?: Record +) => DraftAgent; + +export type CloneAgentFn = (agent: Agent) => DraftAgent; + +export type SaveAgentCallbacks = { + onToastError: (error: unknown) => void; + currentAgentModelConfig: ModelConfig; + permissionSettings: Record; + resolveDisplayName: (name: string) => string; + upsertAgent: (agent: Agent) => void; + setDraftAgent: (agent: DraftAgent) => void; + ensureProfiles: EnsureProfilesFn; + cloneAgent: CloneAgentFn; + replaceRoute: (path: string) => void; +}; + +export async function saveNewAgent( + draftAgent: DraftAgent, + callbacks: SaveAgentCallbacks +) { + let created = await createAgentAction({ + name: draftAgent.name, + workspace_id: draftAgent.workspace_id, + profiles: draftAgent.profiles.map((profile) => ({ + name: profile.name, + model: profile.model, + ...permissionsToProfilePatch(profile), + cli_passthrough: profile.cli_passthrough ?? false, + })), + }); + + await saveMcpForCreatedProfiles(draftAgent, created, callbacks.onToastError); + + if ((draftAgent.mcp_config_path ?? '') !== (created.mcp_config_path ?? '')) { + created = await updateAgentAction(created.id, { + mcp_config_path: draftAgent.mcp_config_path ?? '', + }); + } + callbacks.upsertAgent(created); + callbacks.setDraftAgent( + callbacks.ensureProfiles( + callbacks.cloneAgent(created), + callbacks.resolveDisplayName(created.name), + callbacks.currentAgentModelConfig.default_model, + callbacks.permissionSettings + ) + ); + callbacks.replaceRoute(`/settings/agents/${encodeURIComponent(created.name)}`); +} + +async function saveExistingAgentPatch(draftAgent: DraftAgent, savedAgent: Agent) { + const agentPatch: { workspace_id?: string | null; mcp_config_path?: string | null } = {}; + if ((draftAgent.workspace_id ?? null) !== (savedAgent.workspace_id ?? null)) { + agentPatch.workspace_id = draftAgent.workspace_id ?? null; + } + if ((draftAgent.mcp_config_path ?? '') !== (savedAgent.mcp_config_path ?? '')) { + agentPatch.mcp_config_path = draftAgent.mcp_config_path ?? ''; + } + if (Object.keys(agentPatch).length > 0) { + await updateAgentAction(savedAgent.id, agentPatch); + } +} + +async function saveExistingProfiles( + draftAgent: DraftAgent, + savedAgent: Agent, + isCreateMode: boolean, + onToastError: (error: unknown) => void +): Promise { + const savedProfilesById = new Map(savedAgent.profiles.map((p) => [p.id, p])); + const nextProfiles: AgentProfile[] = isCreateMode ? [...savedAgent.profiles] : []; + + for (const profile of draftAgent.profiles) { + const savedProfile = savedProfilesById.get(profile.id); + if (!savedProfile) { + const createdProfile = await createAgentProfileAction(savedAgent.id, { + name: profile.name, + model: profile.model, + ...permissionsToProfilePatch(profile), + cli_passthrough: profile.cli_passthrough ?? false, + }); + await saveMcpForProfile({ draftProfile: profile, targetProfileId: createdProfile.id, onToastError }); + nextProfiles.push(createdProfile); + continue; + } + if (isProfileDirty(profile, savedProfile)) { + const updatedProfile = await updateAgentProfileAction(profile.id, { + name: profile.name, + model: profile.model, + ...permissionsToProfilePatch(profile), + }); + nextProfiles.push(updatedProfile); + continue; + } + nextProfiles.push(savedProfile); + } + return nextProfiles; +} + +async function deleteRemovedProfiles(draftAgent: DraftAgent, savedAgent: Agent) { + for (const savedProfile of savedAgent.profiles) { + const stillExists = draftAgent.profiles.some((p) => p.id === savedProfile.id); + if (!stillExists) { + await deleteAgentProfileAction(savedProfile.id); + } + } +} + +export async function saveExistingAgent( + draftAgent: DraftAgent, + savedAgent: Agent, + isCreateMode: boolean, + callbacks: SaveAgentCallbacks +) { + await saveExistingAgentPatch(draftAgent, savedAgent); + + const nextProfiles = await saveExistingProfiles(draftAgent, savedAgent, isCreateMode, callbacks.onToastError); + + if (!isCreateMode) { + await deleteRemovedProfiles(draftAgent, savedAgent); + } + + const nextAgent = { + ...savedAgent, + workspace_id: draftAgent.workspace_id ?? null, + mcp_config_path: draftAgent.mcp_config_path ?? '', + profiles: nextProfiles, + }; + callbacks.upsertAgent(nextAgent); + callbacks.setDraftAgent( + callbacks.ensureProfiles( + callbacks.cloneAgent(nextAgent), + callbacks.resolveDisplayName(nextAgent.name), + callbacks.currentAgentModelConfig.default_model, + callbacks.permissionSettings + ) + ); + if (isCreateMode) { + callbacks.replaceRoute(`/settings/agents/${encodeURIComponent(savedAgent.name)}`); + } +} + +import { arePermissionsDirty } from '@/lib/agent-permissions'; + +export function isProfileDirty(draft: DraftProfile, saved?: AgentProfile): boolean { + if (!saved) return true; + return ( + draft.name !== saved.name || + draft.model !== saved.model || + arePermissionsDirty(draft, saved) || + draft.cli_passthrough !== saved.cli_passthrough + ); +} diff --git a/apps/web/app/settings/agents/[agentId]/page.tsx b/apps/web/app/settings/agents/[agentId]/page.tsx index 7606c7a448..00d38813f2 100644 --- a/apps/web/app/settings/agents/[agentId]/page.tsx +++ b/apps/web/app/settings/agents/[agentId]/page.tsx @@ -10,32 +10,14 @@ import { Separator } from '@kandev/ui/separator'; import { useToast } from '@/components/toast-provider'; import { UnsavedChangesBadge, UnsavedSaveButton } from '@/components/settings/unsaved-indicator'; import { ProfileFormFields } from '@/components/settings/profile-form-fields'; -import { - createAgentAction, - createAgentProfileAction, - deleteAgentProfileAction, - updateAgentAction, - updateAgentProfileAction, - updateAgentProfileMcpConfigAction, -} from '@/app/actions/agents'; -import type { Agent, AgentDiscovery, AgentProfile, McpServerDef, AvailableAgent, PermissionSetting } from '@/lib/types/http'; -import { buildDefaultPermissions, arePermissionsDirty, permissionsToProfilePatch } from '@/lib/agent-permissions'; +import type { Agent, AgentDiscovery, AvailableAgent, PermissionSetting, ModelConfig, PassthroughConfig } from '@/lib/types/http'; +import { buildDefaultPermissions } from '@/lib/agent-permissions'; import { generateUUID } from '@/lib/utils'; import { useAppStore } from '@/components/state-provider'; import { useAvailableAgents } from '@/hooks/domains/settings/use-available-agents'; import { ProfileMcpConfigCard } from './profile-mcp-config-card'; - -type DraftProfile = Omit & { - allow_indexing?: boolean; - isNew?: boolean; - mcp_config?: { - enabled: boolean; - servers: string; - dirty: boolean; - error: string | null; - }; -}; -type DraftAgent = Omit & { profiles: DraftProfile[]; isNew?: boolean }; +import { saveNewAgent, saveExistingAgent, isProfileDirty } from './agent-save-helpers'; +import type { DraftProfile, DraftAgent } from './agent-save-helpers'; const defaultMcpConfig: NonNullable = { enabled: false, @@ -84,37 +66,69 @@ const ensureProfiles = ( defaultModel: string, permissionSettings?: Record ): DraftAgent => { - if (agent.profiles.length > 0) { - return agent; - } + if (agent.profiles.length > 0) return agent; return { ...agent, profiles: [createDraftProfile(agent.id, agentDisplayName, defaultModel, permissionSettings)], }; }; -function AgentSetupForm({ - initialAgent, - savedAgent, - discoveryAgent, - onToastError, - isCreateMode = false, -}: AgentSetupFormProps) { - const router = useRouter(); - const settingsAgents = useAppStore((state) => state.settingsAgents.items); - const setSettingsAgents = useAppStore((state) => state.setSettingsAgents); - const setAgentProfiles = useAppStore((state) => state.setAgentProfiles); - const availableAgents = useAvailableAgents().items; +type ProfileCardItemProps = { + profile: DraftProfile; + isNew: boolean; + draftAgent: DraftAgent; + currentAgentModelConfig: ModelConfig; + permissionSettings: Record; + passthroughConfig: PassthroughConfig | null; + onProfileChange: (profileId: string, patch: Partial) => void; + onProfileMcpChange: (profileId: string, patch: Partial>) => void; + onRemoveProfile: (profileId: string) => void; + onToastError: (error: unknown) => void; +}; + +function ProfileCardItem({ + profile, isNew, draftAgent, currentAgentModelConfig, permissionSettings, + passthroughConfig, onProfileChange, onProfileMcpChange, onRemoveProfile, onToastError, +}: ProfileCardItemProps) { + return ( + + + onProfileChange(profile.id, patch)} + modelConfig={currentAgentModelConfig} + permissionSettings={permissionSettings} + passthroughConfig={passthroughConfig} + agentName={draftAgent.name} + onRemove={() => onRemoveProfile(profile.id)} + canRemove={draftAgent.profiles.length > 1} + /> + onProfileMcpChange(profile.id, patch)} + onToastError={onToastError} + /> + + + ); +} + +function useAgentFormState(initialAgent: DraftAgent, savedAgent: Agent | null, availableAgents: AvailableAgent[]) { const [draftAgent, setDraftAgent] = useState(initialAgent); const [saveStatus, setSaveStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); - const [newProfileId, setNewProfileId] = useState(null); + const resolveDisplayName = (name: string) => availableAgents.find((item: AvailableAgent) => item.name === name)?.display_name ?? ''; - const hasInvalidMcpConfig = useMemo(() => { - return draftAgent.profiles.some((profile) => Boolean(profile.mcp_config?.error)); - }, [draftAgent.profiles]); - // Get model config and permission settings for the current agent const currentAvailableAgent = useMemo(() => { return availableAgents.find((item: AvailableAgent) => item.name === draftAgent.name) ?? null; }, [availableAgents, draftAgent.name]); @@ -123,48 +137,68 @@ function AgentSetupForm({ return currentAvailableAgent?.model_config ?? { default_model: '', available_models: [], supports_dynamic_models: false }; }, [currentAvailableAgent]); - const permissionSettings = useMemo(() => { - return currentAvailableAgent?.permission_settings ?? {}; - }, [currentAvailableAgent]); + const permissionSettings = useMemo(() => currentAvailableAgent?.permission_settings ?? {}, [currentAvailableAgent]); + const passthroughConfig = useMemo(() => currentAvailableAgent?.passthrough_config ?? null, [currentAvailableAgent]); - const passthroughConfig = useMemo(() => { - return currentAvailableAgent?.passthrough_config ?? null; - }, [currentAvailableAgent]); - - const isProfileDirty = (draft: DraftProfile, saved?: AgentProfile) => { - if (!saved) return true; - return ( - draft.name !== saved.name || - draft.model !== saved.model || - arePermissionsDirty(draft, saved) || - draft.cli_passthrough !== saved.cli_passthrough - ); - }; + const hasInvalidMcpConfig = useMemo(() => { + return draftAgent.profiles.some((profile) => Boolean(profile.mcp_config?.error)); + }, [draftAgent.profiles]); const isAgentDirty = useMemo(() => { - if (!draftAgent) return false; - if (!savedAgent) return true; + if (!draftAgent || !savedAgent) return !savedAgent; if ((draftAgent.workspace_id ?? null) !== (savedAgent.workspace_id ?? null)) return true; if ((draftAgent.mcp_config_path ?? '') !== (savedAgent.mcp_config_path ?? '')) return true; if (draftAgent.profiles.length !== savedAgent.profiles.length) return true; - const savedProfiles = new Map(savedAgent.profiles.map((profile) => [profile.id, profile])); + const savedProfiles = new Map(savedAgent.profiles.map((p) => [p.id, p])); for (const profile of draftAgent.profiles) { - const savedProfile = savedProfiles.get(profile.id); - if (!savedProfile || isProfileDirty(profile, savedProfile)) return true; + if (!savedProfiles.has(profile.id) || isProfileDirty(profile, savedProfiles.get(profile.id))) return true; } return false; }, [draftAgent, savedAgent]); + return { + draftAgent, setDraftAgent, saveStatus, setSaveStatus, + resolveDisplayName, currentAgentModelConfig, permissionSettings, passthroughConfig, + hasInvalidMcpConfig, isAgentDirty, + }; +} + +type AgentHeaderProps = { + displayName: string; + matchedPath: string | null | undefined; + isCreateMode: boolean; +}; + +function AgentHeader({ displayName, matchedPath, isCreateMode }: AgentHeaderProps) { + return ( +
+
+
+

{displayName}

+ + {matchedPath ?? 'Installation not detected'} + +
+

+ {isCreateMode ? 'Create a new profile for this agent.' : 'Configure profiles and defaults for this agent.'} +

+
+
+ ); +} + +function useAgentStoreSync() { + const settingsAgents = useAppStore((state) => state.settingsAgents.items); + const setSettingsAgents = useAppStore((state) => state.setSettingsAgents); + const setAgentProfiles = useAppStore((state) => state.setAgentProfiles); + const syncAgentsToStore = (nextAgents: Agent[]) => { setSettingsAgents(nextAgents); setAgentProfiles( nextAgents.flatMap((agent) => agent.profiles.map((profile) => ({ - id: profile.id, - label: `${profile.agent_display_name} • ${profile.name}`, - agent_id: agent.id, - agent_name: agent.name, - cli_passthrough: profile.cli_passthrough, + id: profile.id, label: `${profile.agent_display_name} • ${profile.name}`, + agent_id: agent.id, agent_name: agent.name, cli_passthrough: profile.cli_passthrough, })) ) ); @@ -172,79 +206,48 @@ function AgentSetupForm({ const upsertAgent = (agent: Agent) => { const exists = settingsAgents.some((item: Agent) => item.id === agent.id); - const nextAgents = exists - ? settingsAgents.map((item: Agent) => (item.id === agent.id ? agent : item)) - : [...settingsAgents, agent]; - syncAgentsToStore(nextAgents); + syncAgentsToStore(exists ? settingsAgents.map((item: Agent) => (item.id === agent.id ? agent : item)) : [...settingsAgents, agent]); }; + return { upsertAgent }; +} + +function useProfileHandlers( + setDraftAgent: (fn: (current: DraftAgent) => DraftAgent) => void, + resolveDisplayName: (name: string) => string, + defaultModel: string, + permissionSettings: Record, +) { + const [newProfileId, setNewProfileId] = useState(null); + const handleAddProfile = () => { const draftId = `draft-${generateUUID()}`; setDraftAgent((current) => ({ ...current, - profiles: [ - ...current.profiles, - { ...createDraftProfile(current.id, resolveDisplayName(current.name), currentAgentModelConfig.default_model, permissionSettings), id: draftId }, - ], + profiles: [...current.profiles, { ...createDraftProfile(current.id, resolveDisplayName(current.name), defaultModel, permissionSettings), id: draftId }], })); setNewProfileId(draftId); }; const handleRemoveProfile = (profileId: string) => { setDraftAgent((current) => { - const remaining = current.profiles.filter((profile) => profile.id !== profileId); - return { - ...current, - profiles: - remaining.length > 0 - ? remaining - : [createDraftProfile(current.id, resolveDisplayName(current.name), currentAgentModelConfig.default_model, permissionSettings)], - }; + const remaining = current.profiles.filter((p) => p.id !== profileId); + return { ...current, profiles: remaining.length > 0 ? remaining : [createDraftProfile(current.id, resolveDisplayName(current.name), defaultModel, permissionSettings)] }; }); - if (newProfileId === profileId) { - setNewProfileId(null); - } + if (newProfileId === profileId) setNewProfileId(null); }; const handleProfileChange = (profileId: string, patch: Partial) => { - setDraftAgent((current) => ({ - ...current, - profiles: current.profiles.map((profile) => - profile.id === profileId ? { ...profile, ...patch } : profile - ), - })); + setDraftAgent((current) => ({ ...current, profiles: current.profiles.map((p) => p.id === profileId ? { ...p, ...patch } : p) })); }; - const handleProfileMcpChange = ( - profileId: string, - patch: Partial> - ) => { + const handleProfileMcpChange = (profileId: string, patch: Partial>) => { setDraftAgent((current) => ({ ...current, - profiles: current.profiles.map((profile) => - profile.id === profileId - ? { ...profile, mcp_config: { ...(profile.mcp_config ?? defaultMcpConfig), ...patch } } - : profile - ), + profiles: current.profiles.map((p) => p.id === profileId ? { ...p, mcp_config: { ...(p.mcp_config ?? defaultMcpConfig), ...patch } } : p), })); }; - const parseProfileMcpServers = (raw: string) => { - if (!raw.trim()) return {}; - const parsed = JSON.parse(raw) as unknown; - if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { - throw new Error('MCP servers config must be a JSON object'); - } - if ('mcpServers' in parsed) { - const nested = (parsed as { mcpServers?: unknown }).mcpServers; - if (!nested || typeof nested !== 'object' || Array.isArray(nested)) { - throw new Error('mcpServers must be a JSON object'); - } - return nested as Record; - } - return parsed as Record; - }; - useEffect(() => { if (!newProfileId) return; const target = document.getElementById(`profile-card-${newProfileId}`); @@ -254,191 +257,46 @@ function AgentSetupForm({ return () => clearTimeout(timeout); }, [newProfileId]); + return { newProfileId, handleAddProfile, handleRemoveProfile, handleProfileChange, handleProfileMcpChange }; +} + +function AgentSetupForm({ initialAgent, savedAgent, discoveryAgent, onToastError, isCreateMode = false }: AgentSetupFormProps) { + const router = useRouter(); + const availableAgents = useAvailableAgents().items; + const { upsertAgent } = useAgentStoreSync(); + + const { + draftAgent, setDraftAgent, saveStatus, setSaveStatus, + resolveDisplayName, currentAgentModelConfig, permissionSettings, passthroughConfig, + hasInvalidMcpConfig, isAgentDirty, + } = useAgentFormState(initialAgent, savedAgent, availableAgents); + + const { newProfileId, handleAddProfile, handleRemoveProfile, handleProfileChange, handleProfileMcpChange } = + useProfileHandlers(setDraftAgent, resolveDisplayName, currentAgentModelConfig.default_model, permissionSettings); + const handleSave = async () => { - if (draftAgent.profiles.some((profile) => !profile.name.trim())) { - onToastError(new Error('Profile name is required.')); - return; - } - if (draftAgent.profiles.some((profile) => !profile.model.trim())) { - onToastError(new Error('Model is required for all profiles.')); - return; - } - if (hasInvalidMcpConfig) { - onToastError(new Error('Fix invalid MCP JSON before saving.')); - return; - } + if (draftAgent.profiles.some((p) => !p.name.trim())) { onToastError(new Error('Profile name is required.')); return; } + if (draftAgent.profiles.some((p) => !p.model.trim())) { onToastError(new Error('Model is required for all profiles.')); return; } + if (hasInvalidMcpConfig) { onToastError(new Error('Fix invalid MCP JSON before saving.')); return; } setSaveStatus('loading'); + const callbacks = { onToastError, currentAgentModelConfig, permissionSettings, resolveDisplayName, upsertAgent, setDraftAgent, ensureProfiles, cloneAgent, replaceRoute: (path: string) => router.replace(path) }; try { - if (!savedAgent) { - let created = await createAgentAction({ - name: draftAgent.name, - workspace_id: draftAgent.workspace_id, - profiles: draftAgent.profiles.map((profile) => ({ - name: profile.name, - model: profile.model, - ...permissionsToProfilePatch(profile), - cli_passthrough: profile.cli_passthrough ?? false, - })), - }); - if (created.profiles.length === draftAgent.profiles.length) { - for (let index = 0; index < draftAgent.profiles.length; index += 1) { - const draftProfile = draftAgent.profiles[index]; - const createdProfile = created.profiles[index]; - if ( - draftProfile.mcp_config && - draftProfile.mcp_config.dirty && - draftProfile.mcp_config.servers.trim() - ) { - try { - const servers = parseProfileMcpServers(draftProfile.mcp_config.servers); - await updateAgentProfileMcpConfigAction(createdProfile.id, { - enabled: draftProfile.mcp_config.enabled, - mcpServers: servers, - }); - } catch (error) { - onToastError(error); - } - } - } - } else { - for (const draftProfile of draftAgent.profiles) { - if ( - !draftProfile.mcp_config || - !draftProfile.mcp_config.dirty || - !draftProfile.mcp_config.servers.trim() - ) { - continue; - } - const createdProfile = created.profiles.find( - (profile) => profile.name === draftProfile.name - ); - if (!createdProfile) continue; - try { - const servers = parseProfileMcpServers(draftProfile.mcp_config.servers); - await updateAgentProfileMcpConfigAction(createdProfile.id, { - enabled: draftProfile.mcp_config.enabled, - mcpServers: servers, - }); - } catch (error) { - onToastError(error); - } - } - } - if ((draftAgent.mcp_config_path ?? '') !== (created.mcp_config_path ?? '')) { - created = await updateAgentAction(created.id, { - mcp_config_path: draftAgent.mcp_config_path ?? '', - }); - } - upsertAgent(created); - setDraftAgent(ensureProfiles(cloneAgent(created), resolveDisplayName(created.name), currentAgentModelConfig.default_model, permissionSettings)); - router.replace(`/settings/agents/${encodeURIComponent(created.name)}`); - } else { - const agentPatch: { workspace_id?: string | null; mcp_config_path?: string | null } = {}; - if ((draftAgent.workspace_id ?? null) !== (savedAgent.workspace_id ?? null)) { - agentPatch.workspace_id = draftAgent.workspace_id ?? null; - } - if ((draftAgent.mcp_config_path ?? '') !== (savedAgent.mcp_config_path ?? '')) { - agentPatch.mcp_config_path = draftAgent.mcp_config_path ?? ''; - } - if (Object.keys(agentPatch).length > 0) { - await updateAgentAction(savedAgent.id, agentPatch); - } - - const savedProfilesById = new Map(savedAgent.profiles.map((profile) => [profile.id, profile])); - // In create mode, preserve existing profiles; otherwise start fresh - const nextProfiles: AgentProfile[] = isCreateMode ? [...savedAgent.profiles] : []; - for (const profile of draftAgent.profiles) { - const savedProfile = savedProfilesById.get(profile.id); - if (!savedProfile) { - const createdProfile = await createAgentProfileAction(savedAgent.id, { - name: profile.name, - model: profile.model, - ...permissionsToProfilePatch(profile), - cli_passthrough: profile.cli_passthrough ?? false, - }); - if (profile.mcp_config && profile.mcp_config.dirty && profile.mcp_config.servers.trim()) { - try { - const servers = parseProfileMcpServers(profile.mcp_config.servers); - await updateAgentProfileMcpConfigAction(createdProfile.id, { - enabled: profile.mcp_config.enabled, - mcpServers: servers, - }); - } catch (error) { - onToastError(error); - } - } - nextProfiles.push(createdProfile); - continue; - } - if (isProfileDirty(profile, savedProfile)) { - const updatedProfile = await updateAgentProfileAction(profile.id, { - name: profile.name, - model: profile.model, - ...permissionsToProfilePatch(profile), - }); - nextProfiles.push(updatedProfile); - continue; - } - nextProfiles.push(savedProfile); - } - // In create mode, don't delete existing profiles - we're only adding new ones - if (!isCreateMode) { - for (const savedProfile of savedAgent.profiles) { - const stillExists = draftAgent.profiles.some((profile) => profile.id === savedProfile.id); - if (!stillExists) { - await deleteAgentProfileAction(savedProfile.id); - } - } - } - - const nextAgent = { - ...savedAgent, - workspace_id: draftAgent.workspace_id ?? null, - mcp_config_path: draftAgent.mcp_config_path ?? '', - profiles: nextProfiles, - }; - upsertAgent(nextAgent); - setDraftAgent(ensureProfiles(cloneAgent(nextAgent), resolveDisplayName(nextAgent.name), currentAgentModelConfig.default_model, permissionSettings)); - // In create mode, redirect to manage mode after successful save - if (isCreateMode) { - router.replace(`/settings/agents/${encodeURIComponent(savedAgent.name)}`); - } - } + if (!savedAgent) { await saveNewAgent(draftAgent, callbacks); } + else { await saveExistingAgent(draftAgent, savedAgent, isCreateMode, callbacks); } setSaveStatus('success'); - } catch (error) { - setSaveStatus('error'); - onToastError(error); - } + } catch (error) { setSaveStatus('error'); onToastError(error); } }; + const displayName = draftAgent.profiles[0]?.agent_display_name ?? draftAgent.name; + return (
-
-
-
-

- {draftAgent.profiles[0]?.agent_display_name ?? draftAgent.name} -

- - {discoveryAgent?.matched_path ?? 'Installation not detected'} - -
-

- {isCreateMode ? 'Create a new profile for this agent.' : 'Configure profiles and defaults for this agent.'} -

-
-
- + -
- - {isCreateMode - ? `Create ${draftAgent.profiles[0]?.agent_display_name ?? draftAgent.name} Profile` - : `${draftAgent.profiles[0]?.agent_display_name ?? draftAgent.name} Profiles`} - + {isCreateMode ? `Create ${displayName} Profile` : `${displayName} Profiles`} {isAgentDirty && }
- {draftAgent.profiles.map((profile) => { - const isNew = profile.id === newProfileId; - return ( - - - handleProfileChange(profile.id, patch)} - modelConfig={currentAgentModelConfig} - permissionSettings={permissionSettings} - passthroughConfig={passthroughConfig} - agentName={draftAgent.name} - onRemove={() => handleRemoveProfile(profile.id)} - canRemove={draftAgent.profiles.length > 1} - /> - - handleProfileMcpChange(profile.id, patch)} - onToastError={onToastError} - /> - - - ); - })} + {draftAgent.profiles.map((profile) => ( + + ))}
- +
-
); } @@ -512,51 +334,24 @@ export default function AgentSetupPage() { const savedAgents = useAppStore((state) => state.settingsAgents.items); const availableAgents = useAvailableAgents().items; - const discoveryAgent = useMemo(() => { - return discoveryAgents.find((agent: AgentDiscovery) => agent.name === decodedKey); - }, [decodedKey, discoveryAgents]); - - const savedAgent = useMemo(() => { - return savedAgents.find((agent: Agent) => agent.id === decodedKey || agent.name === decodedKey) ?? null; - }, [decodedKey, savedAgents]); + const discoveryAgent = useMemo(() => discoveryAgents.find((a: AgentDiscovery) => a.name === decodedKey), [decodedKey, discoveryAgents]); + const savedAgent = useMemo(() => savedAgents.find((a: Agent) => a.id === decodedKey || a.name === decodedKey) ?? null, [decodedKey, savedAgents]); const initialAgent = useMemo(() => { if (!decodedKey) return null; - const resolveAvailableAgent = (name: string) => - availableAgents.find((item: AvailableAgent) => item.name === name); - const resolveDisplayName = (name: string) => - resolveAvailableAgent(name)?.display_name ?? ''; - const resolveDefaultModel = (name: string) => - resolveAvailableAgent(name)?.model_config?.default_model ?? ''; - const resolvePermissionSettings = (name: string) => - resolveAvailableAgent(name)?.permission_settings; + const resolve = (name: string) => availableAgents.find((item: AvailableAgent) => item.name === name); + const dn = (name: string) => resolve(name)?.display_name ?? ''; + const dm = (name: string) => resolve(name)?.model_config?.default_model ?? ''; + const ps = (name: string) => resolve(name)?.permission_settings; if (savedAgent) { - // In create mode, start with a blank profile instead of loading existing profiles if (isCreateMode) { - const draft: DraftAgent = { - ...savedAgent, - workspace_id: savedAgent.workspace_id ?? null, - mcp_config_path: savedAgent.mcp_config_path ?? '', - profiles: [], - isNew: false, - }; - return ensureProfiles(draft, resolveDisplayName(savedAgent.name), resolveDefaultModel(savedAgent.name), resolvePermissionSettings(savedAgent.name)); + return ensureProfiles({ ...savedAgent, workspace_id: savedAgent.workspace_id ?? null, mcp_config_path: savedAgent.mcp_config_path ?? '', profiles: [], isNew: false }, dn(savedAgent.name), dm(savedAgent.name), ps(savedAgent.name)); } - return ensureProfiles(cloneAgent(savedAgent), resolveDisplayName(savedAgent.name), resolveDefaultModel(savedAgent.name), resolvePermissionSettings(savedAgent.name)); + return ensureProfiles(cloneAgent(savedAgent), dn(savedAgent.name), dm(savedAgent.name), ps(savedAgent.name)); } if (discoveryAgent) { - const draft: DraftAgent = { - id: `draft-${generateUUID()}`, - name: discoveryAgent.name, - workspace_id: null, - supports_mcp: discoveryAgent.supports_mcp, - mcp_config_path: discoveryAgent.mcp_config_path ?? '', - profiles: [], - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - isNew: true, - }; - return ensureProfiles(draft, resolveDisplayName(draft.name), resolveDefaultModel(draft.name), resolvePermissionSettings(draft.name)); + const draft: DraftAgent = { id: `draft-${generateUUID()}`, name: discoveryAgent.name, workspace_id: null, supports_mcp: discoveryAgent.supports_mcp, mcp_config_path: discoveryAgent.mcp_config_path ?? '', profiles: [], created_at: new Date().toISOString(), updated_at: new Date().toISOString(), isNew: true }; + return ensureProfiles(draft, dn(draft.name), dm(draft.name), ps(draft.name)); } return null; }, [decodedKey, discoveryAgent, savedAgent, availableAgents, isCreateMode]); @@ -566,34 +361,23 @@ export default function AgentSetupPage() {

Agent not found.

- +
); } - if (!initialAgent) { - return null; - } + if (!initialAgent) return null; const handleToastError = (error: unknown) => { - toast({ - title: 'Failed to save agent', - description: error instanceof Error ? error.message : 'Request failed', - variant: 'error', - }); + toast({ title: 'Failed to save agent', description: error instanceof Error ? error.message : 'Request failed', variant: 'error' }); }; return ( ); } diff --git a/apps/web/app/settings/agents/[agentId]/profile-mcp-config-card.tsx b/apps/web/app/settings/agents/[agentId]/profile-mcp-config-card.tsx index 013bb863a9..16ddc6c381 100644 --- a/apps/web/app/settings/agents/[agentId]/profile-mcp-config-card.tsx +++ b/apps/web/app/settings/agents/[agentId]/profile-mcp-config-card.tsx @@ -28,6 +28,259 @@ type ProfileMcpConfigCardProps = { onToastError: (error: unknown) => void; }; +const POPULAR_SERVERS: Record> = { + playwright: { + type: 'stdio', + command: 'npx', + args: ['-y', '@modelcontextprotocol/server-playwright'], + }, + 'chrome-devtools': { + type: 'stdio', + command: 'npx', + args: ['-y', '@modelcontextprotocol/server-chrome-devtools'], + }, + context7: { + type: 'stdio', + command: 'npx', + args: ['-y', '@context7/mcp'], + env: { + CONTEXT7_API_KEY: 'your_api_key_here', + }, + }, + github: { + type: 'stdio', + command: 'npx', + args: ['-y', '@modelcontextprotocol/server-github'], + env: { + GITHUB_TOKEN: 'your_token_here', + }, + }, +}; + +const KANDEV_TOOLS_DESCRIPTION = 'Tools: list_workspaces, list_boards, list_workflow_steps, list_tasks, create_task, update_task'; + +type PopularServerButtonProps = { + label: string; + displayName: string; + onApply: (label: string) => void; +}; + +function PopularServerButton({ label, displayName, onApply }: PopularServerButtonProps) { + return ( + + ); +} + +function applyPopularServerToJson( + currentServers: string, + label: string, + isDraft: boolean, + onDraftStateChange?: (next: { servers?: string; dirty?: boolean; error?: string | null }) => void, + handleMcpServersChange?: (value: string) => void, +) { + const base = currentServers.trim() || '{\n "mcpServers": {}\n}'; + let parsed: Record = {}; + try { + parsed = JSON.parse(base) as Record; + } catch { + return; + } + const root = + parsed && typeof parsed === 'object' && !Array.isArray(parsed) + ? parsed + : { mcpServers: {} }; + const servers = (root.mcpServers && + typeof root.mcpServers === 'object' && + !Array.isArray(root.mcpServers) + ? root.mcpServers + : {}) as Record; + + if (servers[label]) return; + servers[label] = POPULAR_SERVERS[label] ?? { type: 'stdio', command: 'npx', args: ['-y'] }; + root.mcpServers = servers; + const nextValue = JSON.stringify(root, null, 2); + + if (isDraft) { + onDraftStateChange?.({ servers: nextValue, dirty: true, error: null }); + return; + } + handleMcpServersChange?.(nextValue); +} + +function validateDraftServers(value: string): string | null { + if (!value.trim()) return null; + try { + const parsed = JSON.parse(value); + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + return 'MCP servers config must be a JSON object'; + } + if ('mcpServers' in parsed) { + const nested = (parsed as { mcpServers?: unknown }).mcpServers; + if (!nested || typeof nested !== 'object' || Array.isArray(nested)) { + return 'mcpServers must be a JSON object'; + } + } + } catch { + return 'Invalid JSON'; + } + return null; +} + +type McpServersEditorProps = { + profileId: string; + currentServers: string; + currentError: string | null; + isDraft: boolean; + isEditableProfile: boolean; + onDraftStateChange?: (next: { servers?: string; dirty?: boolean; error?: string | null }) => void; + handleMcpServersChange: (value: string) => void; +}; + +function McpServersEditor({ + profileId, + currentServers, + currentError, + isDraft, + isEditableProfile, + onDraftStateChange, + handleMcpServersChange, +}: McpServersEditorProps) { + const handleApplyServer = (label: string) => { + applyPopularServerToJson(currentServers, label, isDraft, onDraftStateChange, handleMcpServersChange); + }; + + const handleDraftChange = (value: string) => { + if (!onDraftStateChange) return; + const error = validateDraftServers(value); + onDraftStateChange({ servers: value, dirty: true, error }); + }; + + return ( +
+ +