mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
fix(desktop): remove install center
This commit is contained in:
parent
761031c036
commit
928be2a4ff
@ -132,13 +132,13 @@ describe('Settings > General tab', () => {
|
||||
expect(useSettingsStore.getState().setSkipWebFetchPreflight).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('keeps install and extension tabs available after removing the embedded terminal tab', () => {
|
||||
it('keeps extension tabs available alongside the terminal tab', () => {
|
||||
render(<Settings />)
|
||||
|
||||
expect(screen.getByText('Install')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Install')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('Terminal')).toBeInTheDocument()
|
||||
expect(screen.getByText('MCP')).toBeInTheDocument()
|
||||
expect(screen.getByText('Plugins')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Terminal')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -50,7 +50,8 @@ async function request<T>(method: string, path: string, body?: unknown, options?
|
||||
}
|
||||
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), options?.timeout ?? 30_000)
|
||||
const timeoutMs = options?.timeout ?? 30_000
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs)
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
@ -69,6 +70,9 @@ async function request<T>(method: string, path: string, body?: unknown, options?
|
||||
return res.json() as Promise<T>
|
||||
} catch (err) {
|
||||
clearTimeout(timeout)
|
||||
if (controller.signal.aborted) {
|
||||
throw new Error(`Request timed out after ${Math.round(timeoutMs / 1000)}s`)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,6 +38,10 @@ export const pluginsApi = {
|
||||
|
||||
reload: (cwd?: string) => {
|
||||
const query = cwd ? `?cwd=${encodeURIComponent(cwd)}` : ''
|
||||
return api.post<{ ok: true; summary: PluginReloadSummary }>(`/api/plugins/reload${query}`)
|
||||
return api.post<{ ok: true; summary: PluginReloadSummary }>(
|
||||
`/api/plugins/reload${query}`,
|
||||
undefined,
|
||||
{ timeout: 120_000 },
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import type { SkillMeta, SkillDetail } from '../types/skill'
|
||||
export const skillsApi = {
|
||||
list: (cwd?: string) => {
|
||||
const query = cwd ? `?cwd=${encodeURIComponent(cwd)}` : ''
|
||||
return api.get<{ skills: SkillMeta[] }>(`/api/skills${query}`)
|
||||
return api.get<{ skills: SkillMeta[] }>(`/api/skills${query}`, { timeout: 120_000 })
|
||||
},
|
||||
|
||||
detail: (source: string, name: string, cwd?: string) => {
|
||||
@ -14,6 +14,9 @@ export const skillsApi = {
|
||||
})
|
||||
if (cwd) query.set('cwd', cwd)
|
||||
|
||||
return api.get<{ detail: SkillDetail }>(`/api/skills/detail?${query.toString()}`)
|
||||
return api.get<{ detail: SkillDetail }>(
|
||||
`/api/skills/detail?${query.toString()}`,
|
||||
{ timeout: 120_000 },
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,235 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
import type { SessionListItem } from '../../types/session'
|
||||
|
||||
const sessionStoreState: {
|
||||
sessions: SessionListItem[]
|
||||
fetchSessions: ReturnType<typeof vi.fn>
|
||||
} = {
|
||||
sessions: [],
|
||||
fetchSessions: vi.fn(),
|
||||
}
|
||||
|
||||
const chatStoreState: {
|
||||
sessions: Record<string, { chatState: 'idle'; pendingComputerUsePermission: { request: unknown } | null }>
|
||||
connectToSession: ReturnType<typeof vi.fn>
|
||||
disconnectSession: ReturnType<typeof vi.fn>
|
||||
sendMessage: ReturnType<typeof vi.fn>
|
||||
stopGeneration: ReturnType<typeof vi.fn>
|
||||
} = {
|
||||
sessions: {},
|
||||
connectToSession: vi.fn(),
|
||||
disconnectSession: vi.fn(),
|
||||
sendMessage: vi.fn(),
|
||||
stopGeneration: vi.fn(),
|
||||
}
|
||||
|
||||
const pluginStoreState = {
|
||||
fetchPlugins: vi.fn(),
|
||||
reloadPlugins: vi.fn(),
|
||||
}
|
||||
|
||||
const skillStoreState = {
|
||||
fetchSkills: vi.fn(),
|
||||
}
|
||||
|
||||
const mcpStoreState = {
|
||||
fetchServers: vi.fn(),
|
||||
}
|
||||
|
||||
const uiStoreState = {
|
||||
addToast: vi.fn(),
|
||||
setPendingSettingsTab: vi.fn(),
|
||||
}
|
||||
|
||||
const { settingsApiState } = vi.hoisted(() => ({
|
||||
settingsApiState: {
|
||||
getCliLauncherStatus: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../../stores/sessionStore', () => ({
|
||||
useSessionStore: (selector: (state: typeof sessionStoreState) => unknown) =>
|
||||
selector(sessionStoreState),
|
||||
}))
|
||||
|
||||
vi.mock('../../stores/chatStore', () => ({
|
||||
useChatStore: (selector: (state: typeof chatStoreState) => unknown) =>
|
||||
selector(chatStoreState),
|
||||
}))
|
||||
|
||||
vi.mock('../../stores/pluginStore', () => ({
|
||||
usePluginStore: (selector: (state: typeof pluginStoreState) => unknown) =>
|
||||
selector(pluginStoreState),
|
||||
}))
|
||||
|
||||
vi.mock('../../stores/skillStore', () => ({
|
||||
useSkillStore: (selector: (state: typeof skillStoreState) => unknown) =>
|
||||
selector(skillStoreState),
|
||||
}))
|
||||
|
||||
vi.mock('../../stores/mcpStore', () => ({
|
||||
useMcpStore: (selector: (state: typeof mcpStoreState) => unknown) =>
|
||||
selector(mcpStoreState),
|
||||
}))
|
||||
|
||||
vi.mock('../../stores/uiStore', () => ({
|
||||
useUIStore: (selector: (state: typeof uiStoreState) => unknown) =>
|
||||
selector(uiStoreState),
|
||||
}))
|
||||
|
||||
vi.mock('../../api/settings', () => ({
|
||||
settingsApi: settingsApiState,
|
||||
}))
|
||||
|
||||
vi.mock('../../i18n', () => {
|
||||
const translations: Record<string, string> = {
|
||||
'settings.install.eyebrow': 'AI 安装助手',
|
||||
'settings.install.title': '安装中心',
|
||||
'settings.install.description': '安装中心描述',
|
||||
'settings.install.targets.plugins': 'Plugins',
|
||||
'settings.install.targets.mcp': 'MCP',
|
||||
'settings.install.targets.skills': 'Skills',
|
||||
'settings.install.contextAuto': '默认上下文',
|
||||
'settings.install.composeTitle': '自然语言安装',
|
||||
'settings.install.composeHint': '安装提示',
|
||||
'settings.install.refresh': '刷新安装状态',
|
||||
'settings.install.newConversation': '新建安装会话',
|
||||
'settings.install.placeholder': '占位符',
|
||||
'settings.install.send': '发送安装请求',
|
||||
'settings.install.cliTitle': '内置 CLI 命令',
|
||||
'settings.install.cliDescription': 'CLI 描述',
|
||||
'settings.install.cliLoading': '正在检查内置 CLI launcher 状态…',
|
||||
'settings.install.cliReady': '当前终端已可直接使用',
|
||||
'settings.install.cliNeedsRestart': '已安装完成;请新开一个终端以加载 PATH 变更。',
|
||||
'settings.install.cliPathMissing': 'launcher 已安装,但 PATH 仍未完全就绪。',
|
||||
'settings.install.cliUnavailable': '内置 CLI launcher 还没有准备好。',
|
||||
'settings.install.cliLocation': 'CLI 路径',
|
||||
'settings.install.cliConfigTarget': 'PATH 集成目标:{target}',
|
||||
'settings.install.cliError': '内置 CLI 配置告警:{message}',
|
||||
'settings.install.cliSharedConfig': '后续如果你想直接通过命令行安装 Skills、Plugins 或 MCP:电脑上本身装了官方 Claude Code,就继续使用 `claude`;如果没有,就使用 `claude-haha`。这两条命令共用同一套 Skills / Plugins / MCP 配置。',
|
||||
'settings.install.cliUseOfficial': '如果你已经安装了官方 Claude Code,继续用原版命令:',
|
||||
'settings.install.cliUseBundled': '如果没有官方 CLI,就使用我们打包的 `{command}`:',
|
||||
'settings.install.contextDefault': '默认目录提示',
|
||||
'settings.install.contextUsing': '当前安装会话目录:{path}',
|
||||
'settings.install.contextTitle': '执行目录',
|
||||
'settings.install.contextHint': '执行目录提示',
|
||||
'settings.install.goPlugins': '查看插件',
|
||||
'settings.install.goMcp': '查看 MCP',
|
||||
'settings.install.goSkills': '查看技能',
|
||||
'settings.install.sessionTitle': '安装助手会话',
|
||||
'settings.install.sessionHint': '会话提示',
|
||||
'settings.install.sessionEmpty': '还没有安装会话',
|
||||
'settings.install.sessionEmptyHint': '空会话提示',
|
||||
'settings.install.clearConversation': '清除对话',
|
||||
'settings.install.clearConversationReady': '已清除当前安装对话;下一条请求会启动新的安装上下文。',
|
||||
'settings.install.newConversationReady': '已准备新的安装会话;下一条请求会启动新的安装上下文。',
|
||||
}
|
||||
|
||||
return {
|
||||
useTranslation: () => (
|
||||
key: string,
|
||||
params?: Record<string, string | number>,
|
||||
) => {
|
||||
let text = translations[key] ?? key
|
||||
if (params) {
|
||||
for (const [name, value] of Object.entries(params)) {
|
||||
text = text.replace(new RegExp(`\\{${name}\\}`, 'g'), String(value))
|
||||
}
|
||||
}
|
||||
return text
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../chat/MessageList', () => ({
|
||||
MessageList: ({ sessionId }: { sessionId: string }) => (
|
||||
<div data-testid="message-list">session:{sessionId}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../chat/ComputerUsePermissionModal', () => ({
|
||||
ComputerUsePermissionModal: () => null,
|
||||
}))
|
||||
|
||||
vi.mock('../shared/DirectoryPicker', () => ({
|
||||
DirectoryPicker: () => <div>Directory picker</div>,
|
||||
}))
|
||||
|
||||
import { InstallCenter } from './InstallCenter'
|
||||
|
||||
describe('InstallCenter', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
window.localStorage.clear()
|
||||
|
||||
sessionStoreState.sessions = [
|
||||
{
|
||||
id: 'installer-1',
|
||||
title: '安装助手会话',
|
||||
createdAt: '2026-04-23T00:00:00.000Z',
|
||||
modifiedAt: '2026-04-23T00:00:00.000Z',
|
||||
messageCount: 2,
|
||||
projectPath: '',
|
||||
workDir: '/Users/nanmi',
|
||||
workDirExists: true,
|
||||
},
|
||||
]
|
||||
sessionStoreState.fetchSessions = vi.fn()
|
||||
|
||||
chatStoreState.sessions = {
|
||||
'installer-1': {
|
||||
chatState: 'idle',
|
||||
pendingComputerUsePermission: null,
|
||||
},
|
||||
}
|
||||
chatStoreState.connectToSession = vi.fn()
|
||||
chatStoreState.disconnectSession = vi.fn()
|
||||
chatStoreState.sendMessage = vi.fn()
|
||||
chatStoreState.stopGeneration = vi.fn()
|
||||
|
||||
pluginStoreState.fetchPlugins = vi.fn()
|
||||
pluginStoreState.reloadPlugins = vi.fn()
|
||||
skillStoreState.fetchSkills = vi.fn()
|
||||
mcpStoreState.fetchServers = vi.fn()
|
||||
uiStoreState.addToast = vi.fn()
|
||||
uiStoreState.setPendingSettingsTab = vi.fn()
|
||||
settingsApiState.getCliLauncherStatus = vi.fn().mockResolvedValue({
|
||||
supported: true,
|
||||
command: 'claude-haha',
|
||||
installed: true,
|
||||
launcherPath: '/Users/nanmi/.local/bin/claude-haha',
|
||||
binDir: '/Users/nanmi/.local/bin',
|
||||
pathConfigured: true,
|
||||
pathInCurrentShell: false,
|
||||
availableInNewTerminals: true,
|
||||
needsTerminalRestart: true,
|
||||
configTarget: '/Users/nanmi/.zshrc',
|
||||
lastError: null,
|
||||
})
|
||||
|
||||
window.localStorage.setItem('cc-haha-installer-session-id', 'installer-1')
|
||||
window.localStorage.setItem('cc-haha-installer-context-dir', '/Users/nanmi')
|
||||
})
|
||||
|
||||
it('shows bundled cli launcher status', async () => {
|
||||
render(<InstallCenter />)
|
||||
|
||||
expect(await screen.findByText('claude-haha')).toBeInTheDocument()
|
||||
expect(screen.getByText('已安装完成;请新开一个终端以加载 PATH 变更。')).toBeInTheDocument()
|
||||
expect(screen.getByText(/共用同一套 Skills \/ Plugins \/ MCP 配置/)).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText(
|
||||
'claude plugin install skill-creator@claude-plugins-official --scope user',
|
||||
),
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText(
|
||||
'claude-haha mcp add docs --transport http https://example.com/mcp',
|
||||
),
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByTestId('message-list')).toHaveTextContent('session:installer-1')
|
||||
})
|
||||
})
|
||||
@ -1,590 +0,0 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { sessionsApi } from '../../api/sessions'
|
||||
import { settingsApi, type CliLauncherStatus } from '../../api/settings'
|
||||
import { useChatStore } from '../../stores/chatStore'
|
||||
import { useMcpStore } from '../../stores/mcpStore'
|
||||
import { usePluginStore } from '../../stores/pluginStore'
|
||||
import { useSessionStore } from '../../stores/sessionStore'
|
||||
import { useSkillStore } from '../../stores/skillStore'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { MessageList } from '../chat/MessageList'
|
||||
import { ComputerUsePermissionModal } from '../chat/ComputerUsePermissionModal'
|
||||
import { Button } from '../shared/Button'
|
||||
import { Textarea } from '../shared/Textarea'
|
||||
import { DirectoryPicker } from '../shared/DirectoryPicker'
|
||||
import { useUIStore } from '../../stores/uiStore'
|
||||
import { buildInstallerPrompt } from '../../lib/installAssistantPrompt'
|
||||
|
||||
const INSTALLER_SESSION_KEY = 'cc-haha-installer-session-id'
|
||||
const INSTALLER_CONTEXT_KEY = 'cc-haha-installer-context-dir'
|
||||
|
||||
const EXAMPLE_PROMPTS = [
|
||||
'安装 plugin:skill-creator@claude-plugins-official,并应用到当前桌面端',
|
||||
'添加一个 MCP:name=linear,url=https://example.com/mcp,优先用户级',
|
||||
'帮我把一个 GitHub 仓库里的 skill 装到 ~/.claude/skills,并告诉我装到了哪里',
|
||||
]
|
||||
|
||||
function readStoredValue(key: string) {
|
||||
try {
|
||||
return localStorage.getItem(key) || ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function writeStoredValue(key: string, value: string) {
|
||||
try {
|
||||
if (value) {
|
||||
localStorage.setItem(key, value)
|
||||
} else {
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
export function InstallCenter() {
|
||||
const t = useTranslation()
|
||||
const sessions = useSessionStore((s) => s.sessions)
|
||||
const fetchSessions = useSessionStore((s) => s.fetchSessions)
|
||||
const connectToSession = useChatStore((s) => s.connectToSession)
|
||||
const disconnectSession = useChatStore((s) => s.disconnectSession)
|
||||
const sendMessage = useChatStore((s) => s.sendMessage)
|
||||
const stopGeneration = useChatStore((s) => s.stopGeneration)
|
||||
const fetchPlugins = usePluginStore((s) => s.fetchPlugins)
|
||||
const reloadPlugins = usePluginStore((s) => s.reloadPlugins)
|
||||
const fetchSkills = useSkillStore((s) => s.fetchSkills)
|
||||
const fetchServers = useMcpStore((s) => s.fetchServers)
|
||||
const addToast = useUIStore((s) => s.addToast)
|
||||
const setPendingSettingsTab = useUIStore((s) => s.setPendingSettingsTab)
|
||||
|
||||
const [sessionId, setSessionId] = useState<string | null>(() => {
|
||||
const stored = readStoredValue(INSTALLER_SESSION_KEY)
|
||||
return stored || null
|
||||
})
|
||||
const [contextDir, setContextDir] = useState(() => readStoredValue(INSTALLER_CONTEXT_KEY))
|
||||
const [draft, setDraft] = useState('')
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const [cliLauncherStatus, setCliLauncherStatus] = useState<CliLauncherStatus | null>(null)
|
||||
const [isCliLauncherLoading, setIsCliLauncherLoading] = useState(true)
|
||||
const createPromiseRef = useRef<Promise<string> | null>(null)
|
||||
const previousChatStateRef = useRef<'idle' | 'thinking' | 'tool_executing' | 'streaming' | 'permission_pending'>('idle')
|
||||
|
||||
const installerSession = useMemo(
|
||||
() => sessions.find((session) => session.id === sessionId) || null,
|
||||
[sessionId, sessions],
|
||||
)
|
||||
const sessionState = useChatStore((s) =>
|
||||
sessionId ? s.sessions[sessionId] : undefined,
|
||||
)
|
||||
const chatState = sessionState?.chatState ?? 'idle'
|
||||
const pendingComputerUsePermission =
|
||||
sessionState?.pendingComputerUsePermission?.request ?? null
|
||||
const isBusy = isCreating || chatState !== 'idle'
|
||||
|
||||
useEffect(() => {
|
||||
if (!sessionId) return
|
||||
connectToSession(sessionId)
|
||||
return () => {
|
||||
disconnectSession(sessionId)
|
||||
}
|
||||
}, [connectToSession, disconnectSession, sessionId])
|
||||
|
||||
useEffect(() => {
|
||||
writeStoredValue(INSTALLER_CONTEXT_KEY, contextDir.trim())
|
||||
}, [contextDir])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
void (async () => {
|
||||
setIsCliLauncherLoading(true)
|
||||
try {
|
||||
const status = await settingsApi.getCliLauncherStatus()
|
||||
if (!cancelled) {
|
||||
setCliLauncherStatus(status)
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
setCliLauncherStatus(null)
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setIsCliLauncherLoading(false)
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!sessionId) return
|
||||
const previousState = previousChatStateRef.current
|
||||
previousChatStateRef.current = chatState
|
||||
|
||||
if (previousState === 'idle' || chatState !== 'idle') return
|
||||
|
||||
const cwd = installerSession?.workDir || undefined
|
||||
|
||||
void (async () => {
|
||||
await reloadPlugins(cwd).catch(() => null)
|
||||
await Promise.allSettled([
|
||||
fetchPlugins(cwd),
|
||||
fetchSkills(cwd),
|
||||
fetchServers(cwd ? [cwd] : undefined, cwd),
|
||||
])
|
||||
})()
|
||||
}, [
|
||||
chatState,
|
||||
fetchPlugins,
|
||||
fetchServers,
|
||||
fetchSkills,
|
||||
installerSession?.workDir,
|
||||
reloadPlugins,
|
||||
sessionId,
|
||||
])
|
||||
|
||||
const ensureInstallerSession = async () => {
|
||||
if (sessionId && installerSession) {
|
||||
return sessionId
|
||||
}
|
||||
|
||||
if (createPromiseRef.current) {
|
||||
return createPromiseRef.current
|
||||
}
|
||||
|
||||
setIsCreating(true)
|
||||
createPromiseRef.current = (async () => {
|
||||
const { sessionId: createdSessionId } = await sessionsApi.create(
|
||||
contextDir.trim() || undefined,
|
||||
)
|
||||
await sessionsApi.rename(
|
||||
createdSessionId,
|
||||
t('settings.install.sessionTitle'),
|
||||
)
|
||||
writeStoredValue(INSTALLER_SESSION_KEY, createdSessionId)
|
||||
setSessionId(createdSessionId)
|
||||
await fetchSessions()
|
||||
connectToSession(createdSessionId)
|
||||
return createdSessionId
|
||||
})()
|
||||
|
||||
try {
|
||||
return await createPromiseRef.current
|
||||
} finally {
|
||||
createPromiseRef.current = null
|
||||
setIsCreating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const request = draft.trim()
|
||||
if (!request || isBusy) return
|
||||
|
||||
try {
|
||||
const ensuredSessionId = await ensureInstallerSession()
|
||||
sendMessage(
|
||||
ensuredSessionId,
|
||||
buildInstallerPrompt(request),
|
||||
undefined,
|
||||
{ displayContent: request },
|
||||
)
|
||||
setDraft('')
|
||||
} catch (error) {
|
||||
addToast({
|
||||
type: 'error',
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t('settings.install.createFailed'),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = async () => {
|
||||
const cwd = installerSession?.workDir || undefined
|
||||
const [launcherStatus] = await Promise.all([
|
||||
settingsApi.getCliLauncherStatus().catch(() => null),
|
||||
fetchPlugins(cwd),
|
||||
fetchSkills(cwd),
|
||||
fetchServers(cwd ? [cwd] : undefined, cwd),
|
||||
])
|
||||
if (launcherStatus) {
|
||||
setCliLauncherStatus(launcherStatus)
|
||||
}
|
||||
addToast({
|
||||
type: 'success',
|
||||
message: t('settings.install.refreshDone'),
|
||||
})
|
||||
}
|
||||
|
||||
const startFreshConversation = async () => {
|
||||
if (sessionId) {
|
||||
disconnectSession(sessionId)
|
||||
}
|
||||
writeStoredValue(INSTALLER_SESSION_KEY, '')
|
||||
setSessionId(null)
|
||||
previousChatStateRef.current = 'idle'
|
||||
addToast({
|
||||
type: 'info',
|
||||
message: t('settings.install.newConversationReady'),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full min-w-0">
|
||||
<section className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface-container-low)] overflow-hidden">
|
||||
<div className="grid gap-4 px-5 py-5 xl:grid-cols-[minmax(0,1.5fr)_minmax(280px,1fr)] xl:items-start">
|
||||
<div className="min-w-0">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.2em] text-[var(--color-text-tertiary)] mb-2">
|
||||
{t('settings.install.eyebrow')}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className="material-symbols-outlined text-[22px] text-[var(--color-brand)]">
|
||||
download
|
||||
</span>
|
||||
<h2 className="text-lg font-semibold text-[var(--color-text-primary)]">
|
||||
{t('settings.install.title')}
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm leading-6 text-[var(--color-text-secondary)] max-w-3xl">
|
||||
{t('settings.install.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4 xl:grid-cols-2">
|
||||
<SummaryPill
|
||||
label={t('settings.install.targets.plugins')}
|
||||
icon="extension"
|
||||
/>
|
||||
<SummaryPill
|
||||
label={t('settings.install.targets.mcp')}
|
||||
icon="hub"
|
||||
/>
|
||||
<SummaryPill
|
||||
label={t('settings.install.targets.skills')}
|
||||
icon="auto_awesome"
|
||||
/>
|
||||
<SummaryPill
|
||||
label={installerSession?.workDir || t('settings.install.contextAuto')}
|
||||
icon="folder"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{(isCliLauncherLoading || cliLauncherStatus?.supported) && (
|
||||
<section className="mt-6 rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] p-5">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-[var(--color-text-primary)]">
|
||||
{t('settings.install.cliTitle')}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--color-text-tertiary)] mt-1">
|
||||
{t('settings.install.cliDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-full border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-3 py-1 text-sm font-semibold text-[var(--color-text-primary)]">
|
||||
{cliLauncherStatus?.command || 'claude-haha'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface-container-low)] p-4">
|
||||
{isCliLauncherLoading ? (
|
||||
<p className="text-sm text-[var(--color-text-secondary)]">
|
||||
{t('settings.install.cliLoading')}
|
||||
</p>
|
||||
) : cliLauncherStatus ? (
|
||||
<CliLauncherStatusPanel status={cliLauncherStatus} />
|
||||
) : (
|
||||
<p className="text-sm text-[var(--color-danger)]">
|
||||
{t('settings.install.cliUnavailable')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="mt-6 rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] p-5">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-[var(--color-text-primary)]">
|
||||
{t('settings.install.composeTitle')}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--color-text-tertiary)] mt-1">
|
||||
{t('settings.install.composeHint')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => void handleRefresh()}
|
||||
>
|
||||
{t('settings.install.refresh')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => void startFreshConversation()}
|
||||
>
|
||||
{t('settings.install.newConversation')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid gap-4 xl:grid-cols-[minmax(0,1fr)_320px]">
|
||||
<div className="min-w-0">
|
||||
<Textarea
|
||||
value={draft}
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
void handleSubmit()
|
||||
}
|
||||
}}
|
||||
placeholder={t('settings.install.placeholder')}
|
||||
className="min-h-[140px]"
|
||||
/>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||||
{EXAMPLE_PROMPTS.map((example) => (
|
||||
<button
|
||||
key={example}
|
||||
type="button"
|
||||
onClick={() => setDraft(example)}
|
||||
className="rounded-full border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-3 py-1.5 text-xs text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)]"
|
||||
>
|
||||
{example}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap items-center gap-2">
|
||||
{chatState !== 'idle' && sessionId ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
onClick={() => stopGeneration(sessionId)}
|
||||
>
|
||||
{t('settings.install.stop')}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => void handleSubmit()}
|
||||
loading={isCreating}
|
||||
disabled={!draft.trim() || isBusy}
|
||||
icon={
|
||||
!isCreating ? (
|
||||
<span className="material-symbols-outlined text-[16px]">send</span>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{t('settings.install.send')}
|
||||
</Button>
|
||||
<span className="text-xs text-[var(--color-text-tertiary)]">
|
||||
{installerSession?.workDir
|
||||
? t('settings.install.contextUsing', {
|
||||
path: installerSession.workDir,
|
||||
})
|
||||
: t('settings.install.contextDefault')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-[var(--color-border)] bg-[var(--color-surface-container-low)] p-4">
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-[var(--color-text-tertiary)]">
|
||||
{t('settings.install.contextTitle')}
|
||||
</div>
|
||||
<p className="mt-2 text-xs leading-5 text-[var(--color-text-tertiary)]">
|
||||
{t('settings.install.contextHint')}
|
||||
</p>
|
||||
<div className="mt-3">
|
||||
<DirectoryPicker value={contextDir} onChange={setContextDir} />
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setPendingSettingsTab('plugins')}
|
||||
>
|
||||
{t('settings.install.goPlugins')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setPendingSettingsTab('mcp')}
|
||||
>
|
||||
{t('settings.install.goMcp')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setPendingSettingsTab('skills')}
|
||||
>
|
||||
{t('settings.install.goSkills')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-6 rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] overflow-hidden">
|
||||
<div className="flex items-center justify-between gap-3 border-b border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-5 py-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-[var(--color-text-primary)]">
|
||||
{t('settings.install.sessionTitle')}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--color-text-tertiary)] mt-1">
|
||||
{sessionId
|
||||
? t('settings.install.sessionHint')
|
||||
: t('settings.install.sessionEmpty')}
|
||||
</p>
|
||||
</div>
|
||||
{sessionId ? (
|
||||
<span className="rounded-full border border-[var(--color-border)] bg-[var(--color-surface)] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-[var(--color-text-tertiary)]">
|
||||
{chatState}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="min-h-[420px] bg-[var(--color-surface-container-lowest)]">
|
||||
{sessionId ? (
|
||||
<>
|
||||
<MessageList sessionId={sessionId} />
|
||||
<ComputerUsePermissionModal
|
||||
sessionId={sessionId}
|
||||
request={pendingComputerUsePermission}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-[420px] flex-col items-center justify-center px-6 text-center">
|
||||
<span className="material-symbols-outlined text-[36px] text-[var(--color-text-tertiary)] mb-3">
|
||||
forum
|
||||
</span>
|
||||
<p className="text-sm text-[var(--color-text-secondary)]">
|
||||
{t('settings.install.sessionEmpty')}
|
||||
</p>
|
||||
<p className="mt-2 max-w-md text-xs leading-6 text-[var(--color-text-tertiary)]">
|
||||
{t('settings.install.sessionEmptyHint')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CliLauncherStatusPanel({ status }: { status: CliLauncherStatus }) {
|
||||
const t = useTranslation()
|
||||
const bundledCommand = status.command || 'claude-haha'
|
||||
const exampleCommands = [
|
||||
{
|
||||
label: t('settings.install.cliUseOfficial'),
|
||||
command:
|
||||
'claude plugin install skill-creator@claude-plugins-official --scope user',
|
||||
},
|
||||
{
|
||||
label: t('settings.install.cliUseBundled', {
|
||||
command: bundledCommand,
|
||||
}),
|
||||
command: `${bundledCommand} plugin install skill-creator@claude-plugins-official --scope user`,
|
||||
},
|
||||
{
|
||||
label: t('settings.install.cliUseBundled', {
|
||||
command: bundledCommand,
|
||||
}),
|
||||
command: `${bundledCommand} mcp add docs --transport http https://example.com/mcp`,
|
||||
},
|
||||
]
|
||||
|
||||
let statusText = t('settings.install.cliUnavailable')
|
||||
let statusClassName = 'border-[var(--color-danger)]/25 bg-[var(--color-danger)]/10 text-[var(--color-danger)]'
|
||||
|
||||
if (status.installed && status.availableInNewTerminals) {
|
||||
if (status.needsTerminalRestart) {
|
||||
statusText = t('settings.install.cliNeedsRestart')
|
||||
statusClassName = 'border-[var(--color-warning)]/25 bg-[var(--color-warning)]/10 text-[var(--color-warning)]'
|
||||
} else {
|
||||
statusText = t('settings.install.cliReady')
|
||||
statusClassName = 'border-[var(--color-success)]/25 bg-[var(--color-success)]/10 text-[var(--color-success)]'
|
||||
}
|
||||
} else if (status.installed) {
|
||||
statusText = t('settings.install.cliPathMissing')
|
||||
statusClassName = 'border-[var(--color-warning)]/25 bg-[var(--color-warning)]/10 text-[var(--color-warning)]'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<span
|
||||
className={`inline-flex rounded-full border px-2.5 py-1 text-xs font-semibold ${statusClassName}`}
|
||||
>
|
||||
{statusText}
|
||||
</span>
|
||||
<span className="text-xs text-[var(--color-text-tertiary)]">
|
||||
{t('settings.install.cliLocation')}
|
||||
</span>
|
||||
<span className="font-mono text-xs text-[var(--color-text-primary)] break-all">
|
||||
{status.launcherPath}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{status.configTarget && (
|
||||
<p className="text-xs text-[var(--color-text-tertiary)]">
|
||||
{t('settings.install.cliConfigTarget', {
|
||||
target: status.configTarget,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{status.lastError && (
|
||||
<p className="text-xs text-[var(--color-danger)]">
|
||||
{t('settings.install.cliError', {
|
||||
message: status.lastError,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="rounded-xl border border-[var(--color-border)] bg-[var(--color-surface)] p-3">
|
||||
<p className="text-xs leading-6 text-[var(--color-text-secondary)]">
|
||||
{t('settings.install.cliSharedConfig')}
|
||||
</p>
|
||||
<div className="mt-3 space-y-2">
|
||||
{exampleCommands.map((example) => (
|
||||
<div key={example.command} className="space-y-1">
|
||||
<p className="text-[11px] text-[var(--color-text-tertiary)]">
|
||||
{example.label}
|
||||
</p>
|
||||
<div className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-3 py-2 font-mono text-xs text-[var(--color-text-primary)] break-all">
|
||||
{example.command}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SummaryPill({
|
||||
label,
|
||||
icon,
|
||||
}: {
|
||||
label: string
|
||||
icon: string
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-xl border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-3 min-w-0">
|
||||
<div className="flex items-center gap-1.5 text-[11px] uppercase tracking-[0.12em] text-[var(--color-text-tertiary)] min-w-0">
|
||||
<span className="material-symbols-outlined text-[14px] flex-shrink-0">{icon}</span>
|
||||
<span className="truncate">{label}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -51,7 +51,6 @@ export const en = {
|
||||
'settings.tab.providers': 'Providers',
|
||||
'settings.tab.permissions': 'Permissions',
|
||||
'settings.tab.general': 'General',
|
||||
'settings.tab.install': 'Install',
|
||||
'settings.tab.terminal': 'Terminal',
|
||||
'settings.tab.skills': 'Skills',
|
||||
'settings.tab.mcp': 'MCP',
|
||||
@ -309,51 +308,6 @@ export const en = {
|
||||
'settings.agents.source.flagSettings': 'CLI arg',
|
||||
'settings.agents.source.built-in': 'Built-in',
|
||||
|
||||
// Settings > Install Center
|
||||
'settings.install.eyebrow': 'AI Installer',
|
||||
'settings.install.title': 'Install Center',
|
||||
'settings.install.description': 'Describe the plugin, MCP server, or skill you want. The desktop app will reuse a dedicated session to execute the installation flow and then sync results back to the existing management pages.',
|
||||
'settings.install.targets.plugins': 'Plugins',
|
||||
'settings.install.targets.mcp': 'MCP',
|
||||
'settings.install.targets.skills': 'Skills',
|
||||
'settings.install.contextAuto': 'Default context',
|
||||
'settings.install.composeTitle': 'Natural language install',
|
||||
'settings.install.composeHint': 'Describe what to install, where it comes from, and whether it should land in user or project scope.',
|
||||
'settings.install.placeholder': 'Example: install plugin skill-creator@claude-plugins-official and apply it to the current desktop runtime. Or: add a user-scope MCP at https://example.com/mcp.',
|
||||
'settings.install.send': 'Send request',
|
||||
'settings.install.stop': 'Stop',
|
||||
'settings.install.refresh': 'Refresh state',
|
||||
'settings.install.newConversation': 'New install chat',
|
||||
'settings.install.clearConversation': 'Clear chat',
|
||||
'settings.install.cliTitle': 'Bundled CLI command',
|
||||
'settings.install.cliDescription': 'The desktop app installs a terminal command named `claude-haha` so system shells can use the bundled CLI even when the official Claude CLI is not installed.',
|
||||
'settings.install.cliLoading': 'Checking bundled CLI launcher status…',
|
||||
'settings.install.cliReady': 'Ready in current terminals',
|
||||
'settings.install.cliNeedsRestart': 'Installed. Open a new terminal to pick up PATH changes.',
|
||||
'settings.install.cliPathMissing': 'Installed, but PATH still needs attention.',
|
||||
'settings.install.cliUnavailable': 'Bundled CLI launcher is not ready yet.',
|
||||
'settings.install.cliLocation': 'CLI path',
|
||||
'settings.install.cliConfigTarget': 'PATH integration target: {target}',
|
||||
'settings.install.cliError': 'Bundled CLI setup warning: {message}',
|
||||
'settings.install.cliSharedConfig': 'For direct terminal installs later, keep using `claude` if the official Claude Code CLI is already installed. Otherwise use `claude-haha`. Both command paths share the same Skills / Plugins / MCP state.',
|
||||
'settings.install.cliUseOfficial': 'If the official Claude Code CLI is already installed, keep using it:',
|
||||
'settings.install.cliUseBundled': 'If not, use our bundled `{command}` command:',
|
||||
'settings.install.contextDefault': 'If no directory is selected, the installer session starts from your home directory.',
|
||||
'settings.install.contextUsing': 'Installer session directory: {path}',
|
||||
'settings.install.contextTitle': 'Execution directory',
|
||||
'settings.install.contextHint': 'Most user-scope installs do not need a project path. If you need project skills or local/project MCP config, pick a directory here and then start a new install chat.',
|
||||
'settings.install.goPlugins': 'Open Plugins',
|
||||
'settings.install.goMcp': 'Open MCP',
|
||||
'settings.install.goSkills': 'Open Skills',
|
||||
'settings.install.sessionTitle': 'Installer session',
|
||||
'settings.install.sessionHint': 'This panel shows the full installer session, including tool calls and results.',
|
||||
'settings.install.sessionEmpty': 'No installer session yet',
|
||||
'settings.install.sessionEmptyHint': 'After you send the first installation request, the app will reuse the session chat pipeline and show the full execution trace here.',
|
||||
'settings.install.createFailed': 'Failed to create installer session',
|
||||
'settings.install.refreshDone': 'Plugin, MCP, and skill state refreshed',
|
||||
'settings.install.newConversationReady': 'A fresh installer conversation is ready; your next request will start a new install context.',
|
||||
'settings.install.clearConversationReady': 'Installer chat cleared. Your next request will start a fresh install context.',
|
||||
|
||||
// Settings > Skills
|
||||
'settings.skills.title': 'Installed Skills',
|
||||
'settings.skills.description': 'Skills extend Claude with specialized capabilities. Manage skills in ~/.claude/skills/',
|
||||
|
||||
@ -53,7 +53,6 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'settings.tab.providers': '服务商',
|
||||
'settings.tab.permissions': '权限',
|
||||
'settings.tab.general': '通用',
|
||||
'settings.tab.install': '安装中心',
|
||||
'settings.tab.terminal': '终端',
|
||||
'settings.tab.skills': '技能',
|
||||
'settings.tab.mcp': 'MCP',
|
||||
@ -311,51 +310,6 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'settings.agents.source.flagSettings': 'CLI 参数',
|
||||
'settings.agents.source.built-in': '内置',
|
||||
|
||||
// Settings > Install Center
|
||||
'settings.install.eyebrow': 'AI 安装助手',
|
||||
'settings.install.title': '安装中心',
|
||||
'settings.install.description': '直接描述你想安装的 plugin、MCP 或 skill。底层会复用安装专用 session 去执行,并把结果同步回现有管理页。',
|
||||
'settings.install.targets.plugins': 'Plugins',
|
||||
'settings.install.targets.mcp': 'MCP',
|
||||
'settings.install.targets.skills': 'Skills',
|
||||
'settings.install.contextAuto': '默认上下文',
|
||||
'settings.install.composeTitle': '自然语言安装',
|
||||
'settings.install.composeHint': '描述你要安装什么、来源在哪里、希望落到用户级还是项目级。',
|
||||
'settings.install.placeholder': '例如:安装 plugin skill-creator@claude-plugins-official,并帮我应用到当前桌面端;或者:添加一个 user scope 的 MCP,地址是 https://example.com/mcp。',
|
||||
'settings.install.send': '发送安装请求',
|
||||
'settings.install.stop': '中断执行',
|
||||
'settings.install.refresh': '刷新安装状态',
|
||||
'settings.install.newConversation': '新建安装会话',
|
||||
'settings.install.clearConversation': '清除对话',
|
||||
'settings.install.cliTitle': '内置 CLI 命令',
|
||||
'settings.install.cliDescription': '桌面端会把打包进去的 CLI 暴露成系统终端命令 `claude-haha`,即使用户没有安装官方 Claude CLI 也能直接调用。',
|
||||
'settings.install.cliLoading': '正在检查内置 CLI launcher 状态…',
|
||||
'settings.install.cliReady': '当前终端已可直接使用',
|
||||
'settings.install.cliNeedsRestart': '已安装完成;请新开一个终端以加载 PATH 变更。',
|
||||
'settings.install.cliPathMissing': 'launcher 已安装,但 PATH 仍未完全就绪。',
|
||||
'settings.install.cliUnavailable': '内置 CLI launcher 还没有准备好。',
|
||||
'settings.install.cliLocation': 'CLI 路径',
|
||||
'settings.install.cliConfigTarget': 'PATH 集成目标:{target}',
|
||||
'settings.install.cliError': '内置 CLI 配置告警:{message}',
|
||||
'settings.install.cliSharedConfig': '后续如果你想直接通过命令行安装 Skills、Plugins 或 MCP:电脑上本身装了官方 Claude Code,就继续使用 `claude`;如果没有,就使用 `claude-haha`。这两条命令共用同一套 Skills / Plugins / MCP 配置。',
|
||||
'settings.install.cliUseOfficial': '如果你已经安装了官方 Claude Code,继续用原版命令:',
|
||||
'settings.install.cliUseBundled': '如果没有官方 CLI,就使用我们打包的 `{command}`:',
|
||||
'settings.install.contextDefault': '未指定目录时默认使用 Home 目录启动安装会话。',
|
||||
'settings.install.contextUsing': '当前安装会话目录:{path}',
|
||||
'settings.install.contextTitle': '执行目录',
|
||||
'settings.install.contextHint': '大部分用户级安装不依赖项目目录;如果你要安装 project skill 或 local/project MCP,可以先在这里选目录,再开始新的安装会话。',
|
||||
'settings.install.goPlugins': '查看插件',
|
||||
'settings.install.goMcp': '查看 MCP',
|
||||
'settings.install.goSkills': '查看技能',
|
||||
'settings.install.sessionTitle': '安装助手会话',
|
||||
'settings.install.sessionHint': '这里展示安装专用 session 的完整执行过程、工具调用和结果。',
|
||||
'settings.install.sessionEmpty': '还没有安装会话',
|
||||
'settings.install.sessionEmptyHint': '发送第一条安装请求后,这里会直接复用 session 聊天链路展示完整过程。',
|
||||
'settings.install.createFailed': '创建安装会话失败',
|
||||
'settings.install.refreshDone': '已刷新插件、MCP 和技能状态',
|
||||
'settings.install.newConversationReady': '已准备新的安装会话;下一条请求会启动新的安装上下文。',
|
||||
'settings.install.clearConversationReady': '已清除当前安装对话;下一条请求会启动新的安装上下文。',
|
||||
|
||||
// Settings > Skills
|
||||
'settings.skills.title': '已安装技能',
|
||||
'settings.skills.description': '技能扩展 Claude 的能力。在 ~/.claude/skills/ 中管理技能。',
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
const INSTALLER_RULES = `You are the installation assistant for Claude Code Haha desktop.
|
||||
|
||||
Your job is to help the user install or configure Claude Code extensions and integrations, especially:
|
||||
- Skills
|
||||
- MCP servers
|
||||
- Plugins and marketplaces
|
||||
|
||||
Execution rules:
|
||||
1. Act directly when the request is clear. Do not ask for confirmation on obvious next steps.
|
||||
2. Prefer existing Claude CLI capabilities and existing config formats over inventing new files.
|
||||
3. Inspect before changing things if the request depends on the current local state.
|
||||
4. Prefer user scope unless the user explicitly asks for project/local scope.
|
||||
5. For plugin installation, use the existing plugin and marketplace commands, and run reload/apply steps when needed.
|
||||
6. For MCP setup, prefer the existing MCP commands and config locations, and include required auth/header details when available.
|
||||
7. For skill installation, install to ~/.claude/skills unless the user clearly asks for a project-scoped skill.
|
||||
8. Keep unrelated workspace files untouched.
|
||||
|
||||
Command guidance:
|
||||
- In Claude Code Haha Desktop installer sessions, the Bash shell already exposes a \`claude\` command wired to the bundled desktop CLI. Prefer that and do not spend time checking whether a separate global Claude CLI is installed.
|
||||
- Outside the desktop app, the bundled CLI is exposed to system terminals as \`claude-haha\`. Inside installer sessions, continue to use the injected \`claude\` command.
|
||||
- Plugin install: run Bash with \`claude plugin install <plugin-id> --scope <scope>\`
|
||||
- For a normal plugin installation request, do both:
|
||||
1. \`claude plugin install <plugin-id> --scope <scope>\`
|
||||
2. \`claude plugin enable <plugin-id> --scope <scope>\`
|
||||
- Only skip the enable step if the user explicitly asks to install without enabling.
|
||||
- Marketplace add: run Bash with \`claude plugin marketplace add <source> --scope <scope>\`
|
||||
- MCP add: run Bash with \`claude mcp add ...\`
|
||||
- Do NOT run \`claude /plugin ...\` in Bash. Slash commands such as \`/plugin install\` are not shell syntax.
|
||||
- If the user gives a GitHub URL under \`anthropics/claude-plugins-official/external_plugins/<name>\`, treat the plugin id as \`<name>@claude-plugins-official\`.
|
||||
- If the user gives a skill marketplace page, inspect the page and prefer the exact published install command shown on that page.
|
||||
- For AI Templates skill pages (\`aitmpl.com/component/skill/...\`), if the page shows an install command like \`npx claude-code-templates@latest --skill <slug>\`, run that command directly in Bash.
|
||||
- After a successful plugin install or enable step, tell the user that the desktop Install Center will refresh Plugins / Skills / MCP views automatically. Only do extra reload steps if they are necessary for the current task.
|
||||
|
||||
Response rules:
|
||||
- Briefly explain what you are doing.
|
||||
- After finishing, summarize exactly what was installed or changed, where it landed, and whether the user should check Plugins, MCP, or Skills.
|
||||
- If you are blocked by missing information, ask only the minimal question needed to proceed.`
|
||||
|
||||
export function buildInstallerPrompt(userRequest: string) {
|
||||
return `${INSTALLER_RULES}
|
||||
|
||||
User request:
|
||||
${userRequest.trim()}`
|
||||
}
|
||||
@ -28,7 +28,6 @@ import { useUIStore, type SettingsTab } from '../stores/uiStore'
|
||||
import { ClaudeOfficialLogin } from '../components/settings/ClaudeOfficialLogin'
|
||||
import { useUpdateStore } from '../stores/updateStore'
|
||||
import { formatBytes } from '../lib/formatBytes'
|
||||
import { InstallCenter } from '../components/settings/InstallCenter'
|
||||
|
||||
export function Settings() {
|
||||
const [activeTab, setActiveTab] = useState<SettingsTab>('providers')
|
||||
@ -51,7 +50,6 @@ export function Settings() {
|
||||
<TabButton icon="shield" label={t('settings.tab.permissions')} active={activeTab === 'permissions'} onClick={() => setActiveTab('permissions')} />
|
||||
<TabButton icon="tune" label={t('settings.tab.general')} active={activeTab === 'general'} onClick={() => setActiveTab('general')} />
|
||||
<TabButton icon="chat" label={t('settings.tab.adapters')} active={activeTab === 'adapters'} onClick={() => setActiveTab('adapters')} />
|
||||
<TabButton icon="download" label={t('settings.tab.install')} active={activeTab === 'install'} onClick={() => setActiveTab('install')} />
|
||||
<TabButton icon="terminal" label={t('settings.tab.terminal')} active={activeTab === 'terminal'} onClick={() => setActiveTab('terminal')} />
|
||||
<TabButton icon="dns" label={t('settings.tab.mcp')} active={activeTab === 'mcp'} onClick={() => setActiveTab('mcp')} />
|
||||
<TabButton icon="smart_toy" label={t('settings.tab.agents')} active={activeTab === 'agents'} onClick={() => setActiveTab('agents')} />
|
||||
@ -70,7 +68,6 @@ export function Settings() {
|
||||
{activeTab === 'permissions' && <PermissionSettings />}
|
||||
{activeTab === 'general' && <GeneralSettings />}
|
||||
{activeTab === 'adapters' && <AdapterSettings />}
|
||||
{activeTab === 'install' && <InstallCenter />}
|
||||
{activeTab === 'terminal' && <TerminalSettings />}
|
||||
{activeTab === 'mcp' && <McpSettings />}
|
||||
{activeTab === 'agents' && <AgentsSettings />}
|
||||
@ -1400,7 +1397,20 @@ function AboutSettings() {
|
||||
const initialize = useUpdateStore((s) => s.initialize)
|
||||
|
||||
useEffect(() => {
|
||||
import('@tauri-apps/api/app').then((mod) => mod.getVersion()).then(setVersion).catch(() => setVersion('0.1.0'))
|
||||
let cancelled = false
|
||||
|
||||
import('@tauri-apps/api/app')
|
||||
.then((mod) => mod.getVersion())
|
||||
.then((value) => {
|
||||
if (!cancelled) setVersion(value)
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setVersion('0.1.0')
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -33,7 +33,6 @@ export type SettingsTab =
|
||||
| 'permissions'
|
||||
| 'general'
|
||||
| 'adapters'
|
||||
| 'install'
|
||||
| 'terminal'
|
||||
| 'mcp'
|
||||
| 'agents'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user