diff --git a/desktop/package.json b/desktop/package.json
index 22c71ce7..25557b6b 100644
--- a/desktop/package.json
+++ b/desktop/package.json
@@ -1,7 +1,7 @@
{
"name": "claude-code-desktop",
"private": true,
- "version": "0.1.2",
+ "version": "0.1.3",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml
index 5d5c8b26..ef390f63 100644
--- a/desktop/src-tauri/Cargo.toml
+++ b/desktop/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "claude-code-desktop"
-version = "0.1.2"
+version = "0.1.3"
edition = "2021"
[lib]
diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json
index 20269144..861fb7fe 100644
--- a/desktop/src-tauri/tauri.conf.json
+++ b/desktop/src-tauri/tauri.conf.json
@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/nicegui/nicegui/main/nicegui/static/tauri-schema-v2.json",
"productName": "Claude Code Haha",
- "version": "0.1.2",
+ "version": "0.1.3",
"identifier": "com.claude-code-haha.desktop",
"build": {
"frontendDist": "../dist",
diff --git a/desktop/src/__tests__/pages.test.tsx b/desktop/src/__tests__/pages.test.tsx
index bef36c86..ee04748b 100644
--- a/desktop/src/__tests__/pages.test.tsx
+++ b/desktop/src/__tests__/pages.test.tsx
@@ -1,7 +1,15 @@
-import { describe, it, expect } from 'vitest'
+import { describe, it, expect, vi } from 'vitest'
import { fireEvent, render, screen } from '@testing-library/react'
import '@testing-library/jest-dom'
+import { skillsApi } from '../api/skills'
+
+vi.mock('../api/skills', () => ({
+ skillsApi: {
+ list: vi.fn(async () => ({ skills: [] })),
+ },
+}))
+
// Import all pages
import { EmptySession } from '../pages/EmptySession'
import { ActiveSession } from '../pages/ActiveSession'
@@ -21,6 +29,38 @@ import { useTabStore } from '../stores/tabStore'
* and contain key structural elements from the prototype.
*/
describe('Content-only pages render without errors', () => {
+ it('EmptySession slash picker includes dynamic skills before the first session starts', async () => {
+ vi.mocked(skillsApi.list).mockResolvedValueOnce({
+ skills: [
+ {
+ name: 'lark-mail',
+ description: 'Draft, send, and search emails',
+ source: 'user',
+ userInvocable: true,
+ contentLength: 120,
+ hasDirectory: true,
+ },
+ {
+ name: 'internal-only',
+ description: 'Should stay hidden',
+ source: 'user',
+ userInvocable: false,
+ contentLength: 60,
+ hasDirectory: true,
+ },
+ ],
+ })
+
+ render()
+
+ fireEvent.change(screen.getByRole('textbox'), {
+ target: { value: '/', selectionStart: 1 },
+ })
+
+ expect(await screen.findByText('/lark-mail')).toBeInTheDocument()
+ expect(screen.queryByText('/internal-only')).not.toBeInTheDocument()
+ })
+
it('EmptySession renders mascot and composer', () => {
const { container } = render()
expect(container.querySelector('textarea')).toBeInTheDocument()
diff --git a/desktop/src/pages/EmptySession.tsx b/desktop/src/pages/EmptySession.tsx
index e54fe5a6..58bf0403 100644
--- a/desktop/src/pages/EmptySession.tsx
+++ b/desktop/src/pages/EmptySession.tsx
@@ -1,4 +1,5 @@
-import { useEffect, useRef, useState } from 'react'
+import { useEffect, useMemo, useRef, useState } from 'react'
+import { skillsApi } from '../api/skills'
import { useTranslation } from '../i18n'
import { useSessionStore } from '../stores/sessionStore'
import { useChatStore } from '../stores/chatStore'
@@ -13,9 +14,11 @@ import {
FALLBACK_SLASH_COMMANDS,
findSlashToken,
insertSlashTrigger,
+ mergeSlashCommands,
replaceSlashCommand,
} from '../components/chat/composerUtils'
import type { AttachmentRef } from '../types/chat'
+import type { SlashCommandOption } from '../components/chat/composerUtils'
type Attachment = {
id: string
@@ -39,6 +42,7 @@ export function EmptySession() {
const [atCursorPos, setAtCursorPos] = useState(-1)
const [slashFilter, setSlashFilter] = useState('')
const [slashSelectedIndex, setSlashSelectedIndex] = useState(0)
+ const [slashCommands, setSlashCommands] = useState([])
const textareaRef = useRef(null)
const fileInputRef = useRef(null)
const plusMenuRef = useRef(null)
@@ -99,19 +103,50 @@ export function EmptySession() {
return () => document.removeEventListener('mousedown', handleClick)
}, [fileSearchOpen])
- const filteredCommands = FALLBACK_SLASH_COMMANDS.filter((command) => {
- if (!slashFilter) return true
+ useEffect(() => {
+ let cancelled = false
+
+ skillsApi.list(workDir || undefined)
+ .then(({ skills }) => {
+ if (cancelled) return
+ setSlashCommands(
+ skills
+ .filter((skill) => skill.userInvocable)
+ .map((skill) => ({
+ name: skill.name,
+ description: skill.description,
+ })),
+ )
+ })
+ .catch(() => {
+ if (!cancelled) {
+ setSlashCommands([])
+ }
+ })
+
+ return () => {
+ cancelled = true
+ }
+ }, [workDir])
+
+ const filteredCommands = useMemo(() => {
+ const source = mergeSlashCommands(slashCommands, FALLBACK_SLASH_COMMANDS)
+ if (!slashFilter) return source
const lower = slashFilter.toLowerCase()
- return command.name.toLowerCase().includes(lower) || command.description.toLowerCase().includes(lower)
- })
+ return source.filter((command) => (
+ command.name.toLowerCase().includes(lower) ||
+ command.description.toLowerCase().includes(lower)
+ ))
+ }, [slashCommands, slashFilter])
useEffect(() => {
setSlashSelectedIndex(0)
}, [slashFilter])
useEffect(() => {
- if (slashMenuOpen && slashItemRefs.current[slashSelectedIndex]) {
- slashItemRefs.current[slashSelectedIndex]?.scrollIntoView({ block: 'nearest' })
+ const activeItem = slashMenuOpen ? slashItemRefs.current[slashSelectedIndex] : null
+ if (typeof activeItem?.scrollIntoView === 'function') {
+ activeItem.scrollIntoView({ block: 'nearest' })
}
}, [slashMenuOpen, slashSelectedIndex])