bingocode 1.0.3 → 1.0.5
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/package.json +3 -7
- package/desktop/README.md +0 -30
- package/desktop/bunfig.toml +0 -1
- package/desktop/index.html +0 -17
- package/desktop/package.json +0 -55
- package/desktop/pnpm-lock.yaml +0 -3832
- package/desktop/public/app-icon.jpg +0 -0
- package/desktop/public/fonts/inter-latin-ext.woff2 +0 -0
- package/desktop/public/fonts/inter-latin.woff2 +0 -0
- package/desktop/public/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
- package/desktop/public/fonts/jetbrains-mono-latin.woff2 +0 -0
- package/desktop/public/fonts/manrope-latin-ext.woff2 +0 -0
- package/desktop/public/fonts/manrope-latin.woff2 +0 -0
- package/desktop/public/fonts/material-symbols-outlined.woff2 +0 -0
- package/desktop/public/icons/bilibili.svg +0 -1
- package/desktop/public/icons/douyin.svg +0 -1
- package/desktop/public/icons/github.svg +0 -3
- package/desktop/public/icons/xiaohongshu.svg +0 -1
- package/desktop/scripts/build-macos-arm64.sh +0 -270
- package/desktop/scripts/build-sidecars.ts +0 -183
- package/desktop/scripts/build-windows-x64.ps1 +0 -295
- package/desktop/scripts/scan-missing-imports.ts +0 -235
- package/desktop/sidecars/claude-sidecar.ts +0 -156
- package/desktop/src/App.tsx +0 -5
- package/desktop/src/__tests__/agentsSettings.test.tsx +0 -349
- package/desktop/src/__tests__/pages.test.tsx +0 -290
- package/desktop/src/__tests__/skillsSettings.test.tsx +0 -205
- package/desktop/src/api/adapters.ts +0 -12
- package/desktop/src/api/agents.ts +0 -36
- package/desktop/src/api/cliTasks.ts +0 -28
- package/desktop/src/api/client.ts +0 -63
- package/desktop/src/api/computerUse.ts +0 -76
- package/desktop/src/api/filesystem.ts +0 -30
- package/desktop/src/api/hahaOAuth.ts +0 -38
- package/desktop/src/api/models.ts +0 -28
- package/desktop/src/api/providers.ts +0 -63
- package/desktop/src/api/search.ts +0 -29
- package/desktop/src/api/sessions.ts +0 -56
- package/desktop/src/api/settings.ts +0 -20
- package/desktop/src/api/skills.ts +0 -19
- package/desktop/src/api/tasks.ts +0 -36
- package/desktop/src/api/teams.ts +0 -44
- package/desktop/src/api/websocket.ts +0 -164
- package/desktop/src/components/chat/AskUserQuestion.tsx +0 -268
- package/desktop/src/components/chat/AssistantMessage.tsx +0 -29
- package/desktop/src/components/chat/AttachmentGallery.tsx +0 -113
- package/desktop/src/components/chat/ChatInput.tsx +0 -622
- package/desktop/src/components/chat/CodeViewer.tsx +0 -161
- package/desktop/src/components/chat/ComputerUsePermissionModal.test.tsx +0 -174
- package/desktop/src/components/chat/ComputerUsePermissionModal.tsx +0 -311
- package/desktop/src/components/chat/DiffViewer.tsx +0 -157
- package/desktop/src/components/chat/FileSearchMenu.tsx +0 -198
- package/desktop/src/components/chat/ImageGalleryModal.tsx +0 -91
- package/desktop/src/components/chat/InlineImageGallery.tsx +0 -106
- package/desktop/src/components/chat/InlineTaskSummary.tsx +0 -60
- package/desktop/src/components/chat/MermaidRenderer.test.tsx +0 -98
- package/desktop/src/components/chat/MermaidRenderer.tsx +0 -361
- package/desktop/src/components/chat/MessageActionBar.tsx +0 -27
- package/desktop/src/components/chat/MessageList.test.tsx +0 -313
- package/desktop/src/components/chat/MessageList.tsx +0 -249
- package/desktop/src/components/chat/PermissionDialog.tsx +0 -262
- package/desktop/src/components/chat/SessionTaskBar.test.tsx +0 -99
- package/desktop/src/components/chat/SessionTaskBar.tsx +0 -159
- package/desktop/src/components/chat/StreamingIndicator.tsx +0 -41
- package/desktop/src/components/chat/TerminalChrome.tsx +0 -35
- package/desktop/src/components/chat/ThinkingBlock.tsx +0 -87
- package/desktop/src/components/chat/ToolCallBlock.tsx +0 -247
- package/desktop/src/components/chat/ToolCallGroup.tsx +0 -617
- package/desktop/src/components/chat/ToolResultBlock.tsx +0 -107
- package/desktop/src/components/chat/UserMessage.tsx +0 -38
- package/desktop/src/components/chat/chatBlocks.test.tsx +0 -136
- package/desktop/src/components/chat/clipboard.ts +0 -25
- package/desktop/src/components/chat/composerUtils.test.ts +0 -55
- package/desktop/src/components/chat/composerUtils.ts +0 -149
- package/desktop/src/components/controls/ModelSelector.tsx +0 -156
- package/desktop/src/components/controls/PermissionModeSelector.tsx +0 -229
- package/desktop/src/components/layout/AppShell.tsx +0 -107
- package/desktop/src/components/layout/ContentRouter.tsx +0 -27
- package/desktop/src/components/layout/ProjectFilter.tsx +0 -126
- package/desktop/src/components/layout/Sidebar.test.tsx +0 -158
- package/desktop/src/components/layout/Sidebar.tsx +0 -384
- package/desktop/src/components/layout/StatusBar.tsx +0 -31
- package/desktop/src/components/layout/TabBar.test.tsx +0 -136
- package/desktop/src/components/layout/TabBar.tsx +0 -318
- package/desktop/src/components/layout/TitleBar.tsx +0 -96
- package/desktop/src/components/layout/WindowControls.test.tsx +0 -69
- package/desktop/src/components/layout/WindowControls.tsx +0 -89
- package/desktop/src/components/markdown/MarkdownRenderer.test.tsx +0 -100
- package/desktop/src/components/markdown/MarkdownRenderer.tsx +0 -229
- package/desktop/src/components/settings/ClaudeOfficialLogin.tsx +0 -107
- package/desktop/src/components/shared/Button.tsx +0 -63
- package/desktop/src/components/shared/CopyButton.tsx +0 -58
- package/desktop/src/components/shared/DirectoryPicker.tsx +0 -316
- package/desktop/src/components/shared/Dropdown.tsx +0 -91
- package/desktop/src/components/shared/Input.tsx +0 -38
- package/desktop/src/components/shared/Modal.tsx +0 -65
- package/desktop/src/components/shared/ProjectContextChip.tsx +0 -30
- package/desktop/src/components/shared/Spinner.tsx +0 -30
- package/desktop/src/components/shared/Textarea.tsx +0 -38
- package/desktop/src/components/shared/Toast.tsx +0 -47
- package/desktop/src/components/shared/UpdateChecker.tsx +0 -90
- package/desktop/src/components/skills/SkillDetail.test.tsx +0 -89
- package/desktop/src/components/skills/SkillDetail.tsx +0 -403
- package/desktop/src/components/skills/SkillList.tsx +0 -254
- package/desktop/src/components/tasks/DayOfWeekPicker.tsx +0 -57
- package/desktop/src/components/tasks/NewTaskModal.tsx +0 -407
- package/desktop/src/components/tasks/PromptEditor.tsx +0 -74
- package/desktop/src/components/tasks/TaskEmptyState.tsx +0 -30
- package/desktop/src/components/tasks/TaskList.tsx +0 -46
- package/desktop/src/components/tasks/TaskRow.tsx +0 -253
- package/desktop/src/components/tasks/TaskRunsPanel.tsx +0 -195
- package/desktop/src/components/teams/TeamStatusBar.tsx +0 -147
- package/desktop/src/config/providerPresets.ts +0 -78
- package/desktop/src/config/spinnerVerbs.ts +0 -193
- package/desktop/src/hooks/useKeyboardShortcuts.ts +0 -60
- package/desktop/src/i18n/index.ts +0 -54
- package/desktop/src/i18n/locales/en.ts +0 -670
- package/desktop/src/i18n/locales/zh.ts +0 -670
- package/desktop/src/lib/__tests__/cronDescribe.test.ts +0 -93
- package/desktop/src/lib/cronDescribe.ts +0 -188
- package/desktop/src/lib/desktopRuntime.ts +0 -54
- package/desktop/src/lib/parseRunOutput.ts +0 -79
- package/desktop/src/main.tsx +0 -13
- package/desktop/src/mocks/data.ts +0 -202
- package/desktop/src/pages/ActiveSession.test.tsx +0 -181
- package/desktop/src/pages/ActiveSession.tsx +0 -219
- package/desktop/src/pages/AdapterSettings.tsx +0 -375
- package/desktop/src/pages/AgentTeams.tsx +0 -200
- package/desktop/src/pages/ComputerUseSettings.tsx +0 -420
- package/desktop/src/pages/EmptySession.tsx +0 -518
- package/desktop/src/pages/NewTaskModal.tsx +0 -346
- package/desktop/src/pages/ScheduledTasks.tsx +0 -66
- package/desktop/src/pages/ScheduledTasksEmpty.tsx +0 -152
- package/desktop/src/pages/ScheduledTasksList.tsx +0 -416
- package/desktop/src/pages/SessionControls.tsx +0 -460
- package/desktop/src/pages/Settings.tsx +0 -1448
- package/desktop/src/pages/ToolInspection.tsx +0 -235
- package/desktop/src/stores/adapterStore.ts +0 -106
- package/desktop/src/stores/agentStore.ts +0 -34
- package/desktop/src/stores/chatStore.test.ts +0 -505
- package/desktop/src/stores/chatStore.ts +0 -850
- package/desktop/src/stores/cliTaskStore.ts +0 -152
- package/desktop/src/stores/hahaOAuthStore.test.ts +0 -77
- package/desktop/src/stores/hahaOAuthStore.ts +0 -97
- package/desktop/src/stores/providerStore.ts +0 -101
- package/desktop/src/stores/sessionStore.test.ts +0 -63
- package/desktop/src/stores/sessionStore.ts +0 -102
- package/desktop/src/stores/settingsStore.ts +0 -120
- package/desktop/src/stores/skillStore.ts +0 -51
- package/desktop/src/stores/tabStore.ts +0 -169
- package/desktop/src/stores/taskStore.ts +0 -68
- package/desktop/src/stores/teamStore.ts +0 -344
- package/desktop/src/stores/uiStore.ts +0 -100
- package/desktop/src/stores/updateStore.test.ts +0 -71
- package/desktop/src/stores/updateStore.ts +0 -221
- package/desktop/src/theme/globals.css +0 -465
- package/desktop/src/types/adapter.ts +0 -33
- package/desktop/src/types/chat.ts +0 -152
- package/desktop/src/types/cliTask.ts +0 -24
- package/desktop/src/types/provider.ts +0 -62
- package/desktop/src/types/session.ts +0 -27
- package/desktop/src/types/settings.ts +0 -22
- package/desktop/src/types/skill.ts +0 -38
- package/desktop/src/types/task.ts +0 -56
- package/desktop/src/types/team.ts +0 -38
- package/desktop/src-tauri/Cargo.lock +0 -5549
- package/desktop/src-tauri/Cargo.toml +0 -20
- package/desktop/src-tauri/app-icon.svg +0 -13
- package/desktop/src-tauri/build.rs +0 -3
- package/desktop/src-tauri/capabilities/default.json +0 -106
- package/desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml +0 -5
- package/desktop/src-tauri/icons/android/values/ic_launcher_background.xml +0 -4
- package/desktop/src-tauri/icons/icon.icns +0 -0
- package/desktop/src-tauri/icons/icon.ico +0 -0
- package/desktop/src-tauri/src/lib.rs +0 -408
- package/desktop/src-tauri/src/main.rs +0 -6
- package/desktop/src-tauri/tauri.conf.json +0 -78
- package/desktop/src-tauri/tauri.macos.conf.json +0 -18
- package/desktop/src-tauri/tauri.release-ci.json +0 -5
- package/desktop/src-tauri/tauri.windows.conf.json +0 -16
- package/desktop/src-tauri/windows-installer-hooks.nsh +0 -17
- package/desktop/tsconfig.json +0 -25
- package/desktop/vite.config.ts +0 -26
- package/desktop/vitest.config.ts +0 -18
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { CodeViewer } from './CodeViewer'
|
|
2
|
-
import { useState } from 'react'
|
|
3
|
-
import { useTranslation } from '../../i18n'
|
|
4
|
-
import { InlineImageGallery } from './InlineImageGallery'
|
|
5
|
-
|
|
6
|
-
type Props = {
|
|
7
|
-
content: unknown
|
|
8
|
-
isError: boolean
|
|
9
|
-
toolName?: string
|
|
10
|
-
standalone?: boolean
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Standalone tool result block — only shown when not already rendered
|
|
15
|
-
* inline within ToolCallBlock (i.e., when the tool_use and tool_result
|
|
16
|
-
* are NOT grouped together by MessageList).
|
|
17
|
-
*/
|
|
18
|
-
export function ToolResultBlock({ content, isError, toolName, standalone = true }: Props) {
|
|
19
|
-
const [expanded, setExpanded] = useState(false)
|
|
20
|
-
const t = useTranslation()
|
|
21
|
-
|
|
22
|
-
// Don't render standalone if this result is already rendered inline
|
|
23
|
-
if (!standalone) return null
|
|
24
|
-
|
|
25
|
-
const text = extractText(content)
|
|
26
|
-
const preview = text.slice(0, 200)
|
|
27
|
-
const hasMore = text.length > 200
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<div className={`mb-2 ml-10 overflow-hidden rounded-xl border ${
|
|
31
|
-
isError
|
|
32
|
-
? 'border-[var(--color-error)]/20'
|
|
33
|
-
: 'border-[var(--color-outline-variant)]/20'
|
|
34
|
-
}`}>
|
|
35
|
-
{/* Status header */}
|
|
36
|
-
<button
|
|
37
|
-
type="button"
|
|
38
|
-
onClick={() => setExpanded((value) => !value)}
|
|
39
|
-
className={`flex w-full items-center justify-between px-3 py-2 text-left text-[10px] font-bold uppercase tracking-wider ${
|
|
40
|
-
isError
|
|
41
|
-
? 'bg-[var(--color-error-container)] text-[var(--color-error)]'
|
|
42
|
-
: 'bg-[var(--color-surface-container-high)] text-[var(--color-outline)]'
|
|
43
|
-
}`}
|
|
44
|
-
>
|
|
45
|
-
<span className="flex items-center gap-1.5">
|
|
46
|
-
<span className="material-symbols-outlined text-[12px]">
|
|
47
|
-
{isError ? 'error' : 'check_circle'}
|
|
48
|
-
</span>
|
|
49
|
-
{toolName ? t('tool.result', { toolName }) : t('tool.resultGeneric')}
|
|
50
|
-
</span>
|
|
51
|
-
<span className={`px-2 py-0.5 rounded-full text-[9px] ${
|
|
52
|
-
isError
|
|
53
|
-
? 'bg-[var(--color-error)]/10'
|
|
54
|
-
: 'bg-[var(--color-diff-added-bg)] text-[var(--color-diff-added-text)]'
|
|
55
|
-
}`}>
|
|
56
|
-
{isError ? t('tool.error') : t('tool.success')}
|
|
57
|
-
</span>
|
|
58
|
-
</button>
|
|
59
|
-
|
|
60
|
-
{/* Inline image gallery from detected paths */}
|
|
61
|
-
<InlineImageGallery text={text} />
|
|
62
|
-
|
|
63
|
-
{/* Content */}
|
|
64
|
-
{expanded ? (
|
|
65
|
-
isError ? (
|
|
66
|
-
<div className="bg-[var(--color-error-container)]/50 px-3 py-2.5 font-[var(--font-mono)] text-[11px] leading-[1.5] whitespace-pre-wrap break-words text-[var(--color-error)]">
|
|
67
|
-
{text}
|
|
68
|
-
</div>
|
|
69
|
-
) : (
|
|
70
|
-
<CodeViewer
|
|
71
|
-
code={text}
|
|
72
|
-
language="plaintext"
|
|
73
|
-
maxLines={12}
|
|
74
|
-
/>
|
|
75
|
-
)
|
|
76
|
-
) : (
|
|
77
|
-
<div className="bg-[var(--color-surface-container-lowest)] px-3 py-2 font-[var(--font-mono)] text-[10px] leading-[1.35] text-[var(--color-text-tertiary)]">
|
|
78
|
-
{preview}
|
|
79
|
-
{hasMore ? '…' : ''}
|
|
80
|
-
</div>
|
|
81
|
-
)}
|
|
82
|
-
|
|
83
|
-
{hasMore && (
|
|
84
|
-
<button
|
|
85
|
-
onClick={() => setExpanded((value) => !value)}
|
|
86
|
-
className="w-full py-1 text-[10px] font-medium text-[var(--color-text-accent)] hover:underline bg-[var(--color-surface-container-low)] border-t border-[var(--color-outline-variant)]/10"
|
|
87
|
-
>
|
|
88
|
-
{expanded ? t('tool.showLess') : t('tool.showMore', { count: text.length - 200 })}
|
|
89
|
-
</button>
|
|
90
|
-
)}
|
|
91
|
-
</div>
|
|
92
|
-
)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function extractText(content: unknown): string {
|
|
96
|
-
if (typeof content === 'string') return content
|
|
97
|
-
if (Array.isArray(content)) {
|
|
98
|
-
return content
|
|
99
|
-
.map((c: any) => (typeof c === 'string' ? c : c?.text || ''))
|
|
100
|
-
.filter(Boolean)
|
|
101
|
-
.join('\n')
|
|
102
|
-
}
|
|
103
|
-
if (content && typeof content === 'object') {
|
|
104
|
-
return JSON.stringify(content, null, 2)
|
|
105
|
-
}
|
|
106
|
-
return String(content ?? '')
|
|
107
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { UIAttachment } from '../../types/chat'
|
|
2
|
-
import { AttachmentGallery } from './AttachmentGallery'
|
|
3
|
-
import { MessageActionBar } from './MessageActionBar'
|
|
4
|
-
|
|
5
|
-
type Props = {
|
|
6
|
-
content: string
|
|
7
|
-
attachments?: UIAttachment[]
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function UserMessage({ content, attachments }: Props) {
|
|
11
|
-
const hasText = content.trim().length > 0
|
|
12
|
-
|
|
13
|
-
return (
|
|
14
|
-
<div className="group mb-5 flex items-end justify-end gap-1.5">
|
|
15
|
-
<div className="min-w-0 max-w-[82%] space-y-2">
|
|
16
|
-
{attachments && attachments.length > 0 && (
|
|
17
|
-
<AttachmentGallery attachments={attachments} variant="message" />
|
|
18
|
-
)}
|
|
19
|
-
|
|
20
|
-
{hasText && (
|
|
21
|
-
<div
|
|
22
|
-
className="bg-[var(--color-surface-user-msg)] px-4 py-3 text-sm leading-relaxed text-[var(--color-text-primary)] whitespace-pre-wrap break-words"
|
|
23
|
-
style={{ borderRadius: '18px 4px 18px 18px' }}
|
|
24
|
-
>
|
|
25
|
-
{content}
|
|
26
|
-
</div>
|
|
27
|
-
)}
|
|
28
|
-
</div>
|
|
29
|
-
|
|
30
|
-
{hasText && (
|
|
31
|
-
<MessageActionBar
|
|
32
|
-
copyText={content}
|
|
33
|
-
copyLabel="Copy prompt"
|
|
34
|
-
/>
|
|
35
|
-
)}
|
|
36
|
-
</div>
|
|
37
|
-
)
|
|
38
|
-
}
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
-
import { fireEvent, render, screen } from '@testing-library/react'
|
|
3
|
-
import { ThinkingBlock } from './ThinkingBlock'
|
|
4
|
-
import { ToolCallBlock } from './ToolCallBlock'
|
|
5
|
-
import { PermissionDialog } from './PermissionDialog'
|
|
6
|
-
import { useChatStore } from '../../stores/chatStore'
|
|
7
|
-
import { useTabStore } from '../../stores/tabStore'
|
|
8
|
-
|
|
9
|
-
describe('chat blocks', () => {
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
useTabStore.setState({ activeTabId: 'active-tab', tabs: [{ sessionId: 'active-tab', title: 'Test', type: 'session' as const, status: 'idle' }] })
|
|
12
|
-
useChatStore.setState({ sessions: {} })
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
it('keeps thinking collapsed by default', () => {
|
|
16
|
-
const { container } = render(<ThinkingBlock content="this is a long internal reasoning trace" isActive />)
|
|
17
|
-
|
|
18
|
-
expect(screen.getByText(/Thinking/)).toBeTruthy()
|
|
19
|
-
expect(container.textContent).toContain('this is a long internal reasoning trace')
|
|
20
|
-
expect(container.querySelector('.thinking-cursor')).toBeNull()
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it('does not animate inactive historical thinking blocks', () => {
|
|
24
|
-
const { container } = render(<ThinkingBlock content="old reasoning" isActive={false} />)
|
|
25
|
-
|
|
26
|
-
expect(container.querySelector('.thinking-inline-cursor')).toBeNull()
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('shows tool previews only after expanding the tool block', () => {
|
|
30
|
-
const { container } = render(
|
|
31
|
-
<ToolCallBlock
|
|
32
|
-
toolName="Read"
|
|
33
|
-
input={{ file_path: '/tmp/example.ts', limit: 20 }}
|
|
34
|
-
result={{ content: 'const answer = 42\nconsole.log(answer)', isError: false }}
|
|
35
|
-
/>,
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
expect(container.textContent).toContain('Read')
|
|
39
|
-
expect(container.textContent).not.toContain('const answer = 42')
|
|
40
|
-
|
|
41
|
-
fireEvent.click(screen.getByRole('button'))
|
|
42
|
-
|
|
43
|
-
expect(container.textContent).toContain('Tool Input')
|
|
44
|
-
expect(container.textContent).not.toContain('const answer = 42')
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('does not surface bash stdout in the transcript preview', () => {
|
|
48
|
-
const { container } = render(
|
|
49
|
-
<ToolCallBlock
|
|
50
|
-
toolName="Bash"
|
|
51
|
-
input={{ command: 'ls -la', description: 'List files' }}
|
|
52
|
-
result={{ content: 'file-a\nfile-b\nfile-c', isError: false }}
|
|
53
|
-
/>,
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
expect(container.textContent).toContain('Bash')
|
|
57
|
-
expect(container.textContent).not.toContain('file-a')
|
|
58
|
-
|
|
59
|
-
fireEvent.click(screen.getByRole('button'))
|
|
60
|
-
|
|
61
|
-
expect(container.textContent).toContain('ls -la')
|
|
62
|
-
expect(container.textContent).not.toContain('file-a')
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('expands tool errors so full Computer Use gate messages are readable', () => {
|
|
66
|
-
const { container } = render(
|
|
67
|
-
<ToolCallBlock
|
|
68
|
-
toolName="mcp__computer-use__left_click"
|
|
69
|
-
input={{ coordinate: [120, 220] }}
|
|
70
|
-
result={{
|
|
71
|
-
content: '"Claude Code Haha" is not in the allowed applications and is currently in front. Take a new screenshot — it may have appeared since your last one.',
|
|
72
|
-
isError: true,
|
|
73
|
-
}}
|
|
74
|
-
/>,
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
expect(container.textContent).toContain('mcp__computer-use__left_click')
|
|
78
|
-
expect(container.textContent).not.toContain('Take a new screenshot')
|
|
79
|
-
|
|
80
|
-
fireEvent.click(screen.getByRole('button'))
|
|
81
|
-
|
|
82
|
-
expect(container.textContent).toContain('Take a new screenshot')
|
|
83
|
-
expect(container.textContent).toContain('allowed applications')
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('shows a diff preview for edit permission requests', () => {
|
|
87
|
-
useChatStore.setState({
|
|
88
|
-
sessions: {
|
|
89
|
-
'active-tab': {
|
|
90
|
-
messages: [],
|
|
91
|
-
chatState: 'idle',
|
|
92
|
-
connectionState: 'connected',
|
|
93
|
-
streamingText: '',
|
|
94
|
-
streamingToolInput: '',
|
|
95
|
-
activeToolUseId: null,
|
|
96
|
-
activeToolName: null,
|
|
97
|
-
activeThinkingId: null,
|
|
98
|
-
pendingPermission: {
|
|
99
|
-
requestId: 'perm-1',
|
|
100
|
-
toolName: 'Edit',
|
|
101
|
-
input: {
|
|
102
|
-
file_path: '/tmp/example.ts',
|
|
103
|
-
old_string: 'const count = 1',
|
|
104
|
-
new_string: 'const count = 2',
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
pendingComputerUsePermission: null,
|
|
108
|
-
tokenUsage: { input_tokens: 0, output_tokens: 0 },
|
|
109
|
-
elapsedSeconds: 0,
|
|
110
|
-
statusVerb: '',
|
|
111
|
-
slashCommands: [],
|
|
112
|
-
agentTaskNotifications: {},
|
|
113
|
-
elapsedTimer: null,
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
const { container } = render(
|
|
119
|
-
<PermissionDialog
|
|
120
|
-
requestId="perm-1"
|
|
121
|
-
toolName="Edit"
|
|
122
|
-
input={{
|
|
123
|
-
file_path: '/tmp/example.ts',
|
|
124
|
-
old_string: 'const count = 1',
|
|
125
|
-
new_string: 'const count = 2',
|
|
126
|
-
}}
|
|
127
|
-
/>,
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
expect(container.textContent).toContain('/tmp/example.ts')
|
|
131
|
-
expect(container.textContent).toContain('Allow')
|
|
132
|
-
// react-diff-viewer-continued uses styled-components tables that don't
|
|
133
|
-
// fully render in jsdom, so we verify the DiffViewer wrapper is mounted
|
|
134
|
-
expect(container.querySelector('[class*="rounded-[var(--radius-lg)]"]')).toBeTruthy()
|
|
135
|
-
})
|
|
136
|
-
})
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
export async function copyTextToClipboard(text: string): Promise<boolean> {
|
|
2
|
-
try {
|
|
3
|
-
if (navigator.clipboard?.writeText) {
|
|
4
|
-
await navigator.clipboard.writeText(text)
|
|
5
|
-
return true
|
|
6
|
-
}
|
|
7
|
-
} catch {
|
|
8
|
-
// Fall through to legacy copy path.
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
try {
|
|
12
|
-
const textarea = document.createElement('textarea')
|
|
13
|
-
textarea.value = text
|
|
14
|
-
textarea.setAttribute('readonly', 'true')
|
|
15
|
-
textarea.style.position = 'fixed'
|
|
16
|
-
textarea.style.opacity = '0'
|
|
17
|
-
document.body.appendChild(textarea)
|
|
18
|
-
textarea.select()
|
|
19
|
-
const copied = document.execCommand('copy')
|
|
20
|
-
document.body.removeChild(textarea)
|
|
21
|
-
return copied
|
|
22
|
-
} catch {
|
|
23
|
-
return false
|
|
24
|
-
}
|
|
25
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
findSlashToken,
|
|
4
|
-
insertSlashTrigger,
|
|
5
|
-
mergeSlashCommands,
|
|
6
|
-
replaceSlashCommand,
|
|
7
|
-
} from './composerUtils'
|
|
8
|
-
|
|
9
|
-
describe('composerUtils', () => {
|
|
10
|
-
it('finds slash token without trailing space', () => {
|
|
11
|
-
expect(findSlashToken('/rev', 4)).toEqual({ start: 0, filter: 'rev' })
|
|
12
|
-
expect(findSlashToken('hello /rev', 10)).toEqual({ start: 6, filter: 'rev' })
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
it('does not treat slash followed by a space as an active token', () => {
|
|
16
|
-
expect(findSlashToken('/ review', 8)).toBeNull()
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('inserts a slash trigger without appending a trailing space', () => {
|
|
20
|
-
expect(insertSlashTrigger('', 0)).toEqual({ value: '/', cursorPos: 1 })
|
|
21
|
-
expect(insertSlashTrigger('hello', 5)).toEqual({ value: 'hello /', cursorPos: 7 })
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it('replaces the current slash token with a command and one trailing separator', () => {
|
|
25
|
-
expect(replaceSlashCommand('/rev', 4, 'review')).toEqual({
|
|
26
|
-
value: '/review ',
|
|
27
|
-
cursorPos: 8,
|
|
28
|
-
})
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('merges fallback commands so built-in entries like /clear remain visible', () => {
|
|
32
|
-
expect(
|
|
33
|
-
mergeSlashCommands([
|
|
34
|
-
{ name: 'help', description: '' },
|
|
35
|
-
]),
|
|
36
|
-
).toEqual(
|
|
37
|
-
expect.arrayContaining([
|
|
38
|
-
{ name: 'help', description: 'Show available commands' },
|
|
39
|
-
{ name: 'clear', description: 'Clear conversation history' },
|
|
40
|
-
]),
|
|
41
|
-
)
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('keeps server-provided descriptions when they exist', () => {
|
|
45
|
-
expect(
|
|
46
|
-
mergeSlashCommands([
|
|
47
|
-
{ name: 'clear', description: 'Server description' },
|
|
48
|
-
]),
|
|
49
|
-
).toEqual(
|
|
50
|
-
expect.arrayContaining([
|
|
51
|
-
{ name: 'clear', description: 'Server description' },
|
|
52
|
-
]),
|
|
53
|
-
)
|
|
54
|
-
})
|
|
55
|
-
})
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
export const FALLBACK_SLASH_COMMANDS = [
|
|
2
|
-
{ name: 'compact', description: 'Compact conversation context' },
|
|
3
|
-
{ name: 'clear', description: 'Clear conversation history' },
|
|
4
|
-
{ name: 'help', description: 'Show available commands' },
|
|
5
|
-
{ name: 'review', description: 'Review code changes' },
|
|
6
|
-
{ name: 'commit', description: 'Create a git commit' },
|
|
7
|
-
{ name: 'pr', description: 'Create a pull request' },
|
|
8
|
-
{ name: 'init', description: 'Initialize project CLAUDE.md' },
|
|
9
|
-
{ name: 'bug', description: 'Report a bug' },
|
|
10
|
-
{ name: 'config', description: 'Open configuration' },
|
|
11
|
-
{ name: 'cost', description: 'Show token usage and costs' },
|
|
12
|
-
{ name: 'doctor', description: 'Diagnose installation issues' },
|
|
13
|
-
{ name: 'login', description: 'Switch Anthropic accounts' },
|
|
14
|
-
{ name: 'logout', description: 'Sign out of current account' },
|
|
15
|
-
{ name: 'memory', description: 'Edit CLAUDE.md memory files' },
|
|
16
|
-
{ name: 'model', description: 'Switch AI model' },
|
|
17
|
-
{ name: 'permissions', description: 'View or manage tool permissions' },
|
|
18
|
-
{ name: 'status', description: 'Show project and session status' },
|
|
19
|
-
{ name: 'terminal-setup', description: 'Set up terminal integration' },
|
|
20
|
-
{ name: 'vim', description: 'Toggle vim editing mode' },
|
|
21
|
-
]
|
|
22
|
-
|
|
23
|
-
export type SlashCommandOption = {
|
|
24
|
-
name: string
|
|
25
|
-
description: string
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function mergeSlashCommands(
|
|
29
|
-
preferred: ReadonlyArray<SlashCommandOption>,
|
|
30
|
-
fallback: ReadonlyArray<SlashCommandOption> = FALLBACK_SLASH_COMMANDS,
|
|
31
|
-
): SlashCommandOption[] {
|
|
32
|
-
const merged = new Map<string, SlashCommandOption>()
|
|
33
|
-
|
|
34
|
-
for (const command of preferred) {
|
|
35
|
-
if (!command?.name) continue
|
|
36
|
-
merged.set(command.name, {
|
|
37
|
-
name: command.name,
|
|
38
|
-
description: command.description?.trim() || '',
|
|
39
|
-
})
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
for (const command of fallback) {
|
|
43
|
-
if (!command?.name) continue
|
|
44
|
-
const existing = merged.get(command.name)
|
|
45
|
-
if (existing) {
|
|
46
|
-
if (!existing.description && command.description) {
|
|
47
|
-
merged.set(command.name, {
|
|
48
|
-
...existing,
|
|
49
|
-
description: command.description,
|
|
50
|
-
})
|
|
51
|
-
}
|
|
52
|
-
continue
|
|
53
|
-
}
|
|
54
|
-
merged.set(command.name, command)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return [...merged.values()]
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export type SlashTrigger = {
|
|
61
|
-
slashPos: number
|
|
62
|
-
filter: string
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function findSlashTrigger(value: string, cursorPos: number): SlashTrigger | null {
|
|
66
|
-
const textBeforeCursor = value.slice(0, cursorPos)
|
|
67
|
-
let slashPos = -1
|
|
68
|
-
|
|
69
|
-
for (let i = textBeforeCursor.length - 1; i >= 0; i--) {
|
|
70
|
-
const ch = textBeforeCursor[i]!
|
|
71
|
-
if (ch === '/') {
|
|
72
|
-
if (i === 0 || /\s/.test(textBeforeCursor[i - 1]!)) {
|
|
73
|
-
slashPos = i
|
|
74
|
-
break
|
|
75
|
-
}
|
|
76
|
-
break
|
|
77
|
-
}
|
|
78
|
-
if (/\s/.test(ch)) {
|
|
79
|
-
break
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (slashPos < 0) return null
|
|
84
|
-
|
|
85
|
-
const filter = textBeforeCursor.slice(slashPos + 1)
|
|
86
|
-
if (/\s/.test(filter)) return null
|
|
87
|
-
|
|
88
|
-
return { slashPos, filter }
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function replaceSlashToken(
|
|
92
|
-
input: string,
|
|
93
|
-
cursorPos: number,
|
|
94
|
-
command: string,
|
|
95
|
-
options?: { trailingSpace?: boolean },
|
|
96
|
-
): { value: string; cursorPos: number } {
|
|
97
|
-
const trigger = findSlashTrigger(input, cursorPos)
|
|
98
|
-
if (!trigger) {
|
|
99
|
-
const prefix = input && !/\s$/.test(input) ? `${input} ` : input
|
|
100
|
-
const token = `/${command}`
|
|
101
|
-
const suffix = options?.trailingSpace !== false ? ' ' : ''
|
|
102
|
-
const value = `${prefix}${token}${suffix}`
|
|
103
|
-
return { value, cursorPos: value.length }
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const before = input.slice(0, trigger.slashPos)
|
|
107
|
-
const after = input.slice(cursorPos)
|
|
108
|
-
const token = `/${command}`
|
|
109
|
-
const suffix = options?.trailingSpace !== false ? ' ' : ''
|
|
110
|
-
const value = `${before}${token}${suffix}${after}`
|
|
111
|
-
const nextCursorPos = before.length + token.length + suffix.length
|
|
112
|
-
return { value, cursorPos: nextCursorPos }
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export type SlashToken = {
|
|
116
|
-
start: number
|
|
117
|
-
filter: string
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function findSlashToken(value: string, cursorPos: number): SlashToken | null {
|
|
121
|
-
const trigger = findSlashTrigger(value, cursorPos)
|
|
122
|
-
if (!trigger) return null
|
|
123
|
-
return { start: trigger.slashPos, filter: trigger.filter }
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function replaceSlashCommand(
|
|
127
|
-
value: string,
|
|
128
|
-
cursorPos: number,
|
|
129
|
-
command: string,
|
|
130
|
-
): { value: string; cursorPos: number } | null {
|
|
131
|
-
const trigger = findSlashTrigger(value, cursorPos)
|
|
132
|
-
if (!trigger) return null
|
|
133
|
-
|
|
134
|
-
return replaceSlashToken(value, cursorPos, command, { trailingSpace: true })
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function insertSlashTrigger(
|
|
138
|
-
value: string,
|
|
139
|
-
cursorPos: number,
|
|
140
|
-
): { value: string; cursorPos: number } {
|
|
141
|
-
const before = value.slice(0, cursorPos)
|
|
142
|
-
const after = value.slice(cursorPos)
|
|
143
|
-
const needsLeadingSpace = before.length > 0 && !/\s$/.test(before)
|
|
144
|
-
const token = `${needsLeadingSpace ? ' ' : ''}/`
|
|
145
|
-
return {
|
|
146
|
-
value: `${before}${token}${after}`,
|
|
147
|
-
cursorPos: before.length + token.length,
|
|
148
|
-
}
|
|
149
|
-
}
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { useState, useRef, useEffect } from 'react'
|
|
2
|
-
import { useSettingsStore } from '../../stores/settingsStore'
|
|
3
|
-
import { useTranslation } from '../../i18n'
|
|
4
|
-
import type { EffortLevel } from '../../types/settings'
|
|
5
|
-
|
|
6
|
-
const MODEL_ICONS = {
|
|
7
|
-
opus: 'diamond',
|
|
8
|
-
sonnet: 'auto_awesome',
|
|
9
|
-
haiku: 'bolt',
|
|
10
|
-
} as const
|
|
11
|
-
|
|
12
|
-
type Props = {
|
|
13
|
-
/** Controlled mode: model ID override */
|
|
14
|
-
value?: string
|
|
15
|
-
/** Controlled mode: called on change instead of updating global store */
|
|
16
|
-
onChange?: (modelId: string) => void
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function ModelSelector({ value, onChange }: Props = {}) {
|
|
20
|
-
const t = useTranslation()
|
|
21
|
-
const { currentModel: storeModel, availableModels, effortLevel, setModel, setEffort } = useSettingsStore()
|
|
22
|
-
const [open, setOpen] = useState(false)
|
|
23
|
-
const ref = useRef<HTMLDivElement>(null)
|
|
24
|
-
|
|
25
|
-
const EFFORT_OPTIONS: { value: EffortLevel; label: string }[] = [
|
|
26
|
-
{ value: 'low', label: t('settings.general.effort.low') },
|
|
27
|
-
{ value: 'medium', label: t('settings.general.effort.medium') },
|
|
28
|
-
{ value: 'high', label: t('settings.general.effort.high') },
|
|
29
|
-
{ value: 'max', label: t('settings.general.effort.max') },
|
|
30
|
-
]
|
|
31
|
-
|
|
32
|
-
const isControlled = value !== undefined
|
|
33
|
-
const selectedModel = isControlled ? availableModels.find((m) => m.id === value) || null : storeModel
|
|
34
|
-
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
if (!open) return
|
|
37
|
-
const handleClick = (e: MouseEvent) => {
|
|
38
|
-
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
|
|
39
|
-
}
|
|
40
|
-
const handleEsc = (e: KeyboardEvent) => {
|
|
41
|
-
if (e.key === 'Escape') setOpen(false)
|
|
42
|
-
}
|
|
43
|
-
document.addEventListener('mousedown', handleClick)
|
|
44
|
-
document.addEventListener('keydown', handleEsc)
|
|
45
|
-
return () => {
|
|
46
|
-
document.removeEventListener('mousedown', handleClick)
|
|
47
|
-
document.removeEventListener('keydown', handleEsc)
|
|
48
|
-
}
|
|
49
|
-
}, [open])
|
|
50
|
-
|
|
51
|
-
const getModelIcon = (id: string): string => {
|
|
52
|
-
const lower = id.toLowerCase()
|
|
53
|
-
if (lower.includes('opus')) return MODEL_ICONS.opus
|
|
54
|
-
if (lower.includes('sonnet')) return MODEL_ICONS.sonnet
|
|
55
|
-
if (lower.includes('haiku')) return MODEL_ICONS.haiku
|
|
56
|
-
return 'smart_toy'
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
<div ref={ref} className="relative">
|
|
61
|
-
<button
|
|
62
|
-
onClick={() => setOpen(!open)}
|
|
63
|
-
className="flex items-center gap-1.5 px-2.5 py-1.5 bg-[var(--color-surface-container-low)] hover:bg-[var(--color-surface-hover)] rounded-full text-xs font-medium text-[var(--color-text-secondary)] transition-colors"
|
|
64
|
-
>
|
|
65
|
-
<span className="material-symbols-outlined text-[14px] text-[var(--color-brand)]">auto_awesome</span>
|
|
66
|
-
<span>{selectedModel?.name ?? t('model.selectModel')}</span>
|
|
67
|
-
<span className="material-symbols-outlined text-[12px]">expand_more</span>
|
|
68
|
-
</button>
|
|
69
|
-
|
|
70
|
-
{open && (
|
|
71
|
-
<div className="absolute right-0 bottom-full mb-2 w-[340px] rounded-xl bg-[var(--color-surface-container-lowest)] border border-[var(--color-border)] shadow-[var(--shadow-dropdown)] z-50">
|
|
72
|
-
{/* Models */}
|
|
73
|
-
<div className="p-3">
|
|
74
|
-
<div className="text-[10px] font-bold uppercase tracking-widest text-[var(--color-outline)] mb-2 px-1">
|
|
75
|
-
{t('model.configuration')}
|
|
76
|
-
</div>
|
|
77
|
-
<div className="space-y-1">
|
|
78
|
-
{availableModels.map((model) => {
|
|
79
|
-
const isSelected = model.id === selectedModel?.id
|
|
80
|
-
return (
|
|
81
|
-
<button
|
|
82
|
-
key={model.id}
|
|
83
|
-
onClick={() => {
|
|
84
|
-
if (isControlled) {
|
|
85
|
-
onChange?.(model.id)
|
|
86
|
-
} else {
|
|
87
|
-
setModel(model.id)
|
|
88
|
-
}
|
|
89
|
-
setOpen(false)
|
|
90
|
-
}}
|
|
91
|
-
className={`
|
|
92
|
-
w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-colors
|
|
93
|
-
${isSelected
|
|
94
|
-
? 'bg-[var(--color-primary-fixed)] border border-[var(--color-brand)]/20'
|
|
95
|
-
: 'hover:bg-[var(--color-surface-hover)]'
|
|
96
|
-
}
|
|
97
|
-
`}
|
|
98
|
-
>
|
|
99
|
-
{/* Radio button */}
|
|
100
|
-
<div className={`w-4 h-4 rounded-full border-2 flex items-center justify-center flex-shrink-0 ${
|
|
101
|
-
isSelected
|
|
102
|
-
? 'border-[var(--color-brand)]'
|
|
103
|
-
: 'border-[var(--color-outline)]'
|
|
104
|
-
}`}>
|
|
105
|
-
{isSelected && (
|
|
106
|
-
<div className="w-2 h-2 rounded-full bg-[var(--color-brand)]" />
|
|
107
|
-
)}
|
|
108
|
-
</div>
|
|
109
|
-
|
|
110
|
-
<span className="material-symbols-outlined text-[18px] text-[var(--color-text-secondary)]">
|
|
111
|
-
{getModelIcon(model.id)}
|
|
112
|
-
</span>
|
|
113
|
-
|
|
114
|
-
<div className="flex-1 min-w-0">
|
|
115
|
-
<div className="text-sm font-semibold text-[var(--color-text-primary)]">{model.name}</div>
|
|
116
|
-
{model.description && (
|
|
117
|
-
<div className="text-[10px] text-[var(--color-text-tertiary)] mt-0.5 truncate">{model.description}</div>
|
|
118
|
-
)}
|
|
119
|
-
</div>
|
|
120
|
-
</button>
|
|
121
|
-
)
|
|
122
|
-
})}
|
|
123
|
-
</div>
|
|
124
|
-
</div>
|
|
125
|
-
|
|
126
|
-
{/* Effort — hidden in controlled mode (not relevant for task creation) */}
|
|
127
|
-
{!isControlled && <div className="border-t border-[var(--color-border)] p-3">
|
|
128
|
-
<div className="text-[10px] font-bold uppercase tracking-widest text-[var(--color-outline)] mb-2 px-1">
|
|
129
|
-
{t('model.effort')}
|
|
130
|
-
</div>
|
|
131
|
-
<div className="grid grid-cols-4 gap-1.5">
|
|
132
|
-
{EFFORT_OPTIONS.map((opt) => {
|
|
133
|
-
const isSelected = opt.value === effortLevel
|
|
134
|
-
return (
|
|
135
|
-
<button
|
|
136
|
-
key={opt.value}
|
|
137
|
-
onClick={() => { setEffort(opt.value); setOpen(false) }}
|
|
138
|
-
className={`
|
|
139
|
-
py-2 rounded-lg text-xs font-semibold transition-colors text-center
|
|
140
|
-
${isSelected
|
|
141
|
-
? 'bg-[var(--color-brand)] text-white'
|
|
142
|
-
: 'bg-[var(--color-surface-container-high)] text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)]'
|
|
143
|
-
}
|
|
144
|
-
`}
|
|
145
|
-
>
|
|
146
|
-
{opt.label}
|
|
147
|
-
</button>
|
|
148
|
-
)
|
|
149
|
-
})}
|
|
150
|
-
</div>
|
|
151
|
-
</div>}
|
|
152
|
-
</div>
|
|
153
|
-
)}
|
|
154
|
-
</div>
|
|
155
|
-
)
|
|
156
|
-
}
|