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.
Files changed (184) hide show
  1. package/package.json +3 -7
  2. package/desktop/README.md +0 -30
  3. package/desktop/bunfig.toml +0 -1
  4. package/desktop/index.html +0 -17
  5. package/desktop/package.json +0 -55
  6. package/desktop/pnpm-lock.yaml +0 -3832
  7. package/desktop/public/app-icon.jpg +0 -0
  8. package/desktop/public/fonts/inter-latin-ext.woff2 +0 -0
  9. package/desktop/public/fonts/inter-latin.woff2 +0 -0
  10. package/desktop/public/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
  11. package/desktop/public/fonts/jetbrains-mono-latin.woff2 +0 -0
  12. package/desktop/public/fonts/manrope-latin-ext.woff2 +0 -0
  13. package/desktop/public/fonts/manrope-latin.woff2 +0 -0
  14. package/desktop/public/fonts/material-symbols-outlined.woff2 +0 -0
  15. package/desktop/public/icons/bilibili.svg +0 -1
  16. package/desktop/public/icons/douyin.svg +0 -1
  17. package/desktop/public/icons/github.svg +0 -3
  18. package/desktop/public/icons/xiaohongshu.svg +0 -1
  19. package/desktop/scripts/build-macos-arm64.sh +0 -270
  20. package/desktop/scripts/build-sidecars.ts +0 -183
  21. package/desktop/scripts/build-windows-x64.ps1 +0 -295
  22. package/desktop/scripts/scan-missing-imports.ts +0 -235
  23. package/desktop/sidecars/claude-sidecar.ts +0 -156
  24. package/desktop/src/App.tsx +0 -5
  25. package/desktop/src/__tests__/agentsSettings.test.tsx +0 -349
  26. package/desktop/src/__tests__/pages.test.tsx +0 -290
  27. package/desktop/src/__tests__/skillsSettings.test.tsx +0 -205
  28. package/desktop/src/api/adapters.ts +0 -12
  29. package/desktop/src/api/agents.ts +0 -36
  30. package/desktop/src/api/cliTasks.ts +0 -28
  31. package/desktop/src/api/client.ts +0 -63
  32. package/desktop/src/api/computerUse.ts +0 -76
  33. package/desktop/src/api/filesystem.ts +0 -30
  34. package/desktop/src/api/hahaOAuth.ts +0 -38
  35. package/desktop/src/api/models.ts +0 -28
  36. package/desktop/src/api/providers.ts +0 -63
  37. package/desktop/src/api/search.ts +0 -29
  38. package/desktop/src/api/sessions.ts +0 -56
  39. package/desktop/src/api/settings.ts +0 -20
  40. package/desktop/src/api/skills.ts +0 -19
  41. package/desktop/src/api/tasks.ts +0 -36
  42. package/desktop/src/api/teams.ts +0 -44
  43. package/desktop/src/api/websocket.ts +0 -164
  44. package/desktop/src/components/chat/AskUserQuestion.tsx +0 -268
  45. package/desktop/src/components/chat/AssistantMessage.tsx +0 -29
  46. package/desktop/src/components/chat/AttachmentGallery.tsx +0 -113
  47. package/desktop/src/components/chat/ChatInput.tsx +0 -622
  48. package/desktop/src/components/chat/CodeViewer.tsx +0 -161
  49. package/desktop/src/components/chat/ComputerUsePermissionModal.test.tsx +0 -174
  50. package/desktop/src/components/chat/ComputerUsePermissionModal.tsx +0 -311
  51. package/desktop/src/components/chat/DiffViewer.tsx +0 -157
  52. package/desktop/src/components/chat/FileSearchMenu.tsx +0 -198
  53. package/desktop/src/components/chat/ImageGalleryModal.tsx +0 -91
  54. package/desktop/src/components/chat/InlineImageGallery.tsx +0 -106
  55. package/desktop/src/components/chat/InlineTaskSummary.tsx +0 -60
  56. package/desktop/src/components/chat/MermaidRenderer.test.tsx +0 -98
  57. package/desktop/src/components/chat/MermaidRenderer.tsx +0 -361
  58. package/desktop/src/components/chat/MessageActionBar.tsx +0 -27
  59. package/desktop/src/components/chat/MessageList.test.tsx +0 -313
  60. package/desktop/src/components/chat/MessageList.tsx +0 -249
  61. package/desktop/src/components/chat/PermissionDialog.tsx +0 -262
  62. package/desktop/src/components/chat/SessionTaskBar.test.tsx +0 -99
  63. package/desktop/src/components/chat/SessionTaskBar.tsx +0 -159
  64. package/desktop/src/components/chat/StreamingIndicator.tsx +0 -41
  65. package/desktop/src/components/chat/TerminalChrome.tsx +0 -35
  66. package/desktop/src/components/chat/ThinkingBlock.tsx +0 -87
  67. package/desktop/src/components/chat/ToolCallBlock.tsx +0 -247
  68. package/desktop/src/components/chat/ToolCallGroup.tsx +0 -617
  69. package/desktop/src/components/chat/ToolResultBlock.tsx +0 -107
  70. package/desktop/src/components/chat/UserMessage.tsx +0 -38
  71. package/desktop/src/components/chat/chatBlocks.test.tsx +0 -136
  72. package/desktop/src/components/chat/clipboard.ts +0 -25
  73. package/desktop/src/components/chat/composerUtils.test.ts +0 -55
  74. package/desktop/src/components/chat/composerUtils.ts +0 -149
  75. package/desktop/src/components/controls/ModelSelector.tsx +0 -156
  76. package/desktop/src/components/controls/PermissionModeSelector.tsx +0 -229
  77. package/desktop/src/components/layout/AppShell.tsx +0 -107
  78. package/desktop/src/components/layout/ContentRouter.tsx +0 -27
  79. package/desktop/src/components/layout/ProjectFilter.tsx +0 -126
  80. package/desktop/src/components/layout/Sidebar.test.tsx +0 -158
  81. package/desktop/src/components/layout/Sidebar.tsx +0 -384
  82. package/desktop/src/components/layout/StatusBar.tsx +0 -31
  83. package/desktop/src/components/layout/TabBar.test.tsx +0 -136
  84. package/desktop/src/components/layout/TabBar.tsx +0 -318
  85. package/desktop/src/components/layout/TitleBar.tsx +0 -96
  86. package/desktop/src/components/layout/WindowControls.test.tsx +0 -69
  87. package/desktop/src/components/layout/WindowControls.tsx +0 -89
  88. package/desktop/src/components/markdown/MarkdownRenderer.test.tsx +0 -100
  89. package/desktop/src/components/markdown/MarkdownRenderer.tsx +0 -229
  90. package/desktop/src/components/settings/ClaudeOfficialLogin.tsx +0 -107
  91. package/desktop/src/components/shared/Button.tsx +0 -63
  92. package/desktop/src/components/shared/CopyButton.tsx +0 -58
  93. package/desktop/src/components/shared/DirectoryPicker.tsx +0 -316
  94. package/desktop/src/components/shared/Dropdown.tsx +0 -91
  95. package/desktop/src/components/shared/Input.tsx +0 -38
  96. package/desktop/src/components/shared/Modal.tsx +0 -65
  97. package/desktop/src/components/shared/ProjectContextChip.tsx +0 -30
  98. package/desktop/src/components/shared/Spinner.tsx +0 -30
  99. package/desktop/src/components/shared/Textarea.tsx +0 -38
  100. package/desktop/src/components/shared/Toast.tsx +0 -47
  101. package/desktop/src/components/shared/UpdateChecker.tsx +0 -90
  102. package/desktop/src/components/skills/SkillDetail.test.tsx +0 -89
  103. package/desktop/src/components/skills/SkillDetail.tsx +0 -403
  104. package/desktop/src/components/skills/SkillList.tsx +0 -254
  105. package/desktop/src/components/tasks/DayOfWeekPicker.tsx +0 -57
  106. package/desktop/src/components/tasks/NewTaskModal.tsx +0 -407
  107. package/desktop/src/components/tasks/PromptEditor.tsx +0 -74
  108. package/desktop/src/components/tasks/TaskEmptyState.tsx +0 -30
  109. package/desktop/src/components/tasks/TaskList.tsx +0 -46
  110. package/desktop/src/components/tasks/TaskRow.tsx +0 -253
  111. package/desktop/src/components/tasks/TaskRunsPanel.tsx +0 -195
  112. package/desktop/src/components/teams/TeamStatusBar.tsx +0 -147
  113. package/desktop/src/config/providerPresets.ts +0 -78
  114. package/desktop/src/config/spinnerVerbs.ts +0 -193
  115. package/desktop/src/hooks/useKeyboardShortcuts.ts +0 -60
  116. package/desktop/src/i18n/index.ts +0 -54
  117. package/desktop/src/i18n/locales/en.ts +0 -670
  118. package/desktop/src/i18n/locales/zh.ts +0 -670
  119. package/desktop/src/lib/__tests__/cronDescribe.test.ts +0 -93
  120. package/desktop/src/lib/cronDescribe.ts +0 -188
  121. package/desktop/src/lib/desktopRuntime.ts +0 -54
  122. package/desktop/src/lib/parseRunOutput.ts +0 -79
  123. package/desktop/src/main.tsx +0 -13
  124. package/desktop/src/mocks/data.ts +0 -202
  125. package/desktop/src/pages/ActiveSession.test.tsx +0 -181
  126. package/desktop/src/pages/ActiveSession.tsx +0 -219
  127. package/desktop/src/pages/AdapterSettings.tsx +0 -375
  128. package/desktop/src/pages/AgentTeams.tsx +0 -200
  129. package/desktop/src/pages/ComputerUseSettings.tsx +0 -420
  130. package/desktop/src/pages/EmptySession.tsx +0 -518
  131. package/desktop/src/pages/NewTaskModal.tsx +0 -346
  132. package/desktop/src/pages/ScheduledTasks.tsx +0 -66
  133. package/desktop/src/pages/ScheduledTasksEmpty.tsx +0 -152
  134. package/desktop/src/pages/ScheduledTasksList.tsx +0 -416
  135. package/desktop/src/pages/SessionControls.tsx +0 -460
  136. package/desktop/src/pages/Settings.tsx +0 -1448
  137. package/desktop/src/pages/ToolInspection.tsx +0 -235
  138. package/desktop/src/stores/adapterStore.ts +0 -106
  139. package/desktop/src/stores/agentStore.ts +0 -34
  140. package/desktop/src/stores/chatStore.test.ts +0 -505
  141. package/desktop/src/stores/chatStore.ts +0 -850
  142. package/desktop/src/stores/cliTaskStore.ts +0 -152
  143. package/desktop/src/stores/hahaOAuthStore.test.ts +0 -77
  144. package/desktop/src/stores/hahaOAuthStore.ts +0 -97
  145. package/desktop/src/stores/providerStore.ts +0 -101
  146. package/desktop/src/stores/sessionStore.test.ts +0 -63
  147. package/desktop/src/stores/sessionStore.ts +0 -102
  148. package/desktop/src/stores/settingsStore.ts +0 -120
  149. package/desktop/src/stores/skillStore.ts +0 -51
  150. package/desktop/src/stores/tabStore.ts +0 -169
  151. package/desktop/src/stores/taskStore.ts +0 -68
  152. package/desktop/src/stores/teamStore.ts +0 -344
  153. package/desktop/src/stores/uiStore.ts +0 -100
  154. package/desktop/src/stores/updateStore.test.ts +0 -71
  155. package/desktop/src/stores/updateStore.ts +0 -221
  156. package/desktop/src/theme/globals.css +0 -465
  157. package/desktop/src/types/adapter.ts +0 -33
  158. package/desktop/src/types/chat.ts +0 -152
  159. package/desktop/src/types/cliTask.ts +0 -24
  160. package/desktop/src/types/provider.ts +0 -62
  161. package/desktop/src/types/session.ts +0 -27
  162. package/desktop/src/types/settings.ts +0 -22
  163. package/desktop/src/types/skill.ts +0 -38
  164. package/desktop/src/types/task.ts +0 -56
  165. package/desktop/src/types/team.ts +0 -38
  166. package/desktop/src-tauri/Cargo.lock +0 -5549
  167. package/desktop/src-tauri/Cargo.toml +0 -20
  168. package/desktop/src-tauri/app-icon.svg +0 -13
  169. package/desktop/src-tauri/build.rs +0 -3
  170. package/desktop/src-tauri/capabilities/default.json +0 -106
  171. package/desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml +0 -5
  172. package/desktop/src-tauri/icons/android/values/ic_launcher_background.xml +0 -4
  173. package/desktop/src-tauri/icons/icon.icns +0 -0
  174. package/desktop/src-tauri/icons/icon.ico +0 -0
  175. package/desktop/src-tauri/src/lib.rs +0 -408
  176. package/desktop/src-tauri/src/main.rs +0 -6
  177. package/desktop/src-tauri/tauri.conf.json +0 -78
  178. package/desktop/src-tauri/tauri.macos.conf.json +0 -18
  179. package/desktop/src-tauri/tauri.release-ci.json +0 -5
  180. package/desktop/src-tauri/tauri.windows.conf.json +0 -16
  181. package/desktop/src-tauri/windows-installer-hooks.nsh +0 -17
  182. package/desktop/tsconfig.json +0 -25
  183. package/desktop/vite.config.ts +0 -26
  184. 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
- }