@mseep/anything-analyzer 3.6.50

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 (172) hide show
  1. package/.codeartsdoer/.codebaseignore +0 -0
  2. package/.codeartsdoer/AGENTS.md +12 -0
  3. package/.github/workflows/build.yml +146 -0
  4. package/README.en.md +264 -0
  5. package/README.md +276 -0
  6. package/RELEASE_NOTES.md +16 -0
  7. package/USAGE.md +490 -0
  8. package/color-preview-r3.html +414 -0
  9. package/color-preview.html +414 -0
  10. package/dev-app-update.yml +3 -0
  11. package/electron-builder.yml +36 -0
  12. package/electron.vite.config.ts +40 -0
  13. package/package.json +53 -0
  14. package/report-2026-04-13-copilot-claude-sonnet-4.6.md +955 -0
  15. package/resources/doloffer-logo.png +0 -0
  16. package/resources/entitlements.mac.plist +12 -0
  17. package/resources/icon.ico +0 -0
  18. package/resources/icon.png +0 -0
  19. package/src/main/ai/ai-analyzer.ts +517 -0
  20. package/src/main/ai/crypto-script-extractor.ts +206 -0
  21. package/src/main/ai/data-assembler.ts +205 -0
  22. package/src/main/ai/llm-router.ts +1120 -0
  23. package/src/main/ai/prompt-builder.ts +349 -0
  24. package/src/main/ai/scene-detector.ts +302 -0
  25. package/src/main/capture/capture-engine.ts +130 -0
  26. package/src/main/capture/interaction-recorder.ts +171 -0
  27. package/src/main/capture/js-injector.ts +57 -0
  28. package/src/main/capture/replay-engine.ts +256 -0
  29. package/src/main/capture/storage-collector.ts +76 -0
  30. package/src/main/cdp/cdp-manager.ts +233 -0
  31. package/src/main/db/database.ts +41 -0
  32. package/src/main/db/migrations.ts +235 -0
  33. package/src/main/db/repositories.ts +574 -0
  34. package/src/main/fingerprint/http-spoofing.ts +48 -0
  35. package/src/main/fingerprint/presets.ts +173 -0
  36. package/src/main/fingerprint/profile-generator.ts +115 -0
  37. package/src/main/fingerprint/profile-store.ts +52 -0
  38. package/src/main/index.ts +260 -0
  39. package/src/main/ipc.ts +856 -0
  40. package/src/main/logger.ts +42 -0
  41. package/src/main/mcp/mcp-config.ts +66 -0
  42. package/src/main/mcp/mcp-manager.ts +155 -0
  43. package/src/main/mcp/mcp-server.ts +1038 -0
  44. package/src/main/prompt-templates.ts +170 -0
  45. package/src/main/proxy/ca-manager.ts +204 -0
  46. package/src/main/proxy/cert-download-page.ts +171 -0
  47. package/src/main/proxy/cert-installer.ts +242 -0
  48. package/src/main/proxy/mitm-proxy-config.ts +37 -0
  49. package/src/main/proxy/mitm-proxy-server.ts +1085 -0
  50. package/src/main/proxy/system-proxy.ts +248 -0
  51. package/src/main/session/session-manager.ts +724 -0
  52. package/src/main/tab-manager.ts +582 -0
  53. package/src/main/updater.ts +111 -0
  54. package/src/main/window.ts +235 -0
  55. package/src/preload/hook-script.ts +270 -0
  56. package/src/preload/index.ts +211 -0
  57. package/src/preload/interaction-hook.ts +286 -0
  58. package/src/preload/stealth-script.ts +302 -0
  59. package/src/preload/target-preload.ts +15 -0
  60. package/src/renderer/App.tsx +656 -0
  61. package/src/renderer/components/AiLogDetail.tsx +173 -0
  62. package/src/renderer/components/AiLogList.tsx +101 -0
  63. package/src/renderer/components/AiLogView.module.css +364 -0
  64. package/src/renderer/components/AiLogView.tsx +86 -0
  65. package/src/renderer/components/AnalyzeBar.module.css +79 -0
  66. package/src/renderer/components/AnalyzeBar.tsx +104 -0
  67. package/src/renderer/components/BrowserPanel.module.css +67 -0
  68. package/src/renderer/components/BrowserPanel.tsx +90 -0
  69. package/src/renderer/components/ControlBar.module.css +47 -0
  70. package/src/renderer/components/ControlBar.tsx +205 -0
  71. package/src/renderer/components/HookLog.tsx +132 -0
  72. package/src/renderer/components/InteractionLog.tsx +183 -0
  73. package/src/renderer/components/MCPServerModal.tsx +427 -0
  74. package/src/renderer/components/PromptTemplateModal.tsx +254 -0
  75. package/src/renderer/components/ReportView.module.css +413 -0
  76. package/src/renderer/components/ReportView.tsx +429 -0
  77. package/src/renderer/components/RequestDetail.module.css +191 -0
  78. package/src/renderer/components/RequestDetail.tsx +202 -0
  79. package/src/renderer/components/RequestLog.module.css +69 -0
  80. package/src/renderer/components/RequestLog.tsx +208 -0
  81. package/src/renderer/components/SessionList.module.css +245 -0
  82. package/src/renderer/components/SessionList.tsx +247 -0
  83. package/src/renderer/components/SettingsModal.tsx +100 -0
  84. package/src/renderer/components/StatusBar.module.css +44 -0
  85. package/src/renderer/components/StatusBar.tsx +102 -0
  86. package/src/renderer/components/StorageView.module.css +41 -0
  87. package/src/renderer/components/StorageView.tsx +178 -0
  88. package/src/renderer/components/TabBar.module.css +88 -0
  89. package/src/renderer/components/TabBar.tsx +70 -0
  90. package/src/renderer/components/Titlebar.module.css +254 -0
  91. package/src/renderer/components/Titlebar.tsx +169 -0
  92. package/src/renderer/components/settings/FingerprintSection.tsx +198 -0
  93. package/src/renderer/components/settings/GeneralSection.tsx +164 -0
  94. package/src/renderer/components/settings/LLMSection.tsx +148 -0
  95. package/src/renderer/components/settings/MCPServerSection.tsx +136 -0
  96. package/src/renderer/components/settings/MitmProxySection.tsx +320 -0
  97. package/src/renderer/components/settings/ProxySection.tsx +110 -0
  98. package/src/renderer/css-modules.d.ts +4 -0
  99. package/src/renderer/hooks/useCapture.ts +383 -0
  100. package/src/renderer/hooks/useConfirm.tsx +91 -0
  101. package/src/renderer/hooks/useSession.ts +136 -0
  102. package/src/renderer/hooks/useTabs.ts +103 -0
  103. package/src/renderer/i18n/en.ts +167 -0
  104. package/src/renderer/i18n/index.ts +47 -0
  105. package/src/renderer/i18n/zh.ts +170 -0
  106. package/src/renderer/index.html +12 -0
  107. package/src/renderer/main.tsx +15 -0
  108. package/src/renderer/styles/global.css +144 -0
  109. package/src/renderer/styles/themes/ayu-dark.css +59 -0
  110. package/src/renderer/styles/themes/catppuccin.css +59 -0
  111. package/src/renderer/styles/themes/discord.css +59 -0
  112. package/src/renderer/styles/themes/dracula.css +59 -0
  113. package/src/renderer/styles/themes/github-dark.css +59 -0
  114. package/src/renderer/styles/themes/gruvbox.css +59 -0
  115. package/src/renderer/styles/themes/index.css +11 -0
  116. package/src/renderer/styles/themes/light.css +59 -0
  117. package/src/renderer/styles/themes/nord.css +59 -0
  118. package/src/renderer/styles/themes/one-dark.css +59 -0
  119. package/src/renderer/styles/themes/tokyo-night.css +59 -0
  120. package/src/renderer/styles/tokens.css +137 -0
  121. package/src/renderer/theme.ts +31 -0
  122. package/src/renderer/ui/Badge.module.css +38 -0
  123. package/src/renderer/ui/Badge.tsx +36 -0
  124. package/src/renderer/ui/Button.module.css +142 -0
  125. package/src/renderer/ui/Button.tsx +46 -0
  126. package/src/renderer/ui/Collapse.module.css +49 -0
  127. package/src/renderer/ui/Collapse.tsx +57 -0
  128. package/src/renderer/ui/CopyableBlock.module.css +56 -0
  129. package/src/renderer/ui/CopyableBlock.tsx +42 -0
  130. package/src/renderer/ui/Empty.module.css +19 -0
  131. package/src/renderer/ui/Empty.tsx +34 -0
  132. package/src/renderer/ui/Icons.tsx +346 -0
  133. package/src/renderer/ui/Input.module.css +103 -0
  134. package/src/renderer/ui/Input.tsx +94 -0
  135. package/src/renderer/ui/InputNumber.module.css +68 -0
  136. package/src/renderer/ui/InputNumber.tsx +104 -0
  137. package/src/renderer/ui/Modal.module.css +83 -0
  138. package/src/renderer/ui/Modal.tsx +67 -0
  139. package/src/renderer/ui/Popconfirm.module.css +73 -0
  140. package/src/renderer/ui/Popconfirm.tsx +74 -0
  141. package/src/renderer/ui/Progress.module.css +35 -0
  142. package/src/renderer/ui/Progress.tsx +30 -0
  143. package/src/renderer/ui/Select.module.css +91 -0
  144. package/src/renderer/ui/Select.tsx +100 -0
  145. package/src/renderer/ui/Spinner.module.css +44 -0
  146. package/src/renderer/ui/Spinner.tsx +27 -0
  147. package/src/renderer/ui/Switch.module.css +39 -0
  148. package/src/renderer/ui/Switch.tsx +43 -0
  149. package/src/renderer/ui/Tabs.module.css +76 -0
  150. package/src/renderer/ui/Tabs.tsx +53 -0
  151. package/src/renderer/ui/Tag.module.css +66 -0
  152. package/src/renderer/ui/Tag.tsx +47 -0
  153. package/src/renderer/ui/Timeline.module.css +42 -0
  154. package/src/renderer/ui/Timeline.tsx +29 -0
  155. package/src/renderer/ui/Toast.module.css +99 -0
  156. package/src/renderer/ui/Toast.tsx +90 -0
  157. package/src/renderer/ui/Tooltip.module.css +26 -0
  158. package/src/renderer/ui/Tooltip.tsx +23 -0
  159. package/src/renderer/ui/VirtualTable.module.css +230 -0
  160. package/src/renderer/ui/VirtualTable.tsx +416 -0
  161. package/src/renderer/ui/index.ts +55 -0
  162. package/src/shared/types.ts +695 -0
  163. package/tests/main/ai/crypto-script-extractor.test.ts +281 -0
  164. package/tests/main/ai/llm-router.test.ts +1537 -0
  165. package/tests/main/ai/prompt-builder.test.ts +178 -0
  166. package/tests/main/ai/scene-detector.test.ts +212 -0
  167. package/tests/main/db/migrations.test.ts +134 -0
  168. package/tests/main/release-workflow.test.ts +59 -0
  169. package/tsconfig.json +7 -0
  170. package/tsconfig.node.json +23 -0
  171. package/tsconfig.web.json +24 -0
  172. package/vitest.config.ts +13 -0
