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,229 +0,0 @@
|
|
|
1
|
-
import { useState, useRef, useEffect } from 'react'
|
|
2
|
-
import DOMPurify from 'dompurify'
|
|
3
|
-
import { createPortal } from 'react-dom'
|
|
4
|
-
import { useSettingsStore } from '../../stores/settingsStore'
|
|
5
|
-
import { useChatStore } from '../../stores/chatStore'
|
|
6
|
-
import { useSessionStore } from '../../stores/sessionStore'
|
|
7
|
-
import { useTabStore } from '../../stores/tabStore'
|
|
8
|
-
import { useTranslation } from '../../i18n'
|
|
9
|
-
import type { PermissionMode } from '../../types/settings'
|
|
10
|
-
|
|
11
|
-
const MODE_ICONS: Record<PermissionMode, string> = {
|
|
12
|
-
default: 'verified_user',
|
|
13
|
-
acceptEdits: 'bolt',
|
|
14
|
-
plan: 'architecture',
|
|
15
|
-
bypassPermissions: 'gavel',
|
|
16
|
-
dontAsk: 'gavel',
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
type Props = {
|
|
20
|
-
workDir?: string
|
|
21
|
-
/** Controlled mode: override current value */
|
|
22
|
-
value?: PermissionMode
|
|
23
|
-
/** Controlled mode: called on change instead of updating global store */
|
|
24
|
-
onChange?: (mode: PermissionMode) => void
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function PermissionModeSelector({ workDir: workDirProp, value, onChange }: Props = {}) {
|
|
28
|
-
const t = useTranslation()
|
|
29
|
-
const { permissionMode: storeMode, setPermissionMode } = useSettingsStore()
|
|
30
|
-
const setSessionPermissionMode = useChatStore((s) => s.setSessionPermissionMode)
|
|
31
|
-
const activeTabId = useTabStore((s) => s.activeTabId)
|
|
32
|
-
const sessions = useSessionStore((s) => s.sessions)
|
|
33
|
-
const activeSessionId = useSessionStore((s) => s.activeSessionId)
|
|
34
|
-
const [open, setOpen] = useState(false)
|
|
35
|
-
const [confirmDialog, setConfirmDialog] = useState(false)
|
|
36
|
-
const ref = useRef<HTMLDivElement>(null)
|
|
37
|
-
|
|
38
|
-
const isControlled = value !== undefined
|
|
39
|
-
const currentMode = isControlled ? value : storeMode
|
|
40
|
-
|
|
41
|
-
const PERMISSION_ITEMS: Array<{
|
|
42
|
-
value: PermissionMode
|
|
43
|
-
label: string
|
|
44
|
-
description: string
|
|
45
|
-
icon: string
|
|
46
|
-
color?: string
|
|
47
|
-
}> = [
|
|
48
|
-
{
|
|
49
|
-
value: 'default',
|
|
50
|
-
label: t('permMode.askPermissions'),
|
|
51
|
-
description: t('permMode.askPermDesc'),
|
|
52
|
-
icon: 'verified_user',
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
value: 'acceptEdits',
|
|
56
|
-
label: t('permMode.autoAccept'),
|
|
57
|
-
description: t('permMode.autoAcceptDesc'),
|
|
58
|
-
icon: 'bolt',
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
value: 'plan',
|
|
62
|
-
label: t('permMode.planMode'),
|
|
63
|
-
description: t('permMode.planModeDesc'),
|
|
64
|
-
icon: 'architecture',
|
|
65
|
-
color: 'text-[var(--color-text-tertiary)]',
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
value: 'bypassPermissions',
|
|
69
|
-
label: t('permMode.bypass'),
|
|
70
|
-
description: t('permMode.bypassDesc'),
|
|
71
|
-
icon: 'gavel',
|
|
72
|
-
color: 'text-[var(--color-error)]',
|
|
73
|
-
},
|
|
74
|
-
]
|
|
75
|
-
|
|
76
|
-
const MODE_LABELS: Record<PermissionMode, string> = {
|
|
77
|
-
default: t('permMode.label.default'),
|
|
78
|
-
acceptEdits: t('permMode.label.acceptEdits'),
|
|
79
|
-
plan: t('permMode.label.plan'),
|
|
80
|
-
bypassPermissions: t('permMode.label.bypassPermissions'),
|
|
81
|
-
dontAsk: t('permMode.label.dontAsk'),
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const activeSession = sessions.find((s) => s.id === activeSessionId)
|
|
85
|
-
const workDir = workDirProp || activeSession?.workDir || '~'
|
|
86
|
-
|
|
87
|
-
useEffect(() => {
|
|
88
|
-
if (!open) return
|
|
89
|
-
const handleClick = (e: MouseEvent) => {
|
|
90
|
-
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
|
|
91
|
-
}
|
|
92
|
-
const handleEsc = (e: KeyboardEvent) => {
|
|
93
|
-
if (e.key === 'Escape') setOpen(false)
|
|
94
|
-
}
|
|
95
|
-
document.addEventListener('mousedown', handleClick)
|
|
96
|
-
document.addEventListener('keydown', handleEsc)
|
|
97
|
-
return () => {
|
|
98
|
-
document.removeEventListener('mousedown', handleClick)
|
|
99
|
-
document.removeEventListener('keydown', handleEsc)
|
|
100
|
-
}
|
|
101
|
-
}, [open])
|
|
102
|
-
|
|
103
|
-
return (
|
|
104
|
-
<div ref={ref} className="relative">
|
|
105
|
-
<button
|
|
106
|
-
onClick={() => setOpen(!open)}
|
|
107
|
-
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"
|
|
108
|
-
>
|
|
109
|
-
<span className="material-symbols-outlined text-[14px]">{MODE_ICONS[currentMode]}</span>
|
|
110
|
-
<span>{MODE_LABELS[currentMode]}</span>
|
|
111
|
-
<span className="material-symbols-outlined text-[12px]">expand_more</span>
|
|
112
|
-
</button>
|
|
113
|
-
|
|
114
|
-
{open && (
|
|
115
|
-
<div className="absolute left-0 bottom-full mb-2 w-[320px] rounded-xl bg-[var(--color-surface-container-lowest)] border border-[var(--color-border)] shadow-[var(--shadow-dropdown)] z-50 py-2">
|
|
116
|
-
<div className="px-4 py-2 text-[10px] font-bold uppercase tracking-widest text-[var(--color-outline)]">
|
|
117
|
-
{t('permMode.executionPermissions')}
|
|
118
|
-
</div>
|
|
119
|
-
{PERMISSION_ITEMS.map((item) => (
|
|
120
|
-
<button
|
|
121
|
-
key={item.value}
|
|
122
|
-
onClick={() => {
|
|
123
|
-
if (item.value === 'bypassPermissions') {
|
|
124
|
-
setOpen(false)
|
|
125
|
-
setConfirmDialog(true)
|
|
126
|
-
return
|
|
127
|
-
}
|
|
128
|
-
if (isControlled) {
|
|
129
|
-
onChange?.(item.value)
|
|
130
|
-
} else {
|
|
131
|
-
void setPermissionMode(item.value)
|
|
132
|
-
if (activeTabId) setSessionPermissionMode(activeTabId, item.value)
|
|
133
|
-
}
|
|
134
|
-
setOpen(false)
|
|
135
|
-
}}
|
|
136
|
-
className={`
|
|
137
|
-
w-full flex items-start gap-3 px-4 py-3 text-left transition-colors
|
|
138
|
-
hover:bg-[var(--color-surface-hover)]
|
|
139
|
-
${item.value === currentMode ? 'bg-[var(--color-surface-selected)]' : ''}
|
|
140
|
-
`}
|
|
141
|
-
>
|
|
142
|
-
<span className={`material-symbols-outlined text-[20px] mt-0.5 ${item.color || 'text-[var(--color-text-secondary)]'}`}>
|
|
143
|
-
{item.icon}
|
|
144
|
-
</span>
|
|
145
|
-
<div className="flex-1 min-w-0">
|
|
146
|
-
<div className="text-sm font-semibold text-[var(--color-text-primary)]">{item.label}</div>
|
|
147
|
-
<div className="text-xs text-[var(--color-text-tertiary)] mt-0.5">{item.description}</div>
|
|
148
|
-
</div>
|
|
149
|
-
{item.value === currentMode && (
|
|
150
|
-
<span className="material-symbols-outlined text-[16px] text-[var(--color-brand)] mt-0.5" style={{ fontVariationSettings: "'FILL' 1" }}>
|
|
151
|
-
check_circle
|
|
152
|
-
</span>
|
|
153
|
-
)}
|
|
154
|
-
</button>
|
|
155
|
-
))}
|
|
156
|
-
</div>
|
|
157
|
-
)}
|
|
158
|
-
|
|
159
|
-
{/* Bypass confirmation dialog */}
|
|
160
|
-
{confirmDialog && createPortal(
|
|
161
|
-
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/40 pl-[var(--sidebar-width)]" onClick={() => setConfirmDialog(false)}>
|
|
162
|
-
<div
|
|
163
|
-
className="w-[420px] rounded-2xl bg-[var(--color-surface-container-lowest)] border border-[var(--color-border)] shadow-[var(--shadow-dropdown)] overflow-hidden"
|
|
164
|
-
onClick={(e) => e.stopPropagation()}
|
|
165
|
-
>
|
|
166
|
-
{/* Header */}
|
|
167
|
-
<div className="flex items-center gap-3 px-5 py-4 bg-[var(--color-error)]/8 border-b border-[var(--color-error)]/15">
|
|
168
|
-
<div className="flex items-center justify-center w-10 h-10 rounded-xl bg-[var(--color-error)]/12">
|
|
169
|
-
<span className="material-symbols-outlined text-[22px] text-[var(--color-error)]">warning</span>
|
|
170
|
-
</div>
|
|
171
|
-
<div>
|
|
172
|
-
<div className="text-sm font-bold text-[var(--color-text-primary)]">{t('permMode.enableBypassTitle')}</div>
|
|
173
|
-
<div className="text-xs text-[var(--color-text-tertiary)] mt-0.5">{t('permMode.enableBypassSubtitle')}</div>
|
|
174
|
-
</div>
|
|
175
|
-
</div>
|
|
176
|
-
|
|
177
|
-
{/* Body */}
|
|
178
|
-
<div className="px-5 py-4">
|
|
179
|
-
<p className="text-xs text-[var(--color-text-secondary)] leading-relaxed mb-3" dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(t('permMode.enableBypassBody')) }} />
|
|
180
|
-
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-[var(--color-surface-container)] border border-[var(--color-border)]" title={workDir}>
|
|
181
|
-
<span className="material-symbols-outlined text-[16px] text-[var(--color-text-tertiary)] shrink-0">folder</span>
|
|
182
|
-
<code className="text-xs font-[var(--font-mono)] text-[var(--color-text-primary)] truncate">{workDir}</code>
|
|
183
|
-
</div>
|
|
184
|
-
<ul className="mt-3 space-y-1.5 text-xs text-[var(--color-text-secondary)]">
|
|
185
|
-
<li className="flex items-start gap-2">
|
|
186
|
-
<span className="material-symbols-outlined text-[14px] text-[var(--color-error)] mt-0.5">check</span>
|
|
187
|
-
{t('permMode.permReadWrite')}
|
|
188
|
-
</li>
|
|
189
|
-
<li className="flex items-start gap-2">
|
|
190
|
-
<span className="material-symbols-outlined text-[14px] text-[var(--color-error)] mt-0.5">check</span>
|
|
191
|
-
{t('permMode.permShell')}
|
|
192
|
-
</li>
|
|
193
|
-
<li className="flex items-start gap-2">
|
|
194
|
-
<span className="material-symbols-outlined text-[14px] text-[var(--color-error)] mt-0.5">check</span>
|
|
195
|
-
{t('permMode.permPackages')}
|
|
196
|
-
</li>
|
|
197
|
-
</ul>
|
|
198
|
-
</div>
|
|
199
|
-
|
|
200
|
-
{/* Actions */}
|
|
201
|
-
<div className="flex items-center justify-end gap-2 px-5 py-3 border-t border-[var(--color-border)] bg-[var(--color-surface-container-low)]">
|
|
202
|
-
<button
|
|
203
|
-
onClick={() => setConfirmDialog(false)}
|
|
204
|
-
className="px-4 py-2 text-xs font-semibold text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] rounded-lg transition-colors"
|
|
205
|
-
>
|
|
206
|
-
{t('common.cancel')}
|
|
207
|
-
</button>
|
|
208
|
-
<button
|
|
209
|
-
onClick={() => {
|
|
210
|
-
if (isControlled) {
|
|
211
|
-
onChange?.('bypassPermissions')
|
|
212
|
-
} else {
|
|
213
|
-
void setPermissionMode('bypassPermissions')
|
|
214
|
-
if (activeTabId) setSessionPermissionMode(activeTabId, 'bypassPermissions')
|
|
215
|
-
}
|
|
216
|
-
setConfirmDialog(false)
|
|
217
|
-
}}
|
|
218
|
-
className="px-4 py-2 text-xs font-semibold text-white bg-[var(--color-error)] hover:opacity-90 rounded-lg transition-colors"
|
|
219
|
-
>
|
|
220
|
-
{t('permMode.enableBypassBtn')}
|
|
221
|
-
</button>
|
|
222
|
-
</div>
|
|
223
|
-
</div>
|
|
224
|
-
</div>,
|
|
225
|
-
document.body,
|
|
226
|
-
)}
|
|
227
|
-
</div>
|
|
228
|
-
)
|
|
229
|
-
}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
import { Sidebar } from './Sidebar'
|
|
3
|
-
import { ContentRouter } from './ContentRouter'
|
|
4
|
-
import { ToastContainer } from '../shared/Toast'
|
|
5
|
-
import { UpdateChecker } from '../shared/UpdateChecker'
|
|
6
|
-
import { useSettingsStore } from '../../stores/settingsStore'
|
|
7
|
-
import { useUIStore, type SettingsTab } from '../../stores/uiStore'
|
|
8
|
-
import { useKeyboardShortcuts } from '../../hooks/useKeyboardShortcuts'
|
|
9
|
-
import { initializeDesktopServerUrl } from '../../lib/desktopRuntime'
|
|
10
|
-
import { TabBar } from './TabBar'
|
|
11
|
-
import { useTabStore, SETTINGS_TAB_ID } from '../../stores/tabStore'
|
|
12
|
-
import { useChatStore } from '../../stores/chatStore'
|
|
13
|
-
import { useTranslation } from '../../i18n'
|
|
14
|
-
|
|
15
|
-
export function AppShell() {
|
|
16
|
-
const fetchSettings = useSettingsStore((s) => s.fetchAll)
|
|
17
|
-
const [ready, setReady] = useState(false)
|
|
18
|
-
const [startupError, setStartupError] = useState<string | null>(null)
|
|
19
|
-
const t = useTranslation()
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
let cancelled = false
|
|
23
|
-
|
|
24
|
-
const bootstrap = async () => {
|
|
25
|
-
try {
|
|
26
|
-
await initializeDesktopServerUrl()
|
|
27
|
-
await fetchSettings()
|
|
28
|
-
|
|
29
|
-
// Restore tabs from localStorage
|
|
30
|
-
await useTabStore.getState().restoreTabs()
|
|
31
|
-
const activeId = useTabStore.getState().activeTabId
|
|
32
|
-
if (activeId) {
|
|
33
|
-
useChatStore.getState().connectToSession(activeId)
|
|
34
|
-
}
|
|
35
|
-
if (!cancelled) {
|
|
36
|
-
setReady(true)
|
|
37
|
-
}
|
|
38
|
-
} catch (error) {
|
|
39
|
-
if (!cancelled) {
|
|
40
|
-
setStartupError(error instanceof Error ? error.message : String(error))
|
|
41
|
-
setReady(false)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
void bootstrap()
|
|
47
|
-
|
|
48
|
-
return () => {
|
|
49
|
-
cancelled = true
|
|
50
|
-
}
|
|
51
|
-
}, [fetchSettings])
|
|
52
|
-
|
|
53
|
-
// Listen for macOS native menu navigation events (About / Settings)
|
|
54
|
-
useEffect(() => {
|
|
55
|
-
let unlisten: (() => void) | undefined
|
|
56
|
-
import(/* @vite-ignore */ '@tauri-apps/api/event')
|
|
57
|
-
.then(({ listen }) =>
|
|
58
|
-
listen<string>('native-menu-navigate', (event) => {
|
|
59
|
-
const target = event.payload as SettingsTab | 'settings'
|
|
60
|
-
if (target === 'about') {
|
|
61
|
-
useUIStore.getState().setPendingSettingsTab('about')
|
|
62
|
-
}
|
|
63
|
-
useTabStore.getState().openTab(SETTINGS_TAB_ID, 'Settings', 'settings')
|
|
64
|
-
}),
|
|
65
|
-
)
|
|
66
|
-
.then((fn) => { unlisten = fn })
|
|
67
|
-
.catch(() => {})
|
|
68
|
-
return () => { unlisten?.() }
|
|
69
|
-
}, [])
|
|
70
|
-
|
|
71
|
-
useKeyboardShortcuts()
|
|
72
|
-
|
|
73
|
-
if (startupError) {
|
|
74
|
-
return (
|
|
75
|
-
<div className="h-screen flex items-center justify-center bg-[var(--color-surface)] px-6">
|
|
76
|
-
<div className="max-w-xl rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface-container-low)] p-6">
|
|
77
|
-
<h1 className="text-lg font-semibold text-[var(--color-text-primary)]">
|
|
78
|
-
{t('app.serverFailed')}
|
|
79
|
-
</h1>
|
|
80
|
-
<p className="mt-2 text-sm text-[var(--color-text-secondary)]">
|
|
81
|
-
{startupError}
|
|
82
|
-
</p>
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (!ready) {
|
|
89
|
-
return (
|
|
90
|
-
<div className="h-screen flex items-center justify-center bg-[var(--color-surface)] text-[var(--color-text-secondary)]">
|
|
91
|
-
{t('app.launching')}
|
|
92
|
-
</div>
|
|
93
|
-
)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return (
|
|
97
|
-
<div className="h-screen flex overflow-hidden">
|
|
98
|
-
<Sidebar />
|
|
99
|
-
<main id="content-area" className="flex-1 flex flex-col overflow-hidden">
|
|
100
|
-
<TabBar />
|
|
101
|
-
<ContentRouter />
|
|
102
|
-
</main>
|
|
103
|
-
<ToastContainer />
|
|
104
|
-
<UpdateChecker />
|
|
105
|
-
</div>
|
|
106
|
-
)
|
|
107
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { useTabStore } from '../../stores/tabStore'
|
|
2
|
-
import { EmptySession } from '../../pages/EmptySession'
|
|
3
|
-
import { ActiveSession } from '../../pages/ActiveSession'
|
|
4
|
-
import { ScheduledTasks } from '../../pages/ScheduledTasks'
|
|
5
|
-
import { Settings } from '../../pages/Settings'
|
|
6
|
-
|
|
7
|
-
export function ContentRouter() {
|
|
8
|
-
const activeTabId = useTabStore((s) => s.activeTabId)
|
|
9
|
-
const activeTabType = useTabStore((s) => s.tabs.find((t) => t.sessionId === s.activeTabId)?.type)
|
|
10
|
-
|
|
11
|
-
// No tabs open — show empty session
|
|
12
|
-
if (!activeTabId || !activeTabType) {
|
|
13
|
-
return <EmptySession />
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// Special tabs
|
|
17
|
-
if (activeTabType === 'settings') {
|
|
18
|
-
return <Settings />
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (activeTabType === 'scheduled') {
|
|
22
|
-
return <ScheduledTasks />
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Session tab — ActiveSession handles both regular and member sessions
|
|
26
|
-
return <ActiveSession />
|
|
27
|
-
}
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { useState, useRef, useEffect } from 'react'
|
|
2
|
-
import { useSessionStore } from '../../stores/sessionStore'
|
|
3
|
-
import { useTranslation } from '../../i18n'
|
|
4
|
-
|
|
5
|
-
export function ProjectFilter() {
|
|
6
|
-
const t = useTranslation()
|
|
7
|
-
const { availableProjects, selectedProjects, setSelectedProjects } = useSessionStore()
|
|
8
|
-
const [open, setOpen] = useState(false)
|
|
9
|
-
const ref = useRef<HTMLDivElement>(null)
|
|
10
|
-
|
|
11
|
-
// Close dropdown on click outside
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
if (!open) return
|
|
14
|
-
const close = (e: MouseEvent) => {
|
|
15
|
-
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
|
|
16
|
-
}
|
|
17
|
-
document.addEventListener('mousedown', close)
|
|
18
|
-
return () => document.removeEventListener('mousedown', close)
|
|
19
|
-
}, [open])
|
|
20
|
-
|
|
21
|
-
const isAllSelected = selectedProjects.length === 0
|
|
22
|
-
|
|
23
|
-
const label = isAllSelected
|
|
24
|
-
? t('sidebar.allProjects')
|
|
25
|
-
: selectedProjects.length === 1
|
|
26
|
-
? getDisplayName(selectedProjects[0]!, t('sidebar.other'))
|
|
27
|
-
: `${selectedProjects.length} projects`
|
|
28
|
-
|
|
29
|
-
const toggleProject = (path: string) => {
|
|
30
|
-
if (isAllSelected) {
|
|
31
|
-
// Switch from "all" to "only this one"
|
|
32
|
-
setSelectedProjects([path])
|
|
33
|
-
} else if (selectedProjects.includes(path)) {
|
|
34
|
-
const next = selectedProjects.filter((p) => p !== path)
|
|
35
|
-
// If nothing left, revert to all
|
|
36
|
-
setSelectedProjects(next.length === 0 ? [] : next)
|
|
37
|
-
} else {
|
|
38
|
-
const next = [...selectedProjects, path]
|
|
39
|
-
// If all are selected individually, revert to "all"
|
|
40
|
-
setSelectedProjects(next.length >= availableProjects.length ? [] : next)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const selectAll = () => setSelectedProjects([])
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
<div ref={ref} className="relative">
|
|
48
|
-
<button
|
|
49
|
-
onClick={() => setOpen(!open)}
|
|
50
|
-
className="flex items-center gap-1 px-2 py-1 text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors rounded-[var(--radius-md)] hover:bg-[var(--color-surface-hover)]"
|
|
51
|
-
>
|
|
52
|
-
<span className="truncate max-w-[140px]">{label}</span>
|
|
53
|
-
<ChevronIcon open={open} />
|
|
54
|
-
</button>
|
|
55
|
-
|
|
56
|
-
{open && (
|
|
57
|
-
<div
|
|
58
|
-
className="absolute left-0 top-full mt-1 z-50 min-w-[200px] max-h-[300px] overflow-y-auto bg-[var(--color-surface)] border border-[var(--color-border)] rounded-[var(--radius-md)] py-1"
|
|
59
|
-
style={{ boxShadow: 'var(--shadow-dropdown)' }}
|
|
60
|
-
>
|
|
61
|
-
{/* All projects */}
|
|
62
|
-
<button
|
|
63
|
-
onClick={selectAll}
|
|
64
|
-
className="w-full flex items-center gap-2.5 px-3 py-1.5 text-sm text-left hover:bg-[var(--color-surface-hover)] transition-colors"
|
|
65
|
-
>
|
|
66
|
-
<FolderIcon />
|
|
67
|
-
<span className="flex-1 text-[var(--color-text-primary)]">{t('sidebar.allProjects')}</span>
|
|
68
|
-
{isAllSelected && <CheckIcon />}
|
|
69
|
-
</button>
|
|
70
|
-
|
|
71
|
-
<div className="mx-2 my-1 border-t border-[var(--color-border)]" />
|
|
72
|
-
|
|
73
|
-
{/* Individual projects */}
|
|
74
|
-
{availableProjects.map((path) => {
|
|
75
|
-
const checked = !isAllSelected && selectedProjects.includes(path)
|
|
76
|
-
return (
|
|
77
|
-
<button
|
|
78
|
-
key={path}
|
|
79
|
-
onClick={() => toggleProject(path)}
|
|
80
|
-
className="w-full flex items-center gap-2.5 px-3 py-1.5 text-sm text-left hover:bg-[var(--color-surface-hover)] transition-colors"
|
|
81
|
-
>
|
|
82
|
-
<FolderIcon />
|
|
83
|
-
<span className="flex-1 truncate text-[var(--color-text-primary)]">{getDisplayName(path, t('sidebar.other'))}</span>
|
|
84
|
-
{checked && <CheckIcon />}
|
|
85
|
-
</button>
|
|
86
|
-
)
|
|
87
|
-
})}
|
|
88
|
-
</div>
|
|
89
|
-
)}
|
|
90
|
-
</div>
|
|
91
|
-
)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function getDisplayName(sanitizedPath: string, fallback: string = 'Other'): string {
|
|
95
|
-
if (!sanitizedPath || sanitizedPath === '_unknown') return fallback
|
|
96
|
-
const segments = sanitizedPath.split('-').filter(Boolean)
|
|
97
|
-
return segments[segments.length - 1] || fallback
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function ChevronIcon({ open }: { open: boolean }) {
|
|
101
|
-
return (
|
|
102
|
-
<svg
|
|
103
|
-
width="14" height="14" viewBox="0 0 24 24" fill="none"
|
|
104
|
-
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
|
|
105
|
-
className={`transition-transform ${open ? 'rotate-180' : ''}`}
|
|
106
|
-
>
|
|
107
|
-
<polyline points="6 9 12 15 18 9" />
|
|
108
|
-
</svg>
|
|
109
|
-
)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function FolderIcon() {
|
|
113
|
-
return (
|
|
114
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="text-[var(--color-text-tertiary)] flex-shrink-0">
|
|
115
|
-
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
|
|
116
|
-
</svg>
|
|
117
|
-
)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function CheckIcon() {
|
|
121
|
-
return (
|
|
122
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--color-brand)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="flex-shrink-0">
|
|
123
|
-
<polyline points="20 6 9 17 4 12" />
|
|
124
|
-
</svg>
|
|
125
|
-
)
|
|
126
|
-
}
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
-
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
3
|
-
import '@testing-library/jest-dom'
|
|
4
|
-
|
|
5
|
-
vi.mock('./ProjectFilter', () => ({
|
|
6
|
-
ProjectFilter: () => <div data-testid="project-filter" />,
|
|
7
|
-
}))
|
|
8
|
-
|
|
9
|
-
vi.mock('../../i18n', () => ({
|
|
10
|
-
useTranslation: () => (key: string) => {
|
|
11
|
-
const translations: Record<string, string> = {
|
|
12
|
-
'sidebar.newSession': 'New Session',
|
|
13
|
-
'sidebar.scheduled': 'Scheduled',
|
|
14
|
-
'sidebar.settings': 'Settings',
|
|
15
|
-
'sidebar.searchPlaceholder': 'Search sessions',
|
|
16
|
-
'sidebar.noSessions': 'No sessions',
|
|
17
|
-
'sidebar.noMatching': 'No matching sessions',
|
|
18
|
-
'sidebar.sessionListFailed': 'Session list failed',
|
|
19
|
-
'common.retry': 'Retry',
|
|
20
|
-
'common.delete': 'Delete',
|
|
21
|
-
'common.rename': 'Rename',
|
|
22
|
-
'sidebar.timeGroup.today': 'Today',
|
|
23
|
-
'sidebar.timeGroup.yesterday': 'Yesterday',
|
|
24
|
-
'sidebar.timeGroup.last7days': 'Last 7 Days',
|
|
25
|
-
'sidebar.timeGroup.last30days': 'Last 30 Days',
|
|
26
|
-
'sidebar.timeGroup.older': 'Older',
|
|
27
|
-
'sidebar.missingDir': 'Missing',
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return translations[key] ?? key
|
|
31
|
-
},
|
|
32
|
-
}))
|
|
33
|
-
|
|
34
|
-
import { Sidebar } from './Sidebar'
|
|
35
|
-
import { useChatStore } from '../../stores/chatStore'
|
|
36
|
-
import { useSessionStore } from '../../stores/sessionStore'
|
|
37
|
-
import { useTabStore } from '../../stores/tabStore'
|
|
38
|
-
import { useUIStore } from '../../stores/uiStore'
|
|
39
|
-
|
|
40
|
-
describe('Sidebar', () => {
|
|
41
|
-
const connectToSession = vi.fn()
|
|
42
|
-
const disconnectSession = vi.fn()
|
|
43
|
-
const fetchSessions = vi.fn()
|
|
44
|
-
const createSession = vi.fn()
|
|
45
|
-
const deleteSession = vi.fn()
|
|
46
|
-
const addToast = vi.fn()
|
|
47
|
-
|
|
48
|
-
beforeEach(() => {
|
|
49
|
-
connectToSession.mockReset()
|
|
50
|
-
disconnectSession.mockReset()
|
|
51
|
-
fetchSessions.mockReset()
|
|
52
|
-
createSession.mockReset()
|
|
53
|
-
deleteSession.mockReset()
|
|
54
|
-
addToast.mockReset()
|
|
55
|
-
|
|
56
|
-
useTabStore.setState({ tabs: [], activeTabId: null })
|
|
57
|
-
useSessionStore.setState({
|
|
58
|
-
sessions: [],
|
|
59
|
-
activeSessionId: null,
|
|
60
|
-
isLoading: false,
|
|
61
|
-
error: null,
|
|
62
|
-
selectedProjects: [],
|
|
63
|
-
availableProjects: [],
|
|
64
|
-
fetchSessions,
|
|
65
|
-
createSession,
|
|
66
|
-
deleteSession,
|
|
67
|
-
})
|
|
68
|
-
useChatStore.setState({
|
|
69
|
-
connectToSession,
|
|
70
|
-
disconnectSession,
|
|
71
|
-
} as Partial<ReturnType<typeof useChatStore.getState>>)
|
|
72
|
-
useUIStore.setState({
|
|
73
|
-
addToast,
|
|
74
|
-
} as Partial<ReturnType<typeof useUIStore.getState>>)
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
afterEach(() => {
|
|
78
|
-
useTabStore.setState({ tabs: [], activeTabId: null })
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it('opens a new tab when creating a session from the sidebar', async () => {
|
|
82
|
-
createSession.mockResolvedValue('session-new-1')
|
|
83
|
-
|
|
84
|
-
render(<Sidebar />)
|
|
85
|
-
|
|
86
|
-
await act(async () => {
|
|
87
|
-
fireEvent.click(screen.getByRole('button', { name: 'New Session' }))
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
await waitFor(() => {
|
|
91
|
-
expect(createSession).toHaveBeenCalled()
|
|
92
|
-
expect(connectToSession).toHaveBeenCalledWith('session-new-1')
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
expect(useTabStore.getState().tabs).toEqual([
|
|
96
|
-
{ sessionId: 'session-new-1', title: 'New Session', type: 'session', status: 'idle' },
|
|
97
|
-
])
|
|
98
|
-
expect(useTabStore.getState().activeTabId).toBe('session-new-1')
|
|
99
|
-
expect(screen.getByRole('complementary')).not.toHaveAttribute('data-tauri-drag-region')
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
it('shows a toast when session creation fails', async () => {
|
|
103
|
-
createSession.mockRejectedValue(new Error('boom'))
|
|
104
|
-
|
|
105
|
-
render(<Sidebar />)
|
|
106
|
-
|
|
107
|
-
await act(async () => {
|
|
108
|
-
fireEvent.click(screen.getByRole('button', { name: 'New Session' }))
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
await waitFor(() => {
|
|
112
|
-
expect(addToast).toHaveBeenCalledWith({
|
|
113
|
-
type: 'error',
|
|
114
|
-
message: 'boom',
|
|
115
|
-
})
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
expect(useTabStore.getState().tabs).toEqual([])
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it('removes the matching tab when deleting a session from the sidebar', async () => {
|
|
122
|
-
deleteSession.mockResolvedValue(undefined)
|
|
123
|
-
useSessionStore.setState({
|
|
124
|
-
sessions: [
|
|
125
|
-
{
|
|
126
|
-
id: 'session-1',
|
|
127
|
-
title: 'Open Session',
|
|
128
|
-
createdAt: new Date().toISOString(),
|
|
129
|
-
modifiedAt: new Date().toISOString(),
|
|
130
|
-
messageCount: 1,
|
|
131
|
-
projectPath: '/workspace/project',
|
|
132
|
-
workDir: '/workspace/project',
|
|
133
|
-
workDirExists: true,
|
|
134
|
-
},
|
|
135
|
-
],
|
|
136
|
-
})
|
|
137
|
-
useTabStore.setState({
|
|
138
|
-
tabs: [{ sessionId: 'session-1', title: 'Open Session', type: 'session', status: 'idle' }],
|
|
139
|
-
activeTabId: 'session-1',
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
render(<Sidebar />)
|
|
143
|
-
|
|
144
|
-
fireEvent.contextMenu(screen.getByRole('button', { name: /Open Session/ }))
|
|
145
|
-
|
|
146
|
-
await act(async () => {
|
|
147
|
-
fireEvent.click(screen.getByRole('button', { name: 'Delete' }))
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
await waitFor(() => {
|
|
151
|
-
expect(deleteSession).toHaveBeenCalledWith('session-1')
|
|
152
|
-
expect(disconnectSession).toHaveBeenCalledWith('session-1')
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
expect(useTabStore.getState().tabs).toEqual([])
|
|
156
|
-
expect(useTabStore.getState().activeTabId).toBeNull()
|
|
157
|
-
})
|
|
158
|
-
})
|