bingocode 1.0.3 → 1.0.4
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 +1 -1
- 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,262 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
|
-
import { useChatStore } from '../../stores/chatStore'
|
|
3
|
-
import { useTabStore } from '../../stores/tabStore'
|
|
4
|
-
import { useTranslation } from '../../i18n'
|
|
5
|
-
import type { TranslationKey } from '../../i18n'
|
|
6
|
-
import { Button } from '../shared/Button'
|
|
7
|
-
import { DiffViewer } from './DiffViewer'
|
|
8
|
-
|
|
9
|
-
type Props = {
|
|
10
|
-
requestId: string
|
|
11
|
-
toolName: string
|
|
12
|
-
input: unknown
|
|
13
|
-
description?: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Icons for known tool types.
|
|
18
|
-
* Uses Material Symbols Outlined names.
|
|
19
|
-
*/
|
|
20
|
-
const TOOL_META: Record<string, { icon: string; label: string; color: string }> = {
|
|
21
|
-
Bash: { icon: 'terminal', label: 'Bash', color: 'var(--color-warning)' },
|
|
22
|
-
Edit: { icon: 'edit_note', label: 'Edit File', color: 'var(--color-brand)' },
|
|
23
|
-
Write: { icon: 'edit_document', label: 'Write File', color: 'var(--color-success)' },
|
|
24
|
-
Read: { icon: 'description', label: 'Read File', color: 'var(--color-secondary)' },
|
|
25
|
-
Glob: { icon: 'search', label: 'Glob Search', color: 'var(--color-secondary)' },
|
|
26
|
-
Grep: { icon: 'find_in_page', label: 'Grep Search', color: 'var(--color-secondary)' },
|
|
27
|
-
Agent: { icon: 'smart_toy', label: 'Agent', color: 'var(--color-tertiary)' },
|
|
28
|
-
WebSearch: { icon: 'travel_explore', label: 'Web Search', color: 'var(--color-secondary)' },
|
|
29
|
-
WebFetch: { icon: 'cloud_download', label: 'Web Fetch', color: 'var(--color-secondary)' },
|
|
30
|
-
NotebookEdit: { icon: 'note', label: 'Notebook Edit', color: 'var(--color-brand)' },
|
|
31
|
-
Skill: { icon: 'auto_awesome', label: 'Skill', color: 'var(--color-tertiary)' },
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Extract human-readable detail lines from tool input.
|
|
36
|
-
*/
|
|
37
|
-
function extractToolDetails(toolName: string, input: unknown, t: (key: TranslationKey, params?: Record<string, string | number>) => string): { primary: string; secondary?: string } {
|
|
38
|
-
const obj = (input && typeof input === 'object') ? input as Record<string, unknown> : {}
|
|
39
|
-
|
|
40
|
-
switch (toolName) {
|
|
41
|
-
case 'Bash': {
|
|
42
|
-
const cmd = typeof obj.command === 'string' ? obj.command : ''
|
|
43
|
-
const desc = typeof obj.description === 'string' ? obj.description : undefined
|
|
44
|
-
return { primary: cmd, secondary: desc }
|
|
45
|
-
}
|
|
46
|
-
case 'Edit': {
|
|
47
|
-
const filePath = typeof obj.file_path === 'string' ? obj.file_path : ''
|
|
48
|
-
return { primary: filePath, secondary: obj.old_string ? t('permission.replacingContent') : undefined }
|
|
49
|
-
}
|
|
50
|
-
case 'Write': {
|
|
51
|
-
const filePath = typeof obj.file_path === 'string' ? obj.file_path : ''
|
|
52
|
-
return { primary: filePath }
|
|
53
|
-
}
|
|
54
|
-
case 'Read': {
|
|
55
|
-
const filePath = typeof obj.file_path === 'string' ? obj.file_path : ''
|
|
56
|
-
return { primary: filePath }
|
|
57
|
-
}
|
|
58
|
-
case 'Glob':
|
|
59
|
-
return { primary: typeof obj.pattern === 'string' ? obj.pattern : '' }
|
|
60
|
-
case 'Grep':
|
|
61
|
-
return { primary: typeof obj.pattern === 'string' ? obj.pattern : '' }
|
|
62
|
-
case 'Agent':
|
|
63
|
-
return { primary: typeof obj.description === 'string' ? obj.description : '' }
|
|
64
|
-
case 'WebSearch':
|
|
65
|
-
return { primary: typeof obj.query === 'string' ? obj.query : '' }
|
|
66
|
-
case 'WebFetch':
|
|
67
|
-
return { primary: typeof obj.url === 'string' ? obj.url : '' }
|
|
68
|
-
default:
|
|
69
|
-
return { primary: typeof input === 'string' ? input : JSON.stringify(input, null, 2) }
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function getPermissionTitle(toolName: string, input: unknown, t: (key: TranslationKey, params?: Record<string, string | number>) => string) {
|
|
74
|
-
const obj = (input && typeof input === 'object') ? input as Record<string, unknown> : {}
|
|
75
|
-
const filePath = typeof obj.file_path === 'string' ? obj.file_path : ''
|
|
76
|
-
const fileName = filePath ? filePath.split('/').pop() || filePath : ''
|
|
77
|
-
|
|
78
|
-
switch (toolName) {
|
|
79
|
-
case 'Edit':
|
|
80
|
-
case 'Write':
|
|
81
|
-
return fileName ? t('permission.allowEditFile', { toolName, fileName }) : t('permission.allowEditFileGeneric', { toolName: toolName.toLowerCase() })
|
|
82
|
-
case 'Bash':
|
|
83
|
-
return t('permission.allowBash')
|
|
84
|
-
default:
|
|
85
|
-
return t('permission.allowTool', { toolName })
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function renderPermissionPreview(toolName: string, input: unknown) {
|
|
90
|
-
const obj = (input && typeof input === 'object') ? input as Record<string, unknown> : {}
|
|
91
|
-
const filePath = typeof obj.file_path === 'string' ? obj.file_path : 'file'
|
|
92
|
-
|
|
93
|
-
if (toolName === 'Edit' && typeof obj.old_string === 'string' && typeof obj.new_string === 'string') {
|
|
94
|
-
return <DiffViewer filePath={filePath} oldString={obj.old_string} newString={obj.new_string} />
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (toolName === 'Write' && typeof obj.content === 'string') {
|
|
98
|
-
return <DiffViewer filePath={filePath} oldString="" newString={obj.content} />
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (toolName === 'Bash' && typeof obj.command === 'string') {
|
|
102
|
-
return (
|
|
103
|
-
<div className="overflow-x-auto rounded-[var(--radius-md)] bg-[var(--color-terminal-bg)] px-3 py-2.5">
|
|
104
|
-
<pre className="font-[var(--font-mono)] text-[11px] leading-[1.3] text-[var(--color-terminal-fg)] whitespace-pre-wrap break-words">
|
|
105
|
-
<span className="text-[var(--color-terminal-accent)] select-none">$ </span>{obj.command}
|
|
106
|
-
</pre>
|
|
107
|
-
</div>
|
|
108
|
-
)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return null
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function PermissionDialog({ requestId, toolName, input, description }: Props) {
|
|
115
|
-
const { respondToPermission } = useChatStore()
|
|
116
|
-
const activeTabId = useTabStore((s) => s.activeTabId)
|
|
117
|
-
const pendingPermission = useChatStore((s) => activeTabId ? s.sessions[activeTabId]?.pendingPermission : undefined)
|
|
118
|
-
const t = useTranslation()
|
|
119
|
-
const isPending = pendingPermission?.requestId === requestId
|
|
120
|
-
const [showRaw, setShowRaw] = useState(false)
|
|
121
|
-
|
|
122
|
-
const meta = TOOL_META[toolName] || { icon: 'shield', label: toolName, color: 'var(--color-text-tertiary)' }
|
|
123
|
-
const details = extractToolDetails(toolName, input, t)
|
|
124
|
-
const rawInput = typeof input === 'string' ? input : JSON.stringify(input, null, 2)
|
|
125
|
-
const preview = renderPermissionPreview(toolName, input)
|
|
126
|
-
const title = getPermissionTitle(toolName, input, t)
|
|
127
|
-
const allowRawToggle = !preview
|
|
128
|
-
|
|
129
|
-
return (
|
|
130
|
-
<div className={`mb-4 ml-10 overflow-hidden rounded-[var(--radius-lg)] border ${
|
|
131
|
-
isPending
|
|
132
|
-
? 'border-[var(--color-warning)] bg-[var(--color-surface-container-lowest)]'
|
|
133
|
-
: 'border-[var(--color-outline-variant)]/40 bg-[var(--color-surface-container-low)] opacity-70'
|
|
134
|
-
}`}>
|
|
135
|
-
{/* Header */}
|
|
136
|
-
<div className={`flex items-center gap-3 px-4 py-3 ${
|
|
137
|
-
isPending
|
|
138
|
-
? 'bg-[var(--color-surface-container)]'
|
|
139
|
-
: 'bg-[var(--color-surface-container-low)]'
|
|
140
|
-
}`}>
|
|
141
|
-
<div
|
|
142
|
-
className="flex items-center justify-center w-8 h-8 rounded-[var(--radius-md)]"
|
|
143
|
-
style={{ backgroundColor: `${meta.color}18` }}
|
|
144
|
-
>
|
|
145
|
-
<span
|
|
146
|
-
className="material-symbols-outlined text-[18px]"
|
|
147
|
-
style={{ color: meta.color }}
|
|
148
|
-
>
|
|
149
|
-
{meta.icon}
|
|
150
|
-
</span>
|
|
151
|
-
</div>
|
|
152
|
-
<div className="flex-1 min-w-0">
|
|
153
|
-
<div className="flex items-center gap-2">
|
|
154
|
-
<span className="text-sm font-semibold text-[var(--color-text-primary)]">
|
|
155
|
-
{title}
|
|
156
|
-
</span>
|
|
157
|
-
{isPending && (
|
|
158
|
-
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider bg-[var(--color-warning)]/15 text-[var(--color-warning)]">
|
|
159
|
-
<span className="w-1.5 h-1.5 rounded-full bg-[var(--color-warning)] animate-pulse-dot" />
|
|
160
|
-
{t('permission.awaitingApproval')}
|
|
161
|
-
</span>
|
|
162
|
-
)}
|
|
163
|
-
{!isPending && (
|
|
164
|
-
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider bg-[var(--color-surface-container-high)] text-[var(--color-text-tertiary)]">
|
|
165
|
-
{t('permission.responded')}
|
|
166
|
-
</span>
|
|
167
|
-
)}
|
|
168
|
-
</div>
|
|
169
|
-
{description && (
|
|
170
|
-
<p className="mt-0.5 text-xs text-[var(--color-text-secondary)] truncate">{description}</p>
|
|
171
|
-
)}
|
|
172
|
-
</div>
|
|
173
|
-
</div>
|
|
174
|
-
|
|
175
|
-
{/* Tool details */}
|
|
176
|
-
<div className="border-t border-[var(--color-outline-variant)]/20 px-4 py-3">
|
|
177
|
-
{preview ? (
|
|
178
|
-
<div className="space-y-2">
|
|
179
|
-
{details.primary && toolName !== 'Bash' ? (
|
|
180
|
-
<div className="flex items-center gap-2 rounded-[var(--radius-md)] bg-[var(--color-surface-container)] px-3 py-2 text-xs font-[var(--font-mono)] text-[var(--color-text-secondary)]">
|
|
181
|
-
<span className="material-symbols-outlined text-[14px] text-[var(--color-outline)] flex-shrink-0">
|
|
182
|
-
folder_open
|
|
183
|
-
</span>
|
|
184
|
-
<span className="truncate">{details.primary}</span>
|
|
185
|
-
</div>
|
|
186
|
-
) : null}
|
|
187
|
-
{preview}
|
|
188
|
-
</div>
|
|
189
|
-
) : details.primary ? (
|
|
190
|
-
<div className="mb-2">
|
|
191
|
-
<div className="flex items-center gap-2 rounded-[var(--radius-md)] bg-[var(--color-surface-container)] px-3 py-2 text-xs font-[var(--font-mono)] text-[var(--color-text-secondary)]">
|
|
192
|
-
<span className="material-symbols-outlined text-[14px] text-[var(--color-outline)] flex-shrink-0">
|
|
193
|
-
{toolName === 'Glob' || toolName === 'Grep' ? 'search' : 'folder_open'}
|
|
194
|
-
</span>
|
|
195
|
-
<span className="truncate">{details.primary}</span>
|
|
196
|
-
</div>
|
|
197
|
-
</div>
|
|
198
|
-
) : null}
|
|
199
|
-
|
|
200
|
-
{/* Secondary detail */}
|
|
201
|
-
{details.secondary && (
|
|
202
|
-
<p className="mt-2 text-xs text-[var(--color-text-tertiary)]">{details.secondary}</p>
|
|
203
|
-
)}
|
|
204
|
-
|
|
205
|
-
{allowRawToggle && (
|
|
206
|
-
<button
|
|
207
|
-
onClick={() => setShowRaw(!showRaw)}
|
|
208
|
-
className="mt-2 flex cursor-pointer items-center gap-1 text-[11px] text-[var(--color-text-accent)] hover:underline"
|
|
209
|
-
>
|
|
210
|
-
<span className="material-symbols-outlined text-[14px]">
|
|
211
|
-
{showRaw ? 'expand_less' : 'expand_more'}
|
|
212
|
-
</span>
|
|
213
|
-
{showRaw ? t('permission.hideDetails') : t('permission.showFullInput')}
|
|
214
|
-
</button>
|
|
215
|
-
)}
|
|
216
|
-
|
|
217
|
-
{allowRawToggle && showRaw && (
|
|
218
|
-
<pre className="mt-2 max-h-[220px] overflow-y-auto overflow-x-auto rounded-[var(--radius-md)] bg-[var(--color-terminal-bg)] px-3 py-2.5 font-[var(--font-mono)] text-[11px] leading-[1.3] text-[var(--color-terminal-fg)] whitespace-pre-wrap break-words">
|
|
219
|
-
{rawInput}
|
|
220
|
-
</pre>
|
|
221
|
-
)}
|
|
222
|
-
</div>
|
|
223
|
-
|
|
224
|
-
{/* Action buttons */}
|
|
225
|
-
{isPending && (
|
|
226
|
-
<div className="flex items-center gap-2 border-t border-[var(--color-outline-variant)]/20 bg-[var(--color-surface-container-low)] px-4 py-3">
|
|
227
|
-
<Button
|
|
228
|
-
variant="primary"
|
|
229
|
-
size="sm"
|
|
230
|
-
onClick={() => activeTabId && respondToPermission(activeTabId, requestId, true)}
|
|
231
|
-
icon={
|
|
232
|
-
<span className="material-symbols-outlined text-[14px]">check</span>
|
|
233
|
-
}
|
|
234
|
-
>
|
|
235
|
-
{t('permission.allow')}
|
|
236
|
-
</Button>
|
|
237
|
-
<Button
|
|
238
|
-
variant="ghost"
|
|
239
|
-
size="sm"
|
|
240
|
-
onClick={() => activeTabId && respondToPermission(activeTabId, requestId, true, 'always')}
|
|
241
|
-
icon={
|
|
242
|
-
<span className="material-symbols-outlined text-[14px]">verified</span>
|
|
243
|
-
}
|
|
244
|
-
>
|
|
245
|
-
{t('permission.allowForSession')}
|
|
246
|
-
</Button>
|
|
247
|
-
<div className="flex-1" />
|
|
248
|
-
<Button
|
|
249
|
-
variant="danger"
|
|
250
|
-
size="sm"
|
|
251
|
-
onClick={() => activeTabId && respondToPermission(activeTabId, requestId, false)}
|
|
252
|
-
icon={
|
|
253
|
-
<span className="material-symbols-outlined text-[14px]">close</span>
|
|
254
|
-
}
|
|
255
|
-
>
|
|
256
|
-
{t('permission.deny')}
|
|
257
|
-
</Button>
|
|
258
|
-
</div>
|
|
259
|
-
)}
|
|
260
|
-
</div>
|
|
261
|
-
)
|
|
262
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
-
import { act, fireEvent, render, screen } from '@testing-library/react'
|
|
3
|
-
import '@testing-library/jest-dom'
|
|
4
|
-
import { SessionTaskBar } from './SessionTaskBar'
|
|
5
|
-
import { useCLITaskStore } from '../../stores/cliTaskStore'
|
|
6
|
-
|
|
7
|
-
vi.mock('../../i18n', () => ({
|
|
8
|
-
useTranslation: () => (key: string) => {
|
|
9
|
-
const translations: Record<string, string> = {
|
|
10
|
-
'tasks.title': 'Tasks',
|
|
11
|
-
'tasks.dismissCompleted': 'Hide completed tasks',
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return translations[key] ?? key
|
|
15
|
-
},
|
|
16
|
-
}))
|
|
17
|
-
|
|
18
|
-
describe('SessionTaskBar', () => {
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
useCLITaskStore.setState({
|
|
21
|
-
sessionId: 'session-1',
|
|
22
|
-
tasks: [],
|
|
23
|
-
expanded: false,
|
|
24
|
-
completedAndDismissed: false,
|
|
25
|
-
dismissedCompletionKey: null,
|
|
26
|
-
})
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
afterEach(() => {
|
|
30
|
-
useCLITaskStore.getState().clearTasks()
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('only shows the dismiss button once every task is completed', () => {
|
|
34
|
-
act(() => {
|
|
35
|
-
useCLITaskStore.getState().setTasksFromTodos([
|
|
36
|
-
{ content: 'first', status: 'completed' },
|
|
37
|
-
{ content: 'second', status: 'in_progress', activeForm: 'working' },
|
|
38
|
-
])
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
act(() => {
|
|
42
|
-
render(<SessionTaskBar />)
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
expect(screen.getByText('Tasks')).toBeInTheDocument()
|
|
46
|
-
expect(screen.queryByRole('button', { name: 'Hide completed tasks' })).toBeNull()
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('hides the bar after dismissing a completed task set', () => {
|
|
50
|
-
act(() => {
|
|
51
|
-
useCLITaskStore.getState().setTasksFromTodos([
|
|
52
|
-
{ content: 'first', status: 'completed' },
|
|
53
|
-
{ content: 'second', status: 'completed' },
|
|
54
|
-
])
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
act(() => {
|
|
58
|
-
render(<SessionTaskBar />)
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
fireEvent.click(screen.getByRole('button', { name: 'Hide completed tasks' }))
|
|
62
|
-
|
|
63
|
-
expect(screen.queryByText('Tasks')).toBeNull()
|
|
64
|
-
expect(useCLITaskStore.getState().completedAndDismissed).toBe(true)
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('shows the bar again for a new task cycle after a previous completed set was dismissed', () => {
|
|
68
|
-
act(() => {
|
|
69
|
-
useCLITaskStore.getState().setTasksFromTodos([
|
|
70
|
-
{ content: 'first', status: 'completed' },
|
|
71
|
-
])
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
act(() => {
|
|
75
|
-
render(<SessionTaskBar />)
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
fireEvent.click(screen.getByRole('button', { name: 'Hide completed tasks' }))
|
|
79
|
-
expect(screen.queryByText('Tasks')).toBeNull()
|
|
80
|
-
|
|
81
|
-
act(() => {
|
|
82
|
-
useCLITaskStore.getState().setTasksFromTodos([
|
|
83
|
-
{ content: 'next task', status: 'in_progress', activeForm: 'running next task' },
|
|
84
|
-
])
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
expect(screen.getByText('Tasks')).toBeInTheDocument()
|
|
88
|
-
expect(screen.queryByRole('button', { name: 'Hide completed tasks' })).toBeNull()
|
|
89
|
-
|
|
90
|
-
act(() => {
|
|
91
|
-
useCLITaskStore.getState().setTasksFromTodos([
|
|
92
|
-
{ content: 'next task', status: 'completed' },
|
|
93
|
-
])
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
expect(screen.getByText('Tasks')).toBeInTheDocument()
|
|
97
|
-
expect(screen.getByRole('button', { name: 'Hide completed tasks' })).toBeInTheDocument()
|
|
98
|
-
})
|
|
99
|
-
})
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import { useCLITaskStore } from '../../stores/cliTaskStore'
|
|
2
|
-
import { useTranslation } from '../../i18n'
|
|
3
|
-
import type { CLITask } from '../../types/cliTask'
|
|
4
|
-
|
|
5
|
-
const statusConfig = {
|
|
6
|
-
pending: {
|
|
7
|
-
icon: 'radio_button_unchecked',
|
|
8
|
-
color: 'var(--color-text-tertiary)',
|
|
9
|
-
label: 'pending',
|
|
10
|
-
},
|
|
11
|
-
in_progress: {
|
|
12
|
-
icon: 'pending',
|
|
13
|
-
color: 'var(--color-warning)',
|
|
14
|
-
label: 'active',
|
|
15
|
-
},
|
|
16
|
-
completed: {
|
|
17
|
-
icon: 'check_circle',
|
|
18
|
-
color: 'var(--color-success)',
|
|
19
|
-
label: 'done',
|
|
20
|
-
},
|
|
21
|
-
} as const
|
|
22
|
-
|
|
23
|
-
export function SessionTaskBar() {
|
|
24
|
-
const {
|
|
25
|
-
tasks,
|
|
26
|
-
expanded,
|
|
27
|
-
toggleExpanded,
|
|
28
|
-
completedAndDismissed,
|
|
29
|
-
markCompletedAndDismissed,
|
|
30
|
-
} = useCLITaskStore()
|
|
31
|
-
const t = useTranslation()
|
|
32
|
-
|
|
33
|
-
if (tasks.length === 0) return null
|
|
34
|
-
|
|
35
|
-
// Don't show sticky bar if tasks were completed and the user already continued chatting
|
|
36
|
-
const allCompleted = tasks.every((tk) => tk.status === 'completed')
|
|
37
|
-
if (allCompleted && completedAndDismissed) return null
|
|
38
|
-
|
|
39
|
-
const completedCount = tasks.filter((tk) => tk.status === 'completed').length
|
|
40
|
-
const totalCount = tasks.length
|
|
41
|
-
const progressPercent = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0
|
|
42
|
-
|
|
43
|
-
return (
|
|
44
|
-
<div className="shrink-0 px-8">
|
|
45
|
-
<div className="mx-auto max-w-[860px] rounded-[var(--radius-lg)] border border-[var(--color-outline-variant)]/40 bg-[var(--color-surface-container-lowest)] overflow-hidden mb-2">
|
|
46
|
-
{/* Header — always visible, clickable to toggle */}
|
|
47
|
-
<div className="flex items-center gap-2 bg-[var(--color-surface-container)] px-2 py-1.5">
|
|
48
|
-
<button
|
|
49
|
-
type="button"
|
|
50
|
-
onClick={toggleExpanded}
|
|
51
|
-
className="flex min-w-0 flex-1 items-center gap-3 rounded-[var(--radius-md)] px-2 py-1 hover:bg-[var(--color-surface-container-low)] transition-colors"
|
|
52
|
-
>
|
|
53
|
-
<div className="flex items-center justify-center w-6 h-6 rounded-[var(--radius-md)] bg-[var(--color-secondary)]/10">
|
|
54
|
-
<span
|
|
55
|
-
className="material-symbols-outlined text-[14px] text-[var(--color-secondary)]"
|
|
56
|
-
>
|
|
57
|
-
checklist
|
|
58
|
-
</span>
|
|
59
|
-
</div>
|
|
60
|
-
|
|
61
|
-
<span className="text-xs font-semibold text-[var(--color-text-primary)]">
|
|
62
|
-
{t('tasks.title')}
|
|
63
|
-
</span>
|
|
64
|
-
|
|
65
|
-
{/* Progress bar */}
|
|
66
|
-
<div className="flex-1 h-1.5 rounded-full bg-[var(--color-border)] overflow-hidden max-w-[200px]">
|
|
67
|
-
<div
|
|
68
|
-
className="h-full rounded-full transition-all duration-300"
|
|
69
|
-
style={{
|
|
70
|
-
width: `${progressPercent}%`,
|
|
71
|
-
backgroundColor: completedCount === totalCount
|
|
72
|
-
? 'var(--color-success)'
|
|
73
|
-
: 'var(--color-brand)',
|
|
74
|
-
}}
|
|
75
|
-
/>
|
|
76
|
-
</div>
|
|
77
|
-
|
|
78
|
-
<span className="text-[10px] text-[var(--color-text-tertiary)] tabular-nums">
|
|
79
|
-
{completedCount}/{totalCount}
|
|
80
|
-
</span>
|
|
81
|
-
|
|
82
|
-
<span
|
|
83
|
-
className="material-symbols-outlined text-[14px] text-[var(--color-text-tertiary)] transition-transform duration-200"
|
|
84
|
-
style={{ transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)' }}
|
|
85
|
-
>
|
|
86
|
-
expand_less
|
|
87
|
-
</span>
|
|
88
|
-
</button>
|
|
89
|
-
|
|
90
|
-
{allCompleted && (
|
|
91
|
-
<button
|
|
92
|
-
type="button"
|
|
93
|
-
aria-label={t('tasks.dismissCompleted')}
|
|
94
|
-
onClick={markCompletedAndDismissed}
|
|
95
|
-
className="flex shrink-0 items-center justify-center rounded-[var(--radius-md)] p-1.5 text-[var(--color-text-tertiary)] hover:bg-[var(--color-surface-container-low)] hover:text-[var(--color-text-primary)] transition-colors"
|
|
96
|
-
>
|
|
97
|
-
<span className="material-symbols-outlined text-[16px]">close</span>
|
|
98
|
-
</button>
|
|
99
|
-
)}
|
|
100
|
-
</div>
|
|
101
|
-
|
|
102
|
-
{/* Expanded task list */}
|
|
103
|
-
{expanded && (
|
|
104
|
-
<div className="px-4 pb-2 pt-1 flex flex-col gap-0.5 max-h-[240px] overflow-y-auto border-t border-[var(--color-outline-variant)]/20">
|
|
105
|
-
{tasks.map((task) => (
|
|
106
|
-
<TaskItem key={task.id} task={task} />
|
|
107
|
-
))}
|
|
108
|
-
</div>
|
|
109
|
-
)}
|
|
110
|
-
</div>
|
|
111
|
-
</div>
|
|
112
|
-
)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function TaskItem({ task }: { task: CLITask }) {
|
|
116
|
-
const config = statusConfig[task.status]
|
|
117
|
-
|
|
118
|
-
return (
|
|
119
|
-
<div className="flex items-start gap-2 py-1.5 px-1 rounded-md">
|
|
120
|
-
<span
|
|
121
|
-
className="material-symbols-outlined text-[16px] mt-px shrink-0"
|
|
122
|
-
style={{ color: config.color, fontVariationSettings: "'FILL' 1" }}
|
|
123
|
-
>
|
|
124
|
-
{config.icon}
|
|
125
|
-
</span>
|
|
126
|
-
|
|
127
|
-
<div className="flex-1 min-w-0">
|
|
128
|
-
<div className="flex items-center gap-1.5">
|
|
129
|
-
<span className="text-[10px] font-mono text-[var(--color-text-tertiary)]">
|
|
130
|
-
#{task.id}
|
|
131
|
-
</span>
|
|
132
|
-
<span className={`text-xs ${
|
|
133
|
-
task.status === 'completed'
|
|
134
|
-
? 'text-[var(--color-text-tertiary)] line-through'
|
|
135
|
-
: 'text-[var(--color-text-primary)]'
|
|
136
|
-
}`}>
|
|
137
|
-
{task.subject}
|
|
138
|
-
</span>
|
|
139
|
-
</div>
|
|
140
|
-
|
|
141
|
-
{task.status === 'in_progress' && task.activeForm && (
|
|
142
|
-
<div className="flex items-center gap-1 mt-0.5">
|
|
143
|
-
<span className="w-1.5 h-1.5 rounded-full bg-[var(--color-warning)] animate-pulse" />
|
|
144
|
-
<span className="text-[10px] text-[var(--color-warning)]">
|
|
145
|
-
{task.activeForm}
|
|
146
|
-
</span>
|
|
147
|
-
</div>
|
|
148
|
-
)}
|
|
149
|
-
|
|
150
|
-
{task.owner && (
|
|
151
|
-
<span className="text-[10px] text-[var(--color-text-tertiary)] mt-0.5 inline-flex items-center gap-0.5">
|
|
152
|
-
<span className="material-symbols-outlined text-[10px]">person</span>
|
|
153
|
-
{task.owner}
|
|
154
|
-
</span>
|
|
155
|
-
)}
|
|
156
|
-
</div>
|
|
157
|
-
</div>
|
|
158
|
-
)
|
|
159
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { useChatStore } from '../../stores/chatStore'
|
|
2
|
-
import { useTabStore } from '../../stores/tabStore'
|
|
3
|
-
|
|
4
|
-
function formatElapsed(seconds: number): string {
|
|
5
|
-
if (seconds < 60) return `${seconds}s`
|
|
6
|
-
const m = Math.floor(seconds / 60)
|
|
7
|
-
const s = seconds % 60
|
|
8
|
-
return `${m}m ${s}s`
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function StreamingIndicator() {
|
|
12
|
-
const activeTabId = useTabStore((s) => s.activeTabId)
|
|
13
|
-
const sessionState = useChatStore((s) => activeTabId ? s.sessions[activeTabId] : undefined)
|
|
14
|
-
const chatState = sessionState?.chatState ?? 'idle'
|
|
15
|
-
const statusVerb = sessionState?.statusVerb ?? ''
|
|
16
|
-
const elapsedSeconds = sessionState?.elapsedSeconds ?? 0
|
|
17
|
-
const tokenUsage = sessionState?.tokenUsage ?? { input_tokens: 0, output_tokens: 0 }
|
|
18
|
-
let verb: string
|
|
19
|
-
if (statusVerb) {
|
|
20
|
-
verb = statusVerb
|
|
21
|
-
} else {
|
|
22
|
-
verb = chatState === 'thinking' ? 'Thinking' : chatState === 'tool_executing' ? 'Running' : 'Working'
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<div className="mb-2 ml-10 flex w-fit items-center gap-2 rounded-full border border-[var(--color-border)]/40 bg-[var(--color-surface-container-low)] px-3 py-1">
|
|
27
|
-
<span className="text-[var(--color-brand)] animate-shimmer text-xs">✦</span>
|
|
28
|
-
<span className="text-xs font-medium text-[var(--color-text-secondary)]">{verb}...</span>
|
|
29
|
-
{elapsedSeconds > 0 && (
|
|
30
|
-
<span className="text-[10px] text-[var(--color-text-tertiary)]">
|
|
31
|
-
{formatElapsed(elapsedSeconds)}
|
|
32
|
-
</span>
|
|
33
|
-
)}
|
|
34
|
-
{tokenUsage.output_tokens > 0 && (
|
|
35
|
-
<span className="text-[10px] text-[var(--color-text-tertiary)]">
|
|
36
|
-
· ↓ {tokenUsage.output_tokens}
|
|
37
|
-
</span>
|
|
38
|
-
)}
|
|
39
|
-
</div>
|
|
40
|
-
)
|
|
41
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from 'react'
|
|
2
|
-
|
|
3
|
-
type Props = {
|
|
4
|
-
title?: string
|
|
5
|
-
children: ReactNode
|
|
6
|
-
className?: string
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* macOS-style terminal window decoration with traffic light buttons.
|
|
11
|
-
* Reusable wrapper for Bash commands, tool results, and code viewers.
|
|
12
|
-
*/
|
|
13
|
-
export function TerminalChrome({ title, children, className = '' }: Props) {
|
|
14
|
-
return (
|
|
15
|
-
<div className={`overflow-hidden rounded-2xl border border-[var(--color-outline-variant)]/20 bg-[var(--color-surface-dim)] ${className}`}>
|
|
16
|
-
{/* Title bar with traffic lights */}
|
|
17
|
-
<div className="flex items-center gap-2 border-b border-[var(--color-terminal-border)] bg-[var(--color-terminal-header)] px-3 py-2">
|
|
18
|
-
<div className="flex gap-1.5">
|
|
19
|
-
<div className="w-2.5 h-2.5 rounded-full bg-[var(--color-terminal-danger)]" />
|
|
20
|
-
<div className="w-2.5 h-2.5 rounded-full bg-[var(--color-terminal-warning)]" />
|
|
21
|
-
<div className="w-2.5 h-2.5 rounded-full bg-[var(--color-terminal-accent)]" />
|
|
22
|
-
</div>
|
|
23
|
-
{title && (
|
|
24
|
-
<span className="ml-2 truncate font-[var(--font-mono)] text-[10px] text-[var(--color-terminal-muted)]">
|
|
25
|
-
{title}
|
|
26
|
-
</span>
|
|
27
|
-
)}
|
|
28
|
-
</div>
|
|
29
|
-
{/* Content */}
|
|
30
|
-
<div className="bg-[var(--color-terminal-bg)] text-[var(--color-terminal-fg)]">
|
|
31
|
-
{children}
|
|
32
|
-
</div>
|
|
33
|
-
</div>
|
|
34
|
-
)
|
|
35
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useRef } from 'react'
|
|
2
|
-
import { useTranslation } from '../../i18n'
|
|
3
|
-
|
|
4
|
-
export function ThinkingBlock({ content, isActive = false }: { content: string; isActive?: boolean }) {
|
|
5
|
-
const t = useTranslation()
|
|
6
|
-
const [expanded, setExpanded] = useState(false)
|
|
7
|
-
const contentRef = useRef<HTMLDivElement>(null)
|
|
8
|
-
|
|
9
|
-
useEffect(() => {
|
|
10
|
-
if (expanded && isActive && contentRef.current) {
|
|
11
|
-
contentRef.current.scrollTop = contentRef.current.scrollHeight
|
|
12
|
-
}
|
|
13
|
-
}, [content, expanded, isActive])
|
|
14
|
-
|
|
15
|
-
// Preview: take first meaningful line, not first 140 chars
|
|
16
|
-
const lines = content.split('\n').filter((l) => l.trim())
|
|
17
|
-
const firstLine = lines[0]?.replace(/\s+/g, ' ').trim() || ''
|
|
18
|
-
const preview = firstLine.length > 80 ? firstLine.slice(0, 80) + '...' : firstLine
|
|
19
|
-
|
|
20
|
-
return (
|
|
21
|
-
<div className="mb-1 ml-10">
|
|
22
|
-
<style>{thinkingStyles}</style>
|
|
23
|
-
<button
|
|
24
|
-
onClick={() => setExpanded((v) => !v)}
|
|
25
|
-
className="flex w-full items-center gap-1.5 rounded-md px-1 py-0.5 text-left text-[12px] text-[var(--color-text-tertiary)] transition-colors hover:text-[var(--color-text-secondary)]"
|
|
26
|
-
>
|
|
27
|
-
<span className="text-[10px] text-[var(--color-outline)]">
|
|
28
|
-
{expanded ? '\u25BE' : '\u25B8'}
|
|
29
|
-
</span>
|
|
30
|
-
<span className="shrink-0 font-medium italic">
|
|
31
|
-
{t('thinking.label')}
|
|
32
|
-
{isActive && <span className="thinking-dots" />}
|
|
33
|
-
</span>
|
|
34
|
-
{!expanded && preview && (
|
|
35
|
-
<span className="min-w-0 flex-1 truncate font-[var(--font-mono)] text-[11px] text-[var(--color-text-tertiary)]">
|
|
36
|
-
{preview}
|
|
37
|
-
{isActive && <span className="thinking-inline-cursor" />}
|
|
38
|
-
</span>
|
|
39
|
-
)}
|
|
40
|
-
</button>
|
|
41
|
-
{expanded && (
|
|
42
|
-
<div
|
|
43
|
-
ref={contentRef}
|
|
44
|
-
className="mt-1 max-h-[300px] overflow-y-auto rounded-lg border border-[var(--color-border)]/40 bg-[var(--color-surface-container-lowest)] p-2.5 font-[var(--font-mono)] text-[11px] leading-[1.35] text-[var(--color-text-secondary)] whitespace-pre-wrap break-words"
|
|
45
|
-
>
|
|
46
|
-
{content}
|
|
47
|
-
{isActive && expanded && <span className="thinking-cursor" />}
|
|
48
|
-
</div>
|
|
49
|
-
)}
|
|
50
|
-
</div>
|
|
51
|
-
)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const thinkingStyles = `
|
|
55
|
-
@keyframes thinking-cursor-blink {
|
|
56
|
-
0%, 100% { opacity: 1; }
|
|
57
|
-
50% { opacity: 0; }
|
|
58
|
-
}
|
|
59
|
-
@keyframes thinking-dots {
|
|
60
|
-
0%, 20% { content: ''; }
|
|
61
|
-
40% { content: '.'; }
|
|
62
|
-
60% { content: '..'; }
|
|
63
|
-
80%, 100% { content: '...'; }
|
|
64
|
-
}
|
|
65
|
-
.thinking-cursor {
|
|
66
|
-
display: inline-block;
|
|
67
|
-
width: 2px;
|
|
68
|
-
height: 1em;
|
|
69
|
-
background: var(--color-text-tertiary);
|
|
70
|
-
vertical-align: middle;
|
|
71
|
-
margin-left: 1px;
|
|
72
|
-
animation: thinking-cursor-blink 1s step-end infinite;
|
|
73
|
-
}
|
|
74
|
-
.thinking-inline-cursor {
|
|
75
|
-
display: inline-block;
|
|
76
|
-
width: 1px;
|
|
77
|
-
height: 0.95em;
|
|
78
|
-
margin-left: 3px;
|
|
79
|
-
vertical-align: text-bottom;
|
|
80
|
-
background: var(--color-text-tertiary);
|
|
81
|
-
animation: thinking-cursor-blink 1s step-end infinite;
|
|
82
|
-
}
|
|
83
|
-
.thinking-dots::after {
|
|
84
|
-
content: '';
|
|
85
|
-
animation: thinking-dots 1.4s steps(1, end) infinite;
|
|
86
|
-
}
|
|
87
|
-
`
|