@assistkick/create 1.9.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/scaffolder.d.ts +12 -1
- package/dist/src/scaffolder.js +40 -3
- package/dist/src/scaffolder.js.map +1 -1
- package/package.json +1 -1
- package/templates/assistkick-product-system/package.json +1 -1
- package/templates/assistkick-product-system/packages/backend/package.json +1 -0
- package/templates/assistkick-product-system/packages/backend/src/mcp/permission_mcp_server.ts +196 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +31 -7
- package/templates/assistkick-product-system/packages/backend/src/routes/auth.ts +15 -12
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_files.test.ts +95 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_files.ts +97 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_permission.ts +94 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_sessions.ts +189 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_upload.test.ts +131 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_upload.ts +94 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +12 -3
- package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +2 -2
- package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +391 -23
- package/templates/assistkick-product-system/packages/backend/src/routes/git_branches.test.ts +306 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/git_connect.test.ts +133 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +66 -9
- package/templates/assistkick-product-system/packages/backend/src/routes/preview.ts +204 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/projects.test.ts +205 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +37 -9
- package/templates/assistkick-product-system/packages/backend/src/routes/skills.test.ts +139 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/skills.ts +95 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +5 -4
- package/templates/assistkick-product-system/packages/backend/src/routes/users.ts +4 -4
- package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +8 -8
- package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +5 -5
- package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +6 -6
- package/templates/assistkick-product-system/packages/backend/src/server.ts +107 -27
- package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +105 -203
- package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +76 -266
- package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.test.ts +427 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.ts +345 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_message_repository.test.ts +170 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_message_repository.ts +106 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_session_service.test.ts +217 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_session_service.ts +188 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.test.ts +1243 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.ts +894 -0
- package/templates/assistkick-product-system/packages/backend/src/services/coherence-review.ts +3 -3
- package/templates/assistkick-product-system/packages/backend/src/services/dev_command_detector.test.ts +85 -0
- package/templates/assistkick-product-system/packages/backend/src/services/dev_command_detector.ts +54 -0
- package/templates/assistkick-product-system/packages/backend/src/services/email_service.ts +13 -10
- package/templates/assistkick-product-system/packages/backend/src/services/init.ts +11 -3
- package/templates/assistkick-product-system/packages/backend/src/services/invitation_service.ts +1 -1
- package/templates/assistkick-product-system/packages/backend/src/services/password_reset_service.ts +1 -1
- package/templates/assistkick-product-system/packages/backend/src/services/permission_service.test.ts +243 -0
- package/templates/assistkick-product-system/packages/backend/src/services/permission_service.ts +259 -0
- package/templates/assistkick-product-system/packages/backend/src/services/preview_server_manager.test.ts +172 -0
- package/templates/assistkick-product-system/packages/backend/src/services/preview_server_manager.ts +225 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +29 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +17 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +255 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +300 -25
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +44 -0
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +62 -7
- package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +77 -6
- package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +149 -14
- package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +2 -1
- package/templates/assistkick-product-system/packages/backend/src/services/title_generator_service.test.ts +45 -0
- package/templates/assistkick-product-system/packages/backend/src/services/title_generator_service.ts +157 -0
- package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +4 -3
- package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +3 -3
- package/templates/assistkick-product-system/packages/frontend/package.json +5 -0
- package/templates/assistkick-product-system/packages/frontend/src/App.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +336 -5
- package/templates/assistkick-product-system/packages/frontend/src/components/AgentsView.tsx +192 -12
- package/templates/assistkick-product-system/packages/frontend/src/components/AttachmentPreviewList.tsx +98 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/AutocompleteDropdown.tsx +65 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatAttachButton.tsx +56 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatDropZone.tsx +80 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageBubble.tsx +155 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageContent.tsx +182 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageInput.tsx +233 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatSessionSidebar.tsx +218 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatStopButton.tsx +32 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatTodoSidebar.tsx +113 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatView.tsx +842 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/CommitMessageModal.tsx +82 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/DiagramOverlay.tsx +160 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/EditorTabBar.tsx +5 -5
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTree.tsx +9 -10
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeInlineInput.tsx +5 -5
- package/templates/assistkick-product-system/packages/frontend/src/components/FilesView.tsx +112 -41
- package/templates/assistkick-product-system/packages/frontend/src/components/GraphLegend.tsx +2 -2
- package/templates/assistkick-product-system/packages/frontend/src/components/HighlightedText.tsx +87 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ImageLightbox.tsx +192 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +2 -2
- package/templates/assistkick-product-system/packages/frontend/src/components/MentionPill.tsx +33 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/MermaidBlock.tsx +148 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/PermissionDialog.tsx +91 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/PermissionModeSelector.tsx +229 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +249 -83
- package/templates/assistkick-product-system/packages/frontend/src/components/QueuedMessageBubble.tsx +38 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +212 -117
- package/templates/assistkick-product-system/packages/frontend/src/components/SystemPromptAccordion.tsx +48 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/TaskIcon.tsx +11 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +25 -9
- package/templates/assistkick-product-system/packages/frontend/src/components/ToolDiffView.tsx +114 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ToolResultCard.tsx +87 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ToolUseCard.tsx +149 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +25 -8
- package/templates/assistkick-product-system/packages/frontend/src/components/UnifiedGitWidget.tsx +722 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +2 -1
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/ProgrammableNode.tsx +178 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +3 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +103 -9
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +26 -2
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +42 -1
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useDocumentTitle.ts +11 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +1 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/use_chat_stream.ts +826 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/use_file_tree_cache.ts +69 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/use_mention_autocomplete.ts +284 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/attachment_manager.test.ts +183 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/attachment_manager.ts +150 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/chat_message_helpers.test.ts +305 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/chat_message_helpers.ts +113 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/context_usage_helpers.test.ts +157 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/context_usage_helpers.ts +95 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/mermaid_helpers.test.ts +65 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/mermaid_helpers.ts +110 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/message_queue.ts +66 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/tool_use_summary.test.ts +124 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/tool_use_summary.ts +112 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/ChatRoute.tsx +8 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +0 -4
- package/templates/assistkick-product-system/packages/frontend/src/routes/DesignSystemRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/KanbanRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/TerminalRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/UsersRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/accept_invitation.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/forgot_password.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/login.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/register.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/reset_password.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useAttachmentStore.ts +66 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useChatSessionStore.ts +107 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useMessageQueueStore.ts +110 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/usePreviewStore.ts +78 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +7 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +6 -1
- package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +30 -357
- package/templates/assistkick-product-system/packages/frontend/src/utils/parse_node_markdown.test.ts +115 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/parse_node_markdown.ts +91 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/preview_utils.test.ts +30 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/preview_utils.ts +3 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0015_magenta_jazinda.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0016_giant_xorn.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0017_sloppy_mentor.sql +6 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0018_vengeful_kabuki.sql +9 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0019_careful_sentinels.sql +8 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0020_clever_spot.sql +27 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0021_graceful_hex.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0022_short_kingpin.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0023_ambiguous_sharon_carter.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0024_fat_unus.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0015_snapshot.json +1552 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0016_snapshot.json +1560 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0017_snapshot.json +1598 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0018_snapshot.json +1657 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0019_snapshot.json +1709 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0020_snapshot.json +1733 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0021_snapshot.json +1740 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0022_snapshot.json +1755 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0023_snapshot.json +1762 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0024_snapshot.json +1769 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +70 -0
- package/templates/assistkick-product-system/packages/shared/db/schema.ts +40 -1
- package/templates/assistkick-product-system/packages/shared/lib/claude-service.test.ts +236 -0
- package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +46 -5
- package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +65 -39
- package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.test.ts +173 -0
- package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.ts +213 -0
- package/templates/assistkick-product-system/packages/shared/lib/validator.test.ts +70 -0
- package/templates/assistkick-product-system/packages/shared/lib/validator.ts +17 -1
- package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +803 -27
- package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +502 -68
- package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +4 -4
- package/templates/assistkick-product-system/packages/shared/package.json +2 -1
- package/templates/assistkick-product-system/packages/shared/test_fixtures/hanging_stream.mjs +46 -0
- package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +44 -0
- package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +7 -0
- package/templates/assistkick-product-system/packages/shared/tools/remove_node.ts +2 -1
- package/templates/assistkick-product-system/packages/shared/tools/resolve_question.ts +2 -1
- package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -1
- package/templates/assistkick-product-system/tests/message_queue.test.ts +178 -0
- package/templates/assistkick-product-system/tests/message_queue_per_session.test.ts +143 -0
- package/templates/skills/assistkick-bootstrap/SKILL.md +26 -26
- package/templates/skills/assistkick-code-reviewer/SKILL.md +45 -46
- package/templates/skills/assistkick-db-explorer/SKILL.md +13 -13
- package/templates/skills/assistkick-debugger/SKILL.md +23 -23
- package/templates/skills/assistkick-developer/SKILL.md +59 -63
- package/templates/skills/assistkick-interview/SKILL.md +26 -26
- package/templates/skills/assistkick-video-composition-agent/SKILL.md +231 -0
- package/templates/skills/assistkick-video-script-writer/SKILL.md +136 -0
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatView — main Chat v2 page component.
|
|
3
|
+
*
|
|
4
|
+
* Layout: session sidebar on the left, chat message area in the center,
|
|
5
|
+
* message input at the bottom. Integrates the existing ChatSessionSidebar,
|
|
6
|
+
* useChatStream hook, message queue, file attachments, permission dialog,
|
|
7
|
+
* and stop/cancel functionality.
|
|
8
|
+
*
|
|
9
|
+
* New sessions are auto-created when no session exists for the project.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
|
13
|
+
import type {UseChatStreamOptions, ImageBlock, FileBlock, ChatMessage} from '../hooks/use_chat_stream';
|
|
14
|
+
import {useChatStream} from '../hooks/use_chat_stream';
|
|
15
|
+
import type {ChatSession} from '../stores/useChatSessionStore';
|
|
16
|
+
import {useChatSessionStore} from '../stores/useChatSessionStore';
|
|
17
|
+
import {useProjectStore} from '../stores/useProjectStore';
|
|
18
|
+
import {useAttachmentStore} from '../stores/useAttachmentStore';
|
|
19
|
+
import {useMessageQueueStore} from '../stores/useMessageQueueStore';
|
|
20
|
+
import {ChatSessionSidebar} from './ChatSessionSidebar';
|
|
21
|
+
import {ChatMessageBubble} from './ChatMessageBubble';
|
|
22
|
+
import {ChatMessageInput} from './ChatMessageInput';
|
|
23
|
+
import {ChatStopButton} from './ChatStopButton';
|
|
24
|
+
import {ChatDropZone} from './ChatDropZone';
|
|
25
|
+
import {AttachmentPreviewList} from './AttachmentPreviewList';
|
|
26
|
+
import {QueuedMessageBubble} from './QueuedMessageBubble';
|
|
27
|
+
import {PermissionDialog} from './PermissionDialog';
|
|
28
|
+
import type {PermissionMode} from './PermissionModeSelector';
|
|
29
|
+
import {PermissionModeSelector} from './PermissionModeSelector';
|
|
30
|
+
import {AttachmentManager} from '../lib/attachment_manager';
|
|
31
|
+
import type {SkillInfo} from '../api/client';
|
|
32
|
+
import {apiClient} from '../api/client';
|
|
33
|
+
import {Check, ChevronDown, ChevronUp, Copy, Search, Settings2, X, Zap} from 'lucide-react';
|
|
34
|
+
import {IconButton} from './ds/IconButton';
|
|
35
|
+
import {formatTokenCount} from '../lib/context_usage_helpers';
|
|
36
|
+
import {useFileTreeCache} from '../hooks/use_file_tree_cache';
|
|
37
|
+
import {ChatTodoSidebar} from './ChatTodoSidebar';
|
|
38
|
+
import {SystemPromptAccordion} from './SystemPromptAccordion';
|
|
39
|
+
import {countMatches} from './HighlightedText';
|
|
40
|
+
|
|
41
|
+
interface ChatViewProps {
|
|
42
|
+
visible: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function ChatView({ visible }: ChatViewProps) {
|
|
46
|
+
const selectedProjectId = useProjectStore((s) => s.selectedProjectId);
|
|
47
|
+
const activeSessionId = useChatSessionStore((s) => s.activeSessionId);
|
|
48
|
+
const sessions = useChatSessionStore((s) => s.sessions);
|
|
49
|
+
const selectSession = useChatSessionStore((s) => s.selectSession);
|
|
50
|
+
const compactAndContinue = useChatSessionStore((s) => s.compactAndContinue);
|
|
51
|
+
|
|
52
|
+
const attachments = useAttachmentStore((s) => s.attachments);
|
|
53
|
+
const addUpload = useAttachmentStore((s) => s.addUpload);
|
|
54
|
+
const markUploaded = useAttachmentStore((s) => s.markUploaded);
|
|
55
|
+
const markUploadFailed = useAttachmentStore((s) => s.markUploadFailed);
|
|
56
|
+
const removeAttachment = useAttachmentStore((s) => s.remove);
|
|
57
|
+
const clearAttachments = useAttachmentStore((s) => s.clear);
|
|
58
|
+
const getReadyPaths = useAttachmentStore((s) => s.getReadyPaths);
|
|
59
|
+
const hasUploading = useAttachmentStore((s) => s.hasUploading);
|
|
60
|
+
|
|
61
|
+
const queue = useMessageQueueStore((s) => s.queue);
|
|
62
|
+
const enqueueMessage = useMessageQueueStore((s) => s.enqueue);
|
|
63
|
+
const removeFromQueue = useMessageQueueStore((s) => s.remove);
|
|
64
|
+
const drainAll = useMessageQueueStore((s) => s.drainAll);
|
|
65
|
+
const setQueueActiveSession = useMessageQueueStore((s) => s.setActiveSession);
|
|
66
|
+
|
|
67
|
+
const {
|
|
68
|
+
connect,
|
|
69
|
+
disconnect,
|
|
70
|
+
sendMessage,
|
|
71
|
+
sendMergedMessages,
|
|
72
|
+
stopStream,
|
|
73
|
+
setOnStreamEnd,
|
|
74
|
+
loadMessages,
|
|
75
|
+
attachSession,
|
|
76
|
+
messages,
|
|
77
|
+
streaming,
|
|
78
|
+
connected,
|
|
79
|
+
error,
|
|
80
|
+
cancelledUserMessage,
|
|
81
|
+
clearCancelledUserMessage,
|
|
82
|
+
clearMessages,
|
|
83
|
+
restoreMessages,
|
|
84
|
+
pendingPermission,
|
|
85
|
+
respondToPermission,
|
|
86
|
+
contextUsage,
|
|
87
|
+
initContextUsage,
|
|
88
|
+
systemPrompt,
|
|
89
|
+
initSystemPrompt,
|
|
90
|
+
} = useChatStream();
|
|
91
|
+
|
|
92
|
+
// File tree and skills for mention autocomplete
|
|
93
|
+
const filePaths = useFileTreeCache(selectedProjectId);
|
|
94
|
+
const [skills, setSkills] = useState<SkillInfo[]>([]);
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!selectedProjectId) return;
|
|
98
|
+
apiClient.fetchSkills(selectedProjectId)
|
|
99
|
+
.then(({ skills: s }) => setSkills(s))
|
|
100
|
+
.catch(() => {});
|
|
101
|
+
}, [selectedProjectId]);
|
|
102
|
+
|
|
103
|
+
const [inputValue, setInputValue] = useState('');
|
|
104
|
+
// Per-session caches for input and context usage
|
|
105
|
+
const inputCacheRef = useRef<Map<string, string>>(new Map());
|
|
106
|
+
const contextUsageCacheRef = useRef<Map<string, import('../hooks/use_chat_stream').ContextUsage>>(new Map());
|
|
107
|
+
const systemPromptCacheRef = useRef<Map<string, string>>(new Map());
|
|
108
|
+
// Per-session messages cache — preserves in-memory streaming messages across session switches
|
|
109
|
+
const messagesCacheRef = useRef<Map<string, ChatMessage[]>>(new Map());
|
|
110
|
+
// Track which session ID was last used for streaming so we update the right cache entry
|
|
111
|
+
const streamingSessionRef = useRef<string | null>(null);
|
|
112
|
+
const [permissionMode, setPermissionMode] = useState<PermissionMode>(() => {
|
|
113
|
+
const saved = localStorage.getItem('chat_permission_mode');
|
|
114
|
+
return saved === 'skip' || saved === 'allowed_tools' ? saved : 'skip';
|
|
115
|
+
});
|
|
116
|
+
const [allowedTools, setAllowedTools] = useState<string[]>(() => {
|
|
117
|
+
try {
|
|
118
|
+
const saved = localStorage.getItem('chat_allowed_tools');
|
|
119
|
+
return saved ? JSON.parse(saved) : [];
|
|
120
|
+
} catch { return []; }
|
|
121
|
+
});
|
|
122
|
+
useEffect(() => { localStorage.setItem('chat_permission_mode', permissionMode); }, [permissionMode]);
|
|
123
|
+
useEffect(() => { localStorage.setItem('chat_allowed_tools', JSON.stringify(allowedTools)); }, [allowedTools]);
|
|
124
|
+
|
|
125
|
+
const [showSettings, setShowSettings] = useState(false);
|
|
126
|
+
const [isNewSession, setIsNewSession] = useState(true);
|
|
127
|
+
const [compacting, setCompacting] = useState(false);
|
|
128
|
+
const [copiedSessionId, setCopiedSessionId] = useState(false);
|
|
129
|
+
const [searchOpen, setSearchOpen] = useState(false);
|
|
130
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
131
|
+
const [activeMatchIndex, setActiveMatchIndex] = useState(0);
|
|
132
|
+
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
133
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
134
|
+
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
|
135
|
+
const sessionRestoredRef = useRef(false);
|
|
136
|
+
|
|
137
|
+
// --- Search: compute total matches and per-message offsets ---
|
|
138
|
+
const { totalMatches, messageOffsets } = useMemo(() => {
|
|
139
|
+
if (!searchQuery) return { totalMatches: 0, messageOffsets: [] as number[] };
|
|
140
|
+
let total = 0;
|
|
141
|
+
const offsets: number[] = [];
|
|
142
|
+
for (const msg of messages) {
|
|
143
|
+
offsets.push(total);
|
|
144
|
+
for (const block of msg.content) {
|
|
145
|
+
if (block.type === 'text') {
|
|
146
|
+
total += countMatches(block.text, searchQuery);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { totalMatches: total, messageOffsets: offsets };
|
|
151
|
+
}, [messages, searchQuery]);
|
|
152
|
+
|
|
153
|
+
// Reset active match when query changes
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
setActiveMatchIndex(0);
|
|
156
|
+
}, [searchQuery]);
|
|
157
|
+
|
|
158
|
+
// Scroll the active match into view
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
if (!searchQuery || totalMatches === 0) return;
|
|
161
|
+
const el = messagesContainerRef.current?.querySelector(
|
|
162
|
+
`[data-search-match="${activeMatchIndex}"]`,
|
|
163
|
+
);
|
|
164
|
+
el?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
165
|
+
}, [activeMatchIndex, searchQuery, totalMatches]);
|
|
166
|
+
|
|
167
|
+
// Cmd+F / Ctrl+F to toggle search
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
const handler = (e: KeyboardEvent) => {
|
|
170
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'f') {
|
|
171
|
+
e.preventDefault();
|
|
172
|
+
setSearchOpen((prev) => {
|
|
173
|
+
if (!prev) {
|
|
174
|
+
setTimeout(() => searchInputRef.current?.focus(), 0);
|
|
175
|
+
} else {
|
|
176
|
+
setSearchQuery('');
|
|
177
|
+
}
|
|
178
|
+
return !prev;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
if (e.key === 'Escape' && searchOpen) {
|
|
182
|
+
setSearchOpen(false);
|
|
183
|
+
setSearchQuery('');
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
window.addEventListener('keydown', handler);
|
|
187
|
+
return () => window.removeEventListener('keydown', handler);
|
|
188
|
+
}, [searchOpen]);
|
|
189
|
+
|
|
190
|
+
const handleSearchNext = useCallback(() => {
|
|
191
|
+
if (totalMatches === 0) return;
|
|
192
|
+
setActiveMatchIndex((prev) => (prev + 1) % totalMatches);
|
|
193
|
+
}, [totalMatches]);
|
|
194
|
+
|
|
195
|
+
const handleSearchPrev = useCallback(() => {
|
|
196
|
+
if (totalMatches === 0) return;
|
|
197
|
+
setActiveMatchIndex((prev) => (prev - 1 + totalMatches) % totalMatches);
|
|
198
|
+
}, [totalMatches]);
|
|
199
|
+
|
|
200
|
+
const handleCloseSearch = useCallback(() => {
|
|
201
|
+
setSearchOpen(false);
|
|
202
|
+
setSearchQuery('');
|
|
203
|
+
}, []);
|
|
204
|
+
|
|
205
|
+
// Find active session
|
|
206
|
+
const activeSession = useMemo(
|
|
207
|
+
() => sessions.find((s) => s.id === activeSessionId) ?? null,
|
|
208
|
+
[sessions, activeSessionId],
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Whether the currently viewed session is the one that's streaming
|
|
212
|
+
const isActiveSessionStreaming = streaming && activeSession?.id === streamingSessionRef.current;
|
|
213
|
+
|
|
214
|
+
// Displayed context usage: use live data when viewing the streaming session, otherwise use per-session cache.
|
|
215
|
+
// IMPORTANT: never fall back to the hook's global contextUsage — it may hold data from a different session.
|
|
216
|
+
const displayedContextUsage = useMemo(() => {
|
|
217
|
+
if (activeSessionId && activeSessionId === streamingSessionRef.current) {
|
|
218
|
+
return contextUsage;
|
|
219
|
+
}
|
|
220
|
+
if (activeSessionId) {
|
|
221
|
+
return contextUsageCacheRef.current.get(activeSessionId) ?? null;
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}, [contextUsage, activeSessionId]
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// Displayed system prompt: prefer live WS data for the streaming session, fall back to per-session cache
|
|
228
|
+
const displayedSystemPrompt = useMemo(() => {
|
|
229
|
+
if (activeSessionId && activeSessionId === streamingSessionRef.current && systemPrompt) {
|
|
230
|
+
return systemPrompt;
|
|
231
|
+
}
|
|
232
|
+
if (activeSessionId) {
|
|
233
|
+
return systemPromptCacheRef.current.get(activeSessionId) ?? null;
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}, [systemPrompt, activeSessionId]);
|
|
237
|
+
|
|
238
|
+
// Keep per-session system prompt cache in sync
|
|
239
|
+
const prevSystemPromptRef = useRef(systemPrompt);
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
if (!systemPrompt || systemPrompt === prevSystemPromptRef.current) return;
|
|
242
|
+
prevSystemPromptRef.current = systemPrompt;
|
|
243
|
+
|
|
244
|
+
const streamingSid = streamingSessionRef.current;
|
|
245
|
+
if (streamingSid) {
|
|
246
|
+
systemPromptCacheRef.current.set(streamingSid, systemPrompt);
|
|
247
|
+
}
|
|
248
|
+
}, [systemPrompt]);
|
|
249
|
+
|
|
250
|
+
// Connect WebSocket when visible
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
if (visible) {
|
|
253
|
+
connect();
|
|
254
|
+
}
|
|
255
|
+
return () => {
|
|
256
|
+
disconnect();
|
|
257
|
+
};
|
|
258
|
+
}, [visible, connect, disconnect]);
|
|
259
|
+
|
|
260
|
+
// Auto-restore persisted session on mount: load messages and attempt stream re-attachment
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
if (sessionRestoredRef.current) return;
|
|
263
|
+
if (!activeSessionId || !sessions.length || !connected) return;
|
|
264
|
+
|
|
265
|
+
const session = sessions.find((s) => s.id === activeSessionId);
|
|
266
|
+
if (!session) {
|
|
267
|
+
// Persisted session no longer exists — clear it
|
|
268
|
+
selectSession(null);
|
|
269
|
+
sessionRestoredRef.current = true;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
sessionRestoredRef.current = true;
|
|
274
|
+
// Initialize per-session queue for the restored session
|
|
275
|
+
setQueueActiveSession(session.id);
|
|
276
|
+
// Restore system prompt from persisted session data
|
|
277
|
+
if (session.systemPrompt) {
|
|
278
|
+
initSystemPrompt(session.systemPrompt);
|
|
279
|
+
systemPromptCacheRef.current.set(session.id, session.systemPrompt);
|
|
280
|
+
}
|
|
281
|
+
// Restore context usage from persisted session data and populate per-session cache
|
|
282
|
+
if (session.contextUsageJson) {
|
|
283
|
+
try {
|
|
284
|
+
const parsed = JSON.parse(session.contextUsageJson);
|
|
285
|
+
initContextUsage(parsed);
|
|
286
|
+
contextUsageCacheRef.current.set(session.id, parsed);
|
|
287
|
+
} catch { /* ignore */ }
|
|
288
|
+
}
|
|
289
|
+
loadMessages(session.id).then((count) => {
|
|
290
|
+
// Only mark as existing session if it has messages — a brand new session
|
|
291
|
+
// with no messages must use --session-id (not --resume) on the first CLI spawn
|
|
292
|
+
setIsNewSession(count === 0);
|
|
293
|
+
attachSession(session.claudeSessionId);
|
|
294
|
+
});
|
|
295
|
+
}, [activeSessionId, sessions, connected, selectSession, loadMessages, attachSession, setQueueActiveSession]);
|
|
296
|
+
|
|
297
|
+
// Restore cancelled user message text into input
|
|
298
|
+
useEffect(() => {
|
|
299
|
+
if (cancelledUserMessage) {
|
|
300
|
+
setInputValue(cancelledUserMessage);
|
|
301
|
+
clearCancelledUserMessage();
|
|
302
|
+
}
|
|
303
|
+
}, [cancelledUserMessage, clearCancelledUserMessage]);
|
|
304
|
+
|
|
305
|
+
// Keep per-session context usage cache in sync with stream updates.
|
|
306
|
+
// If streaming session differs from viewed session, don't let it overwrite the displayed bar.
|
|
307
|
+
const prevContextUsageRef = useRef(contextUsage);
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
if (!contextUsage || contextUsage === prevContextUsageRef.current) return;
|
|
310
|
+
prevContextUsageRef.current = contextUsage;
|
|
311
|
+
|
|
312
|
+
const streamingSid = streamingSessionRef.current;
|
|
313
|
+
if (streamingSid) {
|
|
314
|
+
contextUsageCacheRef.current.set(streamingSid, contextUsage);
|
|
315
|
+
}
|
|
316
|
+
}, [contextUsage]);
|
|
317
|
+
|
|
318
|
+
// Auto-scroll to bottom when new messages arrive
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
321
|
+
}, [messages, queue]);
|
|
322
|
+
|
|
323
|
+
// Keep a ref to sessions so the onStreamEnd callback can look up session details
|
|
324
|
+
// without stale closure issues.
|
|
325
|
+
const sessionsRef = useRef(sessions);
|
|
326
|
+
sessionsRef.current = sessions;
|
|
327
|
+
|
|
328
|
+
// Register onStreamEnd callback for queue draining.
|
|
329
|
+
// Drains the *streaming* session's queue (not the active session's), so messages
|
|
330
|
+
// enqueued for a streaming session are sent to the correct claudeSessionId even
|
|
331
|
+
// if the user has switched to a different session.
|
|
332
|
+
useEffect(() => {
|
|
333
|
+
setOnStreamEnd(() => {
|
|
334
|
+
const streamingSid = streamingSessionRef.current;
|
|
335
|
+
if (!streamingSid || !selectedProjectId) return;
|
|
336
|
+
|
|
337
|
+
// Invalidate cached messages for the completed session so the next
|
|
338
|
+
// switch loads fresh persisted messages from the database.
|
|
339
|
+
messagesCacheRef.current.delete(streamingSid);
|
|
340
|
+
|
|
341
|
+
const drained = drainAll(streamingSid);
|
|
342
|
+
if (drained.length === 0) return;
|
|
343
|
+
|
|
344
|
+
const streamingSession = sessionsRef.current.find((s) => s.id === streamingSid);
|
|
345
|
+
if (!streamingSession) return;
|
|
346
|
+
|
|
347
|
+
const texts = drained.map((m) => m.text);
|
|
348
|
+
const opts: UseChatStreamOptions = {
|
|
349
|
+
projectId: selectedProjectId,
|
|
350
|
+
sessionId: streamingSession.id,
|
|
351
|
+
claudeSessionId: streamingSession.claudeSessionId,
|
|
352
|
+
isNewSession: false,
|
|
353
|
+
permissionMode,
|
|
354
|
+
allowedTools: permissionMode === 'allowed_tools' ? allowedTools : undefined,
|
|
355
|
+
};
|
|
356
|
+
sendMergedMessages(texts, opts);
|
|
357
|
+
});
|
|
358
|
+
return () => setOnStreamEnd(null);
|
|
359
|
+
}, [setOnStreamEnd, drainAll, selectedProjectId, permissionMode, allowedTools, sendMergedMessages]);
|
|
360
|
+
|
|
361
|
+
// Handle session selection — save current session state, restore new one
|
|
362
|
+
const handleSelectSession = useCallback(
|
|
363
|
+
(session: ChatSession) => {
|
|
364
|
+
// Save current session's input, messages, context usage, and system prompt
|
|
365
|
+
if (activeSessionId) {
|
|
366
|
+
inputCacheRef.current.set(activeSessionId, inputValue);
|
|
367
|
+
messagesCacheRef.current.set(activeSessionId, messages);
|
|
368
|
+
if (contextUsage) contextUsageCacheRef.current.set(activeSessionId, contextUsage);
|
|
369
|
+
if (systemPrompt) systemPromptCacheRef.current.set(activeSessionId, systemPrompt);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
clearAttachments();
|
|
373
|
+
|
|
374
|
+
// Switch the queue store to the target session (shows that session's queued messages)
|
|
375
|
+
setQueueActiveSession(session.id);
|
|
376
|
+
|
|
377
|
+
// Restore input for the target session
|
|
378
|
+
setInputValue(inputCacheRef.current.get(session.id) ?? '');
|
|
379
|
+
|
|
380
|
+
// Restore system prompt: prefer in-memory cache, fall back to persisted DB value
|
|
381
|
+
const cachedPrompt = systemPromptCacheRef.current.get(session.id);
|
|
382
|
+
if (cachedPrompt) {
|
|
383
|
+
initSystemPrompt(cachedPrompt);
|
|
384
|
+
} else if (session.systemPrompt) {
|
|
385
|
+
initSystemPrompt(session.systemPrompt);
|
|
386
|
+
systemPromptCacheRef.current.set(session.id, session.systemPrompt);
|
|
387
|
+
} else {
|
|
388
|
+
initSystemPrompt(null);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Restore context usage: prefer in-memory cache (live), fall back to persisted DB value
|
|
392
|
+
const cachedCtx = contextUsageCacheRef.current.get(session.id);
|
|
393
|
+
if (cachedCtx) {
|
|
394
|
+
initContextUsage(cachedCtx);
|
|
395
|
+
} else if (session.contextUsageJson) {
|
|
396
|
+
try {
|
|
397
|
+
const parsed = JSON.parse(session.contextUsageJson);
|
|
398
|
+
initContextUsage(parsed);
|
|
399
|
+
contextUsageCacheRef.current.set(session.id, parsed);
|
|
400
|
+
} catch { initContextUsage(null); }
|
|
401
|
+
} else {
|
|
402
|
+
initContextUsage(null);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Restore messages: prefer in-memory cache (preserves streaming state), fall back to DB
|
|
406
|
+
const cachedMessages = messagesCacheRef.current.get(session.id);
|
|
407
|
+
if (cachedMessages) {
|
|
408
|
+
restoreMessages(cachedMessages);
|
|
409
|
+
setIsNewSession(cachedMessages.length === 0);
|
|
410
|
+
// If this session is currently streaming, re-attach to get catch-up content
|
|
411
|
+
if (session.id === streamingSessionRef.current) {
|
|
412
|
+
attachSession(session.claudeSessionId);
|
|
413
|
+
}
|
|
414
|
+
} else {
|
|
415
|
+
clearMessages();
|
|
416
|
+
loadMessages(session.id).then((count) => {
|
|
417
|
+
setIsNewSession(count === 0);
|
|
418
|
+
// If this session is currently streaming, re-attach to get catch-up content
|
|
419
|
+
if (session.id === streamingSessionRef.current) {
|
|
420
|
+
attachSession(session.claudeSessionId);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
[activeSessionId, inputValue, messages, contextUsage, systemPrompt, clearMessages, restoreMessages, clearAttachments, loadMessages, attachSession, initContextUsage, initSystemPrompt, setQueueActiveSession],
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
// Handle selecting a newly created session
|
|
429
|
+
const handleNewSessionCreated = useCallback(
|
|
430
|
+
(session: ChatSession) => {
|
|
431
|
+
// Save current session's state before switching
|
|
432
|
+
if (activeSessionId) {
|
|
433
|
+
inputCacheRef.current.set(activeSessionId, inputValue);
|
|
434
|
+
messagesCacheRef.current.set(activeSessionId, messages);
|
|
435
|
+
if (contextUsage) contextUsageCacheRef.current.set(activeSessionId, contextUsage);
|
|
436
|
+
if (systemPrompt) systemPromptCacheRef.current.set(activeSessionId, systemPrompt);
|
|
437
|
+
}
|
|
438
|
+
clearMessages();
|
|
439
|
+
clearAttachments();
|
|
440
|
+
initSystemPrompt(null);
|
|
441
|
+
// Switch the queue store to the new session (starts with an empty queue)
|
|
442
|
+
setQueueActiveSession(session.id);
|
|
443
|
+
setIsNewSession(true);
|
|
444
|
+
setInputValue('');
|
|
445
|
+
},
|
|
446
|
+
[activeSessionId, inputValue, messages, contextUsage, systemPrompt, clearMessages, clearAttachments, initSystemPrompt, setQueueActiveSession],
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
// File handling for attachments
|
|
450
|
+
const handleFilesSelected = useCallback(
|
|
451
|
+
async (files: File[]) => {
|
|
452
|
+
if (!selectedProjectId) return;
|
|
453
|
+
|
|
454
|
+
for (const file of files) {
|
|
455
|
+
const isImage = AttachmentManager.isImage(file.type);
|
|
456
|
+
let preview: string | null = null;
|
|
457
|
+
if (isImage) {
|
|
458
|
+
preview = await readFileAsDataUrl(file);
|
|
459
|
+
}
|
|
460
|
+
const att = addUpload(file, preview);
|
|
461
|
+
|
|
462
|
+
// Upload the file
|
|
463
|
+
try {
|
|
464
|
+
const reader = new FileReader();
|
|
465
|
+
const base64 = await new Promise<string>((resolve, reject) => {
|
|
466
|
+
reader.onload = () => {
|
|
467
|
+
const result = reader.result as string;
|
|
468
|
+
resolve(result.split(',')[1]);
|
|
469
|
+
};
|
|
470
|
+
reader.onerror = reject;
|
|
471
|
+
reader.readAsDataURL(file);
|
|
472
|
+
});
|
|
473
|
+
const { path } = await apiClient.uploadChatFile(selectedProjectId, file.name, base64);
|
|
474
|
+
markUploaded(att.id, path);
|
|
475
|
+
} catch {
|
|
476
|
+
markUploadFailed(att.id);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
[selectedProjectId, addUpload, markUploaded, markUploadFailed],
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
// Send message
|
|
484
|
+
const handleSend = useCallback(() => {
|
|
485
|
+
const trimmed = inputValue.trim();
|
|
486
|
+
if (!trimmed || !activeSession || !selectedProjectId) return;
|
|
487
|
+
|
|
488
|
+
// If this session is currently streaming, enqueue instead of sending directly.
|
|
489
|
+
// If a *different* session is streaming, send directly — this session is idle.
|
|
490
|
+
if (streaming && activeSession.id === streamingSessionRef.current) {
|
|
491
|
+
enqueueMessage(trimmed, activeSession.id);
|
|
492
|
+
setInputValue('');
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const readyPaths = getReadyPaths();
|
|
497
|
+
|
|
498
|
+
// Build attachment content blocks for inline rendering and DB persistence
|
|
499
|
+
const attachmentBlocks: (ImageBlock | FileBlock)[] = [];
|
|
500
|
+
for (const att of attachments) {
|
|
501
|
+
if (att.uploading || !att.path) continue;
|
|
502
|
+
if (AttachmentManager.isImage(att.mimeType)) {
|
|
503
|
+
attachmentBlocks.push({ type: 'image', path: att.path, name: att.name, mimeType: att.mimeType });
|
|
504
|
+
} else {
|
|
505
|
+
attachmentBlocks.push({ type: 'file', path: att.path, name: att.name, mimeType: att.mimeType });
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const opts: UseChatStreamOptions = {
|
|
510
|
+
projectId: selectedProjectId,
|
|
511
|
+
sessionId: activeSession.id,
|
|
512
|
+
claudeSessionId: activeSession.claudeSessionId,
|
|
513
|
+
isNewSession,
|
|
514
|
+
permissionMode,
|
|
515
|
+
allowedTools: permissionMode === 'allowed_tools' ? allowedTools : undefined,
|
|
516
|
+
attachments: readyPaths.length > 0 ? readyPaths : undefined,
|
|
517
|
+
attachmentBlocks: attachmentBlocks.length > 0 ? attachmentBlocks : undefined,
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
streamingSessionRef.current = activeSession.id;
|
|
521
|
+
sendMessage(trimmed, opts);
|
|
522
|
+
setInputValue('');
|
|
523
|
+
clearAttachments();
|
|
524
|
+
setIsNewSession(false);
|
|
525
|
+
}, [
|
|
526
|
+
inputValue, activeSession, selectedProjectId, streaming,
|
|
527
|
+
isNewSession, permissionMode, allowedTools,
|
|
528
|
+
sendMessage, enqueueMessage, getReadyPaths, clearAttachments,
|
|
529
|
+
]);
|
|
530
|
+
|
|
531
|
+
// Stop the streaming session's CLI process, targeting the correct claudeSessionId
|
|
532
|
+
const handleStop = useCallback(() => {
|
|
533
|
+
const streamingSid = streamingSessionRef.current;
|
|
534
|
+
const streamingSession = streamingSid ? sessions.find((s) => s.id === streamingSid) : null;
|
|
535
|
+
stopStream(streamingSession?.claudeSessionId);
|
|
536
|
+
}, [sessions, stopStream]);
|
|
537
|
+
|
|
538
|
+
// Permission response handler
|
|
539
|
+
const handlePermissionResponse = useCallback(
|
|
540
|
+
(requestId: string, decision: import('./PermissionDialog').PermissionDecision) => {
|
|
541
|
+
if (!selectedProjectId) return;
|
|
542
|
+
respondToPermission(requestId, selectedProjectId, decision);
|
|
543
|
+
},
|
|
544
|
+
[selectedProjectId, respondToPermission],
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
// Compact and continue: server compacts messages and creates new session with continuation context
|
|
548
|
+
const handleCompactAndContinue = useCallback(async () => {
|
|
549
|
+
if (!activeSession || !selectedProjectId || streaming || compacting) return;
|
|
550
|
+
|
|
551
|
+
setCompacting(true);
|
|
552
|
+
try {
|
|
553
|
+
await compactAndContinue(activeSession.id, selectedProjectId);
|
|
554
|
+
|
|
555
|
+
// Clear UI for the new session (store already selected it)
|
|
556
|
+
clearMessages();
|
|
557
|
+
clearAttachments();
|
|
558
|
+
initContextUsage(null);
|
|
559
|
+
initSystemPrompt(null);
|
|
560
|
+
setIsNewSession(true);
|
|
561
|
+
} catch {
|
|
562
|
+
// Errors will show in console; user can retry
|
|
563
|
+
} finally {
|
|
564
|
+
setCompacting(false);
|
|
565
|
+
}
|
|
566
|
+
}, [
|
|
567
|
+
activeSession, selectedProjectId, streaming, compacting,
|
|
568
|
+
compactAndContinue, clearMessages, clearAttachments, initContextUsage, initSystemPrompt,
|
|
569
|
+
]);
|
|
570
|
+
|
|
571
|
+
return (
|
|
572
|
+
<div className="flex h-full overflow-hidden">
|
|
573
|
+
{/* Session sidebar */}
|
|
574
|
+
<ChatSessionSidebar
|
|
575
|
+
visible={visible}
|
|
576
|
+
onSelectSession={handleSelectSession}
|
|
577
|
+
onNewSessionCreated={handleNewSessionCreated}
|
|
578
|
+
/>
|
|
579
|
+
|
|
580
|
+
{/* Chat area */}
|
|
581
|
+
<ChatDropZone onFilesDropped={handleFilesSelected} disabled={!activeSession}>
|
|
582
|
+
<div className="flex flex-col h-full">
|
|
583
|
+
{/* Header bar — fixed height to prevent layout shift when search toggles */}
|
|
584
|
+
<div className="flex items-center justify-between px-4 border-b border-edge shrink-0 h-11">
|
|
585
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
586
|
+
<span className="text-[13px] font-mono text-content truncate">
|
|
587
|
+
{activeSession?.name ?? 'No session selected'}
|
|
588
|
+
</span>
|
|
589
|
+
{connected && (
|
|
590
|
+
<span className="w-1.5 h-1.5 rounded-full bg-accent shrink-0" title="Connected" />
|
|
591
|
+
)}
|
|
592
|
+
{!connected && visible && (
|
|
593
|
+
<span className="w-1.5 h-1.5 rounded-full bg-error shrink-0" title="Disconnected" />
|
|
594
|
+
)}
|
|
595
|
+
{activeSession && (
|
|
596
|
+
<button
|
|
597
|
+
type="button"
|
|
598
|
+
className="flex items-center gap-1 text-[11px] font-mono text-content-muted hover:text-content transition-colors cursor-pointer bg-transparent border-none p-0 shrink-0"
|
|
599
|
+
title="Copy session ID"
|
|
600
|
+
onClick={() => {
|
|
601
|
+
navigator.clipboard.writeText(activeSession.claudeSessionId);
|
|
602
|
+
setCopiedSessionId(true);
|
|
603
|
+
setTimeout(() => setCopiedSessionId(false), 1500);
|
|
604
|
+
}}
|
|
605
|
+
>
|
|
606
|
+
<span className="truncate max-w-45">{activeSession.claudeSessionId}</span>
|
|
607
|
+
{copiedSessionId ? <Check size={12} /> : <Copy size={12} />}
|
|
608
|
+
</button>
|
|
609
|
+
)}
|
|
610
|
+
</div>
|
|
611
|
+
|
|
612
|
+
{/* Search bar — right side of header */}
|
|
613
|
+
<div className="flex items-center gap-1.5 shrink-0">
|
|
614
|
+
{searchOpen ? (
|
|
615
|
+
<div className="flex items-center gap-1.5 bg-surface-alt border border-edge rounded-lg px-2">
|
|
616
|
+
<Search size={13} className="text-content-muted shrink-0" />
|
|
617
|
+
<input
|
|
618
|
+
ref={searchInputRef}
|
|
619
|
+
type="text"
|
|
620
|
+
value={searchQuery}
|
|
621
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
622
|
+
onKeyDown={(e) => {
|
|
623
|
+
if (e.key === 'Enter') {
|
|
624
|
+
e.shiftKey ? handleSearchPrev() : handleSearchNext();
|
|
625
|
+
}
|
|
626
|
+
if (e.key === 'Escape') handleCloseSearch();
|
|
627
|
+
}}
|
|
628
|
+
placeholder="Search…"
|
|
629
|
+
className="w-36 bg-transparent border-none outline-none text-[12px] font-mono text-content placeholder:text-content-muted"
|
|
630
|
+
autoFocus
|
|
631
|
+
/>
|
|
632
|
+
{searchQuery && (
|
|
633
|
+
<span className="text-[10px] font-mono text-content-muted whitespace-nowrap">
|
|
634
|
+
{totalMatches === 0
|
|
635
|
+
? '0/0'
|
|
636
|
+
: `${activeMatchIndex + 1}/${totalMatches}`}
|
|
637
|
+
</span>
|
|
638
|
+
)}
|
|
639
|
+
<IconButton label="Previous match" variant="ghost" size="sm" onClick={handleSearchPrev} disabled={totalMatches === 0}>
|
|
640
|
+
<ChevronUp size={12} />
|
|
641
|
+
</IconButton>
|
|
642
|
+
<IconButton label="Next match" variant="ghost" size="sm" onClick={handleSearchNext} disabled={totalMatches === 0}>
|
|
643
|
+
<ChevronDown size={12} />
|
|
644
|
+
</IconButton>
|
|
645
|
+
<IconButton label="Close search" variant="ghost" size="sm" onClick={handleCloseSearch}>
|
|
646
|
+
<X size={12} />
|
|
647
|
+
</IconButton>
|
|
648
|
+
</div>
|
|
649
|
+
) : (
|
|
650
|
+
<IconButton
|
|
651
|
+
label="Search messages"
|
|
652
|
+
variant="ghost"
|
|
653
|
+
size="sm"
|
|
654
|
+
onClick={() => {
|
|
655
|
+
setSearchOpen(true);
|
|
656
|
+
setTimeout(() => searchInputRef.current?.focus(), 0);
|
|
657
|
+
}}
|
|
658
|
+
>
|
|
659
|
+
<Search size={14} strokeWidth={2} />
|
|
660
|
+
</IconButton>
|
|
661
|
+
)}
|
|
662
|
+
</div>
|
|
663
|
+
</div>
|
|
664
|
+
|
|
665
|
+
{/* Messages area + optional todo sidebar */}
|
|
666
|
+
<div className="flex-1 flex overflow-hidden min-h-0">
|
|
667
|
+
<div
|
|
668
|
+
ref={messagesContainerRef}
|
|
669
|
+
className="flex-1 overflow-y-auto overflow-x-hidden px-4 py-4 min-w-0"
|
|
670
|
+
>
|
|
671
|
+
{!activeSession && (
|
|
672
|
+
<div className="flex items-center justify-center h-full">
|
|
673
|
+
<p className="text-sm font-mono text-content-muted">
|
|
674
|
+
Select or create a chat session to begin
|
|
675
|
+
</p>
|
|
676
|
+
</div>
|
|
677
|
+
)}
|
|
678
|
+
|
|
679
|
+
{activeSession && messages.length === 0 && !isActiveSessionStreaming && (
|
|
680
|
+
<div className="flex items-center justify-center h-full">
|
|
681
|
+
<p className="text-sm font-mono text-content-muted">
|
|
682
|
+
Start a conversation with Claude
|
|
683
|
+
</p>
|
|
684
|
+
</div>
|
|
685
|
+
)}
|
|
686
|
+
|
|
687
|
+
<div className="flex flex-col gap-4 min-w-0">
|
|
688
|
+
{displayedSystemPrompt && (
|
|
689
|
+
<SystemPromptAccordion prompt={displayedSystemPrompt} />
|
|
690
|
+
)}
|
|
691
|
+
|
|
692
|
+
{messages.map((msg, idx) => (
|
|
693
|
+
<ChatMessageBubble
|
|
694
|
+
key={msg.id}
|
|
695
|
+
message={msg}
|
|
696
|
+
searchQuery={searchQuery}
|
|
697
|
+
activeMatchIndex={activeMatchIndex}
|
|
698
|
+
matchIndexOffset={messageOffsets[idx] ?? 0}
|
|
699
|
+
/>
|
|
700
|
+
))}
|
|
701
|
+
|
|
702
|
+
{/* Queued messages */}
|
|
703
|
+
{queue.map((qm) => (
|
|
704
|
+
<QueuedMessageBubble
|
|
705
|
+
key={qm.id}
|
|
706
|
+
id={qm.id}
|
|
707
|
+
text={qm.text}
|
|
708
|
+
onRemove={removeFromQueue}
|
|
709
|
+
/>
|
|
710
|
+
))}
|
|
711
|
+
|
|
712
|
+
<div ref={messagesEndRef} />
|
|
713
|
+
</div>
|
|
714
|
+
</div>
|
|
715
|
+
|
|
716
|
+
{/* Todo sidebar — shown when tasks exist */}
|
|
717
|
+
<ChatTodoSidebar messages={messages} />
|
|
718
|
+
</div>
|
|
719
|
+
|
|
720
|
+
{/* Error banner */}
|
|
721
|
+
{error && (
|
|
722
|
+
<div className="px-4 py-2 bg-error/10 border-t border-error/30 text-error text-[12px] font-mono shrink-0">
|
|
723
|
+
{error}
|
|
724
|
+
</div>
|
|
725
|
+
)}
|
|
726
|
+
|
|
727
|
+
{/* Attachment previews */}
|
|
728
|
+
{attachments.length > 0 && (
|
|
729
|
+
<div className="border-t border-edge shrink-0 pt-2">
|
|
730
|
+
<AttachmentPreviewList attachments={attachments} onRemove={removeAttachment} />
|
|
731
|
+
</div>
|
|
732
|
+
)}
|
|
733
|
+
|
|
734
|
+
{/* Settings panel (collapsible) */}
|
|
735
|
+
{showSettings && (
|
|
736
|
+
<div className="border-t border-edge px-4 py-3 bg-surface-alt shrink-0">
|
|
737
|
+
<PermissionModeSelector
|
|
738
|
+
mode={permissionMode}
|
|
739
|
+
allowedTools={allowedTools}
|
|
740
|
+
onModeChange={setPermissionMode}
|
|
741
|
+
onAllowedToolsChange={setAllowedTools}
|
|
742
|
+
/>
|
|
743
|
+
</div>
|
|
744
|
+
)}
|
|
745
|
+
|
|
746
|
+
{/* Action buttons + context usage bar */}
|
|
747
|
+
<div className="px-4 py-2 shrink-0 flex items-center gap-2">
|
|
748
|
+
<ChatStopButton streaming={isActiveSessionStreaming} onStop={handleStop} />
|
|
749
|
+
{activeSession && !isActiveSessionStreaming && (
|
|
750
|
+
<button
|
|
751
|
+
type="button"
|
|
752
|
+
className="flex items-center gap-1.5 h-7 px-2.5 rounded text-[11px] font-mono text-content-secondary bg-transparent border border-edge hover:bg-surface-raised hover:text-content transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
|
753
|
+
onClick={handleCompactAndContinue}
|
|
754
|
+
disabled={compacting || messages.length === 0}
|
|
755
|
+
>
|
|
756
|
+
<Zap size={12} strokeWidth={2} className={compacting ? 'animate-pulse' : ''} />
|
|
757
|
+
Compact
|
|
758
|
+
</button>
|
|
759
|
+
)}
|
|
760
|
+
<IconButton
|
|
761
|
+
label="Chat settings"
|
|
762
|
+
variant={showSettings ? 'accent' : 'ghost'}
|
|
763
|
+
size="sm"
|
|
764
|
+
onClick={() => setShowSettings((prev) => !prev)}
|
|
765
|
+
>
|
|
766
|
+
<Settings2 size={14} strokeWidth={2} />
|
|
767
|
+
</IconButton>
|
|
768
|
+
{displayedContextUsage && (() => {
|
|
769
|
+
const ctxWindow = displayedContextUsage.contextWindow ?? 200_000;
|
|
770
|
+
const AUTOCOMPACT_BUFFER = 33_000;
|
|
771
|
+
const usableWindow = ctxWindow - AUTOCOMPACT_BUFFER;
|
|
772
|
+
const bufferPct = (AUTOCOMPACT_BUFFER / ctxWindow) * 100;
|
|
773
|
+
const usagePct = Math.min(Math.round((displayedContextUsage.totalTokens / ctxWindow) * 100), 100);
|
|
774
|
+
return (
|
|
775
|
+
<div className="flex-1 flex items-center gap-2">
|
|
776
|
+
<div className="flex-1 h-1.5 bg-edge rounded-full overflow-hidden relative">
|
|
777
|
+
<div
|
|
778
|
+
className="absolute left-0 top-0 h-full bg-accent rounded-l-full transition-all duration-300"
|
|
779
|
+
style={{ width: `${usagePct}%` }}
|
|
780
|
+
/>
|
|
781
|
+
<div
|
|
782
|
+
className="absolute right-0 top-0 h-full bg-error rounded-r-full"
|
|
783
|
+
style={{ width: `${bufferPct}%` }}
|
|
784
|
+
/>
|
|
785
|
+
</div>
|
|
786
|
+
<span className="text-[11px] font-mono text-content-muted whitespace-nowrap">
|
|
787
|
+
{formatTokenCount(displayedContextUsage.totalTokens)}/{formatTokenCount(usableWindow)}
|
|
788
|
+
</span>
|
|
789
|
+
</div>
|
|
790
|
+
);
|
|
791
|
+
})()}
|
|
792
|
+
</div>
|
|
793
|
+
|
|
794
|
+
{/* Input area */}
|
|
795
|
+
<div className="px-4 py-3 border-t border-edge shrink-0">
|
|
796
|
+
<ChatMessageInput
|
|
797
|
+
value={inputValue}
|
|
798
|
+
onChange={setInputValue}
|
|
799
|
+
onSend={handleSend}
|
|
800
|
+
onFilesSelected={handleFilesSelected}
|
|
801
|
+
disabled={!activeSession || hasUploading() || !connected}
|
|
802
|
+
placeholder={
|
|
803
|
+
!connected
|
|
804
|
+
? 'Disconnected \u2014 reconnecting\u2026'
|
|
805
|
+
: isActiveSessionStreaming
|
|
806
|
+
? 'Type to queue a message\u2026'
|
|
807
|
+
: 'Type a message\u2026'
|
|
808
|
+
}
|
|
809
|
+
filePaths={filePaths}
|
|
810
|
+
skills={skills}
|
|
811
|
+
/>
|
|
812
|
+
</div>
|
|
813
|
+
</div>
|
|
814
|
+
</ChatDropZone>
|
|
815
|
+
|
|
816
|
+
{/* Permission dialog overlay */}
|
|
817
|
+
<PermissionDialog
|
|
818
|
+
request={
|
|
819
|
+
pendingPermission
|
|
820
|
+
? {
|
|
821
|
+
requestId: pendingPermission.requestId,
|
|
822
|
+
toolName: pendingPermission.toolName,
|
|
823
|
+
input: pendingPermission.input,
|
|
824
|
+
}
|
|
825
|
+
: null
|
|
826
|
+
}
|
|
827
|
+
onRespond={handlePermissionResponse}
|
|
828
|
+
/>
|
|
829
|
+
</div>
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Read a file as a data URL for image preview.
|
|
835
|
+
*/
|
|
836
|
+
const readFileAsDataUrl = (file: File): Promise<string> =>
|
|
837
|
+
new Promise((resolve, reject) => {
|
|
838
|
+
const reader = new FileReader();
|
|
839
|
+
reader.onload = () => resolve(reader.result as string);
|
|
840
|
+
reader.onerror = reject;
|
|
841
|
+
reader.readAsDataURL(file);
|
|
842
|
+
});
|