diff --git a/desktop/src/__tests__/pages.test.tsx b/desktop/src/__tests__/pages.test.tsx index b38dd120..831be9f7 100644 --- a/desktop/src/__tests__/pages.test.tsx +++ b/desktop/src/__tests__/pages.test.tsx @@ -13,6 +13,7 @@ import { ToolInspection } from '../pages/ToolInspection' import { Sidebar } from '../components/layout/Sidebar' import { UserMessage } from '../components/chat/UserMessage' import { useChatStore } from '../stores/chatStore' +import { useSessionStore } from '../stores/sessionStore' import { useTabStore } from '../stores/tabStore' /** @@ -62,13 +63,71 @@ describe('Content-only pages render without errors', () => { // With empty messages, the hero is shown expect(container.innerHTML).toContain('New session') // ChatInput has a textarea - expect(container.querySelector('textarea')).toBeInTheDocument() + const textarea = container.querySelector('textarea') + expect(textarea).toBeInTheDocument() + expect(textarea).toHaveAttribute('placeholder', 'Ask anything...') + expect(textarea).toHaveAttribute('rows', '2') expect(container.innerHTML).not.toContain('Preview') // Cleanup useTabStore.setState({ tabs: [], activeTabId: null }) useChatStore.setState({ sessions: {} }) }) + it('ActiveSession keeps the compact composer once messages exist', () => { + const SESSION_ID = 'test-active-session-with-messages' + useTabStore.setState({ tabs: [{ sessionId: SESSION_ID, title: 'Test', type: 'session' as const, status: 'idle' }], activeTabId: SESSION_ID }) + useChatStore.setState({ + sessions: { + [SESSION_ID]: { + messages: [{ + id: 'msg-1', + type: 'user_text', + content: 'hello', + timestamp: Date.now(), + }], + chatState: 'idle', + connectionState: 'connected', + streamingText: '', + streamingToolInput: '', + activeToolUseId: null, + activeToolName: null, + activeThinkingId: null, + pendingPermission: null, + tokenUsage: { input_tokens: 0, output_tokens: 0 }, + elapsedSeconds: 0, + statusVerb: '', + slashCommands: [], + agentTaskNotifications: {}, + elapsedTimer: null, + }, + }, + }) + useSessionStore.setState({ + sessions: [{ + id: SESSION_ID, + title: 'Test', + createdAt: '2026-04-10T00:00:00.000Z', + modifiedAt: '2026-04-10T00:00:00.000Z', + messageCount: 1, + projectPath: '', + workDir: null, + workDirExists: true, + }], + activeSessionId: SESSION_ID, + isLoading: false, + error: null, + }) + + render() + + const textarea = screen.getByPlaceholderText('Ask Claude to edit, debug or explain...') + expect(textarea).toHaveAttribute('rows', '1') + + useTabStore.setState({ tabs: [], activeTabId: null }) + useSessionStore.setState({ sessions: [], activeSessionId: null, isLoading: false, error: null }) + useChatStore.setState({ sessions: {} }) + }) + it('ActiveSession shows a single primary action button while a turn is active', () => { useTabStore.setState({ activeTabId: 'active-tab', tabs: [{ sessionId: 'active-tab', title: 'Test', type: 'session' as const, status: 'idle' }] }) useChatStore.setState({ diff --git a/desktop/src/__tests__/skillsSettings.test.tsx b/desktop/src/__tests__/skillsSettings.test.tsx index 1c522862..19e46203 100644 --- a/desktop/src/__tests__/skillsSettings.test.tsx +++ b/desktop/src/__tests__/skillsSettings.test.tsx @@ -5,6 +5,8 @@ import '@testing-library/jest-dom' import { Settings } from '../pages/Settings' import { useSkillStore } from '../stores/skillStore' import { useSettingsStore } from '../stores/settingsStore' +import { useSessionStore } from '../stores/sessionStore' +import { useTabStore, SETTINGS_TAB_ID } from '../stores/tabStore' vi.mock('../api/agents', () => ({ agentsApi: { @@ -60,6 +62,26 @@ describe('Settings > Skills tab', () => { beforeEach(() => { vi.clearAllMocks() useSettingsStore.setState({ locale: 'en' }) + useSessionStore.setState({ + sessions: [ + { + id: 'session-1', + title: 'Active session', + createdAt: '2026-04-20T00:00:00.000Z', + modifiedAt: '2026-04-20T00:00:00.000Z', + messageCount: 1, + projectPath: '/workspace/project', + workDir: '/workspace/project', + workDirExists: true, + }, + ], + activeSessionId: 'session-1', + isLoading: false, + error: null, + selectedProjects: [], + availableProjects: ['/workspace/project'], + }) + useTabStore.setState({ tabs: [], activeTabId: null }) useSkillStore.setState({ skills: [], selectedSkill: null, @@ -106,6 +128,29 @@ describe('Settings > Skills tab', () => { expect(screen.getByText('Second skill description')).toBeInTheDocument() }) + it('uses the active session workDir even when settings tab is focused', () => { + const fetchSkills = vi.fn() + useSkillStore.setState({ + skills: [], + selectedSkill: null, + isLoading: false, + isDetailLoading: false, + error: null, + fetchSkills, + fetchSkillDetail: MOCK_FETCH_SKILL_DETAIL, + clearSelection: MOCK_CLEAR_SELECTION, + }) + useTabStore.setState({ + activeTabId: SETTINGS_TAB_ID, + tabs: [{ sessionId: SETTINGS_TAB_ID, title: 'Settings', type: 'settings', status: 'idle' }], + }) + + render() + switchToSkillsTab() + + expect(fetchSkills).toHaveBeenCalledWith('/workspace/project') + }) + it('opens skill detail with metadata cards and parsed markdown body', () => { useSkillStore.setState({ selectedSkill: { diff --git a/desktop/src/api/skills.ts b/desktop/src/api/skills.ts index 3ad50b04..18b0c878 100644 --- a/desktop/src/api/skills.ts +++ b/desktop/src/api/skills.ts @@ -2,10 +2,18 @@ import { api } from './client' import type { SkillMeta, SkillDetail } from '../types/skill' export const skillsApi = { - list: () => api.get<{ skills: SkillMeta[] }>('/api/skills'), + list: (cwd?: string) => { + const query = cwd ? `?cwd=${encodeURIComponent(cwd)}` : '' + return api.get<{ skills: SkillMeta[] }>(`/api/skills${query}`) + }, - detail: (source: string, name: string) => - api.get<{ detail: SkillDetail }>( - `/api/skills/detail?source=${encodeURIComponent(source)}&name=${encodeURIComponent(name)}`, - ), + detail: (source: string, name: string, cwd?: string) => { + const query = new URLSearchParams({ + source, + name, + }) + if (cwd) query.set('cwd', cwd) + + return api.get<{ detail: SkillDetail }>(`/api/skills/detail?${query.toString()}`) + }, } diff --git a/desktop/src/components/chat/ChatInput.tsx b/desktop/src/components/chat/ChatInput.tsx index 6f4cb8e1..b9cff77a 100644 --- a/desktop/src/components/chat/ChatInput.tsx +++ b/desktop/src/components/chat/ChatInput.tsx @@ -30,7 +30,11 @@ type Attachment = { data?: string } -export function ChatInput() { +type ChatInputProps = { + variant?: 'default' | 'hero' +} + +export function ChatInput({ variant = 'default' }: ChatInputProps) { const t = useTranslation() const [input, setInput] = useState('') const [attachments, setAttachments] = useState([]) @@ -62,6 +66,7 @@ export function ChatInput() { const isActive = chatState !== 'idle' const isWorkspaceMissing = activeSession?.workDirExists === false const canSubmit = !isWorkspaceMissing && (input.trim().length > 0 || (!isMemberSession && attachments.length > 0)) + const isHeroComposer = variant === 'hero' && !isMemberSession useEffect(() => { textareaRef.current?.focus() @@ -393,11 +398,25 @@ export function ChatInput() { }) } + const composerPlaceholder = + isHeroComposer + ? t('empty.placeholder') + : isWorkspaceMissing + ? t('chat.placeholderMissing') + : isMemberSession + ? t('teams.memberPlaceholder') + : t('chat.placeholder') + + const addFilesLabel = isHeroComposer ? t('empty.addFiles') : t('chat.addFiles') + const slashCommandsLabel = isHeroComposer ? t('empty.slashCommands') : t('chat.slashCommands') + return ( -
-
+
+
event.preventDefault()} onDrop={handleDrop} > @@ -436,18 +455,16 @@ export function ChatInput() { ref={(el) => { slashItemRefs.current[index] = el }} onClick={() => selectSlashCommand(command.name)} onMouseEnter={() => setSlashSelectedIndex(index)} - className={`flex w-full items-center justify-between gap-3 px-4 py-2.5 text-left transition-colors ${ + className={`flex w-full items-center gap-3 px-4 py-2.5 text-left transition-colors ${ index === slashSelectedIndex ? 'bg-[var(--color-surface-hover)]' : 'hover:bg-[var(--color-surface-hover)]' }`} > -
- - /{command.name} - -
- + + /{command.name} + + {command.description} @@ -465,32 +482,50 @@ export function ChatInput() { )} {attachments.length > 0 && ( -
+ isHeroComposer ? ( -
+ ) : ( +
+ +
+ ) )} -