@@ -0,0 +1,102 @@
1
+ import React from 'react'
2
+ import { useLocale } from '../i18n'
3
+ import type { SessionStatus } from '@shared/types'
4
+ import type { AppView } from './Titlebar'
5
+ import styles from './StatusBar.module.css'
6
+
7
+ interface StatusBarProps {
8
+ status: SessionStatus | null
9
+ requestCount: number
10
+ hookCount: number
11
+ interactionCount?: number
12
+ sessionName?: string
13
+ activeView?: AppView
14
+ llmModel?: string
15
+ tokenCount?: number
16
+ }
17
+
18
+ const StatusBar: React.FC<StatusBarProps> = ({
19
+ status,
20
+ requestCount,
21
+ hookCount,
22
+ interactionCount = 0,
23
+ sessionName,
24
+ activeView = 'browser',
25
+ llmModel,
26
+ tokenCount,
27
+ }) => {
28
+ const { t } = useLocale()
29
+ const statusLabels: Record<string, { color: string; label: string; pulse: boolean }> = {
30
+ running: { color: 'var(--color-success)', label: t('capture.running'), pulse: true },
31
+ paused: { color: 'var(--color-warning)', label: t('capture.paused'), pulse: false },
32
+ stopped: { color: 'var(--text-muted)', label: t('capture.stopped'), pulse: false },
33
+ }
34
+ const statusCfg = status ? statusLabels[status] : null
35
+
36
+ return (
37
+ <div className={styles.statusBar}>
38
+ {/* Status dot + label */}
39
+ <div className={styles.item}>
40
+ <span
41
+ className={`${styles.dot} ${statusCfg?.pulse ? styles.pulse : ''}`}
42
+ style={{ background: statusCfg?.color ?? 'var(--text-disabled)' }}
43
+ />
44
+ <span className={styles.label}>{t('status.session')}</span>
45
+ <span className={styles.value} style={{ color: statusCfg?.color }}>
46
+ {statusCfg?.label ?? 'Idle'}
47
+ </span>
48
+ </div>
49
+
50
+ {/* Request count */}
51
+ <div className={styles.item}>
52
+ <span className={styles.label}>{t('status.requests')}</span>
53
+ <span className={styles.value}>{requestCount}</span>
54
+ </div>
55
+
56
+ {/* Hooks count — browser/inspector only */}
57
+ {activeView !== 'report' && (
58
+ <div className={styles.item}>
59
+ <span className={styles.label}>{t('status.hooks')}</span>
60
+ <span className={styles.value}>{hookCount}</span>
61
+ </div>
62
+ )}
63
+
64
+ {/* Interaction recording count */}
65
+ {activeView !== 'report' && interactionCount > 0 && (
66
+ <div className={styles.item}>
67
+ <span
68
+ className={`${styles.dot} ${status === 'running' ? styles.pulse : ''}`}
69
+ style={{ background: status === 'running' ? 'var(--color-error)' : 'var(--text-muted)' }}
70
+ />
71
+ <span className={styles.label}>{t('data.interactions')}</span>
72
+ <span className={styles.value}>{interactionCount}</span>
73
+ </div>
74
+ )}
75
+
76
+ {/* Report view: LLM + Tokens */}
77
+ {activeView === 'report' && llmModel && (
78
+ <div className={styles.item}>
79
+ <span className={styles.label}>LLM</span>
80
+ <span className={styles.value}>{llmModel}</span>
81
+ </div>
82
+ )}
83
+ {activeView === 'report' && tokenCount != null && tokenCount > 0 && (
84
+ <div className={styles.item}>
85
+ <span className={styles.label}>Tokens</span>
86
+ <span className={styles.value}>{tokenCount.toLocaleString()}</span>
87
+ </div>
88
+ )}
89
+
90
+ <div className={styles.spacer} />
91
+
92
+ {/* Session name on the right */}
93
+ {sessionName && (
94
+ <div className={styles.item}>
95
+ <span className={styles.value}>{sessionName}</span>
96
+ </div>
97
+ )}
98
+ </div>
99
+ )
100
+ }
101
+
102
+ export default StatusBar
@@ -0,0 +1,41 @@
1
+ .kvTable {
2
+ width: 100%;
3
+ border-collapse: collapse;
4
+ font-size: var(--font-size-xs);
5
+ }
6
+
7
+ .kvTable th,
8
+ .kvTable td {
9
+ border: 1px solid var(--color-border);
10
+ padding: 5px 10px;
11
+ text-align: left;
12
+ }
13
+
14
+ .kvTable th {
15
+ background: var(--color-surface);
16
+ color: var(--text-secondary);
17
+ font-weight: 600;
18
+ font-size: var(--font-size-xs);
19
+ }
20
+
21
+ .kvTable td {
22
+ color: var(--text-secondary);
23
+ }
24
+
25
+ .valueCell {
26
+ font-family: var(--font-mono);
27
+ font-size: var(--font-size-xs);
28
+ max-width: 0;
29
+ overflow: hidden;
30
+ text-overflow: ellipsis;
31
+ white-space: nowrap;
32
+ }
33
+
34
+ .codeInline {
35
+ font-family: var(--font-mono);
36
+ font-size: var(--font-size-xs);
37
+ background: var(--color-surface);
38
+ padding: 1px 4px;
39
+ border-radius: 3px;
40
+ color: var(--color-warning);
41
+ }
@@ -0,0 +1,178 @@
1
+ import React, { useMemo } from 'react'
2
+ import { Collapse, Tag, Empty, Timeline } from '../ui'
3
+ import { IconShield, IconDatabase, IconApp } from '../ui/Icons'
4
+ import type { StorageSnapshot, StorageType } from '@shared/types'
5
+ import styles from './StorageView.module.css'
6
+
7
+ interface StorageViewProps {
8
+ snapshots: StorageSnapshot[]
9
+ }
10
+
11
+ const STORAGE_META: Record<StorageType, { label: string; icon: React.ReactNode; color: 'orange' | 'info' | 'success' }> = {
12
+ cookie: { label: 'Cookies', icon: <IconShield size={14} />, color: 'orange' },
13
+ localStorage: { label: 'Local Storage', icon: <IconDatabase size={14} />, color: 'info' },
14
+ sessionStorage: { label: 'Session Storage', icon: <IconApp size={14} />, color: 'success' },
15
+ }
16
+
17
+ function parseStorageData(data: string): Array<{ key: string; value: string }> {
18
+ try {
19
+ const parsed = JSON.parse(data)
20
+ if (typeof parsed === 'object' && parsed !== null) {
21
+ return Object.entries(parsed).map(([key, value]) => ({
22
+ key,
23
+ value: typeof value === 'string' ? value : JSON.stringify(value),
24
+ }))
25
+ }
26
+ } catch { /* ignore */ }
27
+ return []
28
+ }
29
+
30
+ function computeDiff(
31
+ oldData: string,
32
+ newData: string
33
+ ): Array<{ key: string; action: 'added' | 'removed' | 'changed'; oldValue?: string; newValue?: string }> {
34
+ const oldEntries = parseStorageData(oldData)
35
+ const newEntries = parseStorageData(newData)
36
+ const oldMap = new Map(oldEntries.map(e => [e.key, e.value]))
37
+ const newMap = new Map(newEntries.map(e => [e.key, e.value]))
38
+ const diffs: Array<{ key: string; action: 'added' | 'removed' | 'changed'; oldValue?: string; newValue?: string }> = []
39
+
40
+ for (const [key, value] of newMap) {
41
+ if (!oldMap.has(key)) diffs.push({ key, action: 'added', newValue: value })
42
+ else if (oldMap.get(key) !== value) diffs.push({ key, action: 'changed', oldValue: oldMap.get(key), newValue: value })
43
+ }
44
+ for (const [key] of oldMap) {
45
+ if (!newMap.has(key)) diffs.push({ key, action: 'removed', oldValue: oldMap.get(key) })
46
+ }
47
+ return diffs
48
+ }
49
+
50
+ const DIFF_COLORS: Record<string, 'success' | 'error' | 'info'> = { added: 'success', removed: 'error', changed: 'info' }
51
+
52
+ // KV Table using plain HTML table
53
+ const KVTable: React.FC<{ entries: Array<{ key: string; value: string }> }> = ({ entries }) => (
54
+ <table className={styles.kvTable}>
55
+ <thead>
56
+ <tr>
57
+ <th style={{ width: '35%' }}>Key</th>
58
+ <th>Value</th>
59
+ </tr>
60
+ </thead>
61
+ <tbody>
62
+ {entries.map(e => (
63
+ <tr key={e.key}>
64
+ <td><code className={styles.codeInline}>{e.key}</code></td>
65
+ <td className={styles.valueCell} title={e.value}>{e.value}</td>
66
+ </tr>
67
+ ))}
68
+ </tbody>
69
+ </table>
70
+ )
71
+
72
+ // Diff timeline
73
+ const DiffTimeline: React.FC<{ snapshots: StorageSnapshot[] }> = ({ snapshots }) => {
74
+ if (snapshots.length < 2) return null
75
+
76
+ const sorted = [...snapshots].sort((a, b) => a.timestamp - b.timestamp)
77
+ const items: Array<{ key: string; color?: string; children: React.ReactNode }> = []
78
+
79
+ for (let i = 1; i < sorted.length; i++) {
80
+ const diffs = computeDiff(sorted[i - 1].data, sorted[i].data)
81
+ if (diffs.length === 0) continue
82
+
83
+ items.push({
84
+ key: `diff-${i}`,
85
+ color: 'var(--color-info)',
86
+ children: (
87
+ <div>
88
+ <span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--text-muted)' }}>
89
+ {new Date(sorted[i].timestamp).toLocaleString()}
90
+ </span>
91
+ <div style={{ marginTop: 4 }}>
92
+ {diffs.map(d => (
93
+ <div key={d.key} style={{ marginBottom: 2, display: 'flex', alignItems: 'center', gap: 4 }}>
94
+ <Tag color={DIFF_COLORS[d.action]}>{d.action}</Tag>
95
+ <code className={styles.codeInline}>{d.key}</code>
96
+ {d.newValue && (
97
+ <span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--text-muted)', marginLeft: 4 }}>
98
+ = {d.newValue.length > 60 ? d.newValue.slice(0, 60) + '...' : d.newValue}
99
+ </span>
100
+ )}
101
+ </div>
102
+ ))}
103
+ </div>
104
+ </div>
105
+ ),
106
+ })
107
+ }
108
+
109
+ if (items.length === 0) return null
110
+
111
+ return (
112
+ <div style={{ marginTop: 12 }}>
113
+ <div style={{ fontWeight: 600, fontSize: 'var(--font-size-base)', color: 'var(--text-primary)', marginBottom: 8 }}>Changes Timeline</div>
114
+ <Timeline items={items} />
115
+ </div>
116
+ )
117
+ }
118
+
119
+ // Single storage type section
120
+ const StorageSection: React.FC<{ type: StorageType; snapshots: StorageSnapshot[] }> = ({ type, snapshots }) => {
121
+ const meta = STORAGE_META[type]
122
+ const sorted = [...snapshots].sort((a, b) => b.timestamp - a.timestamp)
123
+ const latestEntries = sorted.length > 0 ? parseStorageData(sorted[0].data) : []
124
+
125
+ return (
126
+ <div>
127
+ {latestEntries.length > 0 ? (
128
+ <>
129
+ <div style={{ fontSize: 'var(--font-size-xs)', color: 'var(--text-muted)', marginBottom: 8 }}>
130
+ Latest snapshot: {new Date(sorted[0].timestamp).toLocaleString()} | {latestEntries.length} entries | Domain: {sorted[0].domain}
131
+ </div>
132
+ <KVTable entries={latestEntries} />
133
+ {sorted.length > 1 && <DiffTimeline snapshots={sorted} />}
134
+ </>
135
+ ) : (
136
+ <Empty description={`No ${meta.label.toLowerCase()} data`} />
137
+ )}
138
+ </div>
139
+ )
140
+ }
141
+
142
+ const StorageView: React.FC<StorageViewProps> = ({ snapshots }) => {
143
+ const grouped = useMemo(() => {
144
+ const groups: Record<StorageType, StorageSnapshot[]> = { cookie: [], localStorage: [], sessionStorage: [] }
145
+ for (const snap of snapshots) {
146
+ if (groups[snap.storage_type]) groups[snap.storage_type].push(snap)
147
+ }
148
+ return groups
149
+ }, [snapshots])
150
+
151
+ if (snapshots.length === 0) {
152
+ return (
153
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', minHeight: 200 }}>
154
+ <Empty description="No storage snapshots captured yet" />
155
+ </div>
156
+ )
157
+ }
158
+
159
+ const collapseItems = (['cookie', 'localStorage', 'sessionStorage'] as StorageType[]).map(type => {
160
+ const meta = STORAGE_META[type]
161
+ const count = grouped[type].length
162
+ return {
163
+ key: type,
164
+ label: (
165
+ <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
166
+ {meta.icon}
167
+ <span>{meta.label}</span>
168
+ {count > 0 && <Tag color={meta.color}>{count} snapshot{count > 1 ? 's' : ''}</Tag>}
169
+ </span>
170
+ ),
171
+ children: <StorageSection type={type} snapshots={grouped[type]} />,
172
+ }
173
+ })
174
+
175
+ return <Collapse items={collapseItems} defaultActiveKey={['cookie', 'localStorage', 'sessionStorage']} />
176
+ }
177
+
178
+ export default StorageView
@@ -0,0 +1,88 @@
1
+ .tabBar {
2
+ height: 32px;
3
+ display: flex;
4
+ align-items: stretch;
5
+ background: var(--color-sidebar);
6
+ border-bottom: 1px solid var(--color-border);
7
+ overflow: hidden;
8
+ flex-shrink: 0;
9
+ }
10
+
11
+ .tabList {
12
+ display: flex;
13
+ align-items: stretch;
14
+ overflow: hidden;
15
+ flex: 1;
16
+ min-width: 0;
17
+ }
18
+
19
+ .tab {
20
+ display: flex;
21
+ align-items: center;
22
+ gap: 6px;
23
+ padding: 0 10px;
24
+ min-width: 0;
25
+ max-width: 180px;
26
+ cursor: pointer;
27
+ border-bottom: 2px solid transparent;
28
+ border-right: 1px solid var(--color-border);
29
+ transition: background var(--transition-fast);
30
+ background: transparent;
31
+ }
32
+
33
+ .tab:hover {
34
+ background: var(--color-surface);
35
+ }
36
+
37
+ .tabActive {
38
+ background: var(--color-surface-hover);
39
+ border-bottom-color: var(--color-info);
40
+ }
41
+
42
+ .tabLabel {
43
+ flex: 1;
44
+ overflow: hidden;
45
+ text-overflow: ellipsis;
46
+ white-space: nowrap;
47
+ font-size: var(--font-size-xs);
48
+ color: var(--text-muted);
49
+ user-select: none;
50
+ }
51
+
52
+ .tabActive .tabLabel {
53
+ color: var(--text-secondary);
54
+ }
55
+
56
+ .closeBtn {
57
+ display: flex;
58
+ align-items: center;
59
+ color: var(--text-disabled);
60
+ flex-shrink: 0;
61
+ opacity: 0;
62
+ transition: opacity var(--transition-fast);
63
+ cursor: pointer;
64
+ }
65
+
66
+ .tab:hover .closeBtn,
67
+ .tabActive .closeBtn {
68
+ opacity: 1;
69
+ }
70
+
71
+ .closeBtn:hover {
72
+ color: var(--text-secondary);
73
+ }
74
+
75
+ .newTabBtn {
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ width: 32px;
80
+ flex-shrink: 0;
81
+ cursor: pointer;
82
+ color: var(--text-disabled);
83
+ transition: color var(--transition-fast);
84
+ }
85
+
86
+ .newTabBtn:hover {
87
+ color: var(--text-secondary);
88
+ }
@@ -0,0 +1,70 @@
1
+ import React, { useState } from 'react'
2
+ import { IconPlus, IconClose, IconLoading } from '../ui/Icons'
3
+ import type { BrowserTab } from '@shared/types'
4
+ import styles from './TabBar.module.css'
5
+
6
+ interface TabBarProps {
7
+ tabs: BrowserTab[]
8
+ activeTabId: string | null
9
+ onActivate: (tabId: string) => void
10
+ onClose: (tabId: string) => void
11
+ onCreate: () => void
12
+ }
13
+
14
+ /** Extracts a display label for a tab: title > hostname > 'New Tab' */
15
+ function getTabLabel(tab: BrowserTab): string {
16
+ if (tab.title && tab.title !== 'New Tab') return tab.title
17
+ if (tab.url) {
18
+ try { return new URL(tab.url).hostname || 'New Tab' } catch { /* invalid URL */ }
19
+ }
20
+ return 'New Tab'
21
+ }
22
+
23
+ const TabBar: React.FC<TabBarProps> = ({ tabs, activeTabId, onActivate, onClose, onCreate }) => {
24
+ const [hoveredTabId, setHoveredTabId] = useState<string | null>(null)
25
+
26
+ return (
27
+ <div className={styles.tabBar}>
28
+ <div className={styles.tabList}>
29
+ {tabs.map((tab) => {
30
+ const isActive = tab.id === activeTabId
31
+ const isHovered = tab.id === hoveredTabId
32
+ const label = getTabLabel(tab)
33
+
34
+ return (
35
+ <div
36
+ key={tab.id}
37
+ className={`${styles.tab} ${isActive ? styles.tabActive : ''}`}
38
+ onClick={() => onActivate(tab.id)}
39
+ onMouseEnter={() => setHoveredTabId(tab.id)}
40
+ onMouseLeave={() => setHoveredTabId(null)}
41
+ >
42
+ <span className={styles.tabLabel} title={tab.url || label}>
43
+ {tab.isLoading && <IconLoading size={10} />}
44
+ {label}
45
+ </span>
46
+ {(isHovered || isActive) && (
47
+ <span
48
+ className={styles.closeBtn}
49
+ onClick={(e) => {
50
+ e.stopPropagation()
51
+ onClose(tab.id)
52
+ }}
53
+ style={{ opacity: 1 }}
54
+ >
55
+ <IconClose size={10} />
56
+ </span>
57
+ )}
58
+ </div>
59
+ )
60
+ })}
61
+ </div>
62
+
63
+ <div className={styles.newTabBtn} onClick={onCreate} title="New Tab">
64
+ <IconPlus size={12} />
65
+ </div>
66
+ </div>
67
+ )
68
+ }
69
+
70
+ export default TabBar