mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
Cut desktop release 0.1.3 with empty-session slash discovery
The empty-session composer now loads user and project slash commands before the first turn instead of falling back to the built-in list, which keeps the packaged desktop app aligned with the web UI. This commit also bumps the desktop release version metadata to 0.1.3 so the release workflow publishes the correct artifacts. Constraint: Desktop release automation is triggered by semantic version tags and reads the version from desktop release metadata Rejected: Tagging v0.1.3 without a release commit | would publish stale 0.1.2 metadata and miss the slash-command fix Confidence: high Scope-risk: narrow Directive: Keep EmptySession slash-command loading aligned with ChatInput so packaged and web entry flows do not diverge again Tested: cd desktop && bun run test -- --run src/__tests__/pages.test.tsx Tested: cd desktop && bun run lint Tested: cd desktop && bun run build Not-tested: Full GitHub Actions release-desktop workflow after tag push
This commit is contained in:
parent
921a352517
commit
efcf7af988
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "claude-code-desktop",
|
||||
"private": true,
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "claude-code-desktop"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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(<EmptySession />)
|
||||
|
||||
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(<EmptySession />)
|
||||
expect(container.querySelector('textarea')).toBeInTheDocument()
|
||||
|
||||
@ -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<SlashCommandOption[]>([])
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const plusMenuRef = useRef<HTMLDivElement>(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])
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user