@lievclouds/dsh-cortex 0.5.4 → 1.0.0

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.

Potentially problematic release.


This version of @lievclouds/dsh-cortex might be problematic. Click here for more details.

Files changed (44) hide show
  1. package/README.md +1 -1
  2. package/client.js +434 -412
  3. package/index.js +7 -0
  4. package/locale/en.json +6 -6
  5. package/locale/zh.json +6 -6
  6. package/package.json +115 -110
  7. package/src/cortex-ui/dsh/css-overrides.css +8 -0
  8. package/src/cortex-ui/renderer/App.tsx +39 -0
  9. package/src/cortex-ui/renderer/components/miniapp/FileTree.tsx +161 -0
  10. package/src/cortex-ui/renderer/components/miniapp/MiniappCard.tsx +263 -0
  11. package/src/cortex-ui/renderer/components/miniapp/MiniappDetail.tsx +1493 -0
  12. package/src/cortex-ui/renderer/components/retest/CurlImportDialog.tsx +158 -0
  13. package/src/cortex-ui/renderer/components/retest/RepeaterCollectionsDialog.tsx +327 -0
  14. package/src/cortex-ui/renderer/components/retest/RepeaterHistoryDialog.tsx +242 -0
  15. package/src/cortex-ui/renderer/components/retest/RepeaterPanel.tsx +291 -137
  16. package/src/cortex-ui/renderer/components/retest/YakitSendPanel.tsx +411 -0
  17. package/src/cortex-ui/renderer/i18n/messages/common.ts +2 -0
  18. package/src/cortex-ui/renderer/i18n/messages/fuzzer.ts +32 -0
  19. package/src/cortex-ui/renderer/i18n/messages/index.ts +5 -1
  20. package/src/cortex-ui/renderer/i18n/messages/miniapp.ts +392 -0
  21. package/src/cortex-ui/renderer/i18n/messages/retest.ts +144 -2
  22. package/src/cortex-ui/renderer/lib/curl.ts +635 -0
  23. package/src/cortex-ui/renderer/lib/miniappApi.ts +459 -0
  24. package/src/cortex-ui/renderer/lib/navigation.ts +13 -1
  25. package/src/cortex-ui/renderer/lib/repeaterRaw.ts +102 -0
  26. package/src/cortex-ui/renderer/lib/repeaterStoreApi.ts +163 -0
  27. package/src/cortex-ui/renderer/lib/retestResponse.ts +64 -17
  28. package/src/cortex-ui/renderer/pages/FuzzerPage.tsx +212 -0
  29. package/src/cortex-ui/renderer/pages/MiniappPage.tsx +664 -0
  30. package/src/host/cortex/index.js +42 -1
  31. package/src/host/cortex/repeater-store.js +411 -0
  32. package/src/host/cortex/repeater.js +15 -2
  33. package/src/host/cortex/tools.js +2 -0
  34. package/src/host/miniapp/analyze.js +1078 -0
  35. package/src/host/miniapp/assets-import.js +294 -0
  36. package/src/host/miniapp/detect.js +426 -0
  37. package/src/host/miniapp/format.js +263 -0
  38. package/src/host/miniapp/roots.js +360 -0
  39. package/src/host/miniapp/routes.js +304 -0
  40. package/src/host/miniapp/service.js +559 -0
  41. package/src/host/miniapp/store.js +756 -0
  42. package/src/host/miniapp/tools.js +335 -0
  43. package/src/host/miniapp/unpack.js +1010 -0
  44. package/src/host/miniapp/wxapkg.js +416 -0
package/index.js CHANGED
@@ -15,6 +15,12 @@ export const name = 'dsh-cortex'
15
15
  export const inject = ['webServer', 'tools']
16
16
 
17
17
  export function apply(ctx, config) {
18
+ // The mini-program scanner also looks for WeChat's `Documents\xwechat_files`
19
+ // data directory on other drives. This is opt-in so tests and embedders with
20
+ // a synthetic env stay hermetic.
21
+ if (process.env.CORTEX_MINIAPP_EXTRA_DRIVES === undefined) {
22
+ process.env.CORTEX_MINIAPP_EXTRA_DRIVES = '1'
23
+ }
18
24
  let host
19
25
  try {
20
26
  host = createCortexHost({ config })
@@ -52,6 +58,7 @@ export function apply(ctx, config) {
52
58
  '- Project names: reuse the existing project string exactly (list the assets or the audit first). Variants such as `上海春芝堂 SRC` and `上海春芝堂_SRC` are merged defensively, but do not invent a new spelling.',
53
59
  '- Data completeness is mandatory: before finishing a phase call `cortex_audit_completeness`, then fill every gap — every asset must carry interfaces (path/method/params/request), leaked_infos (info_type/value/source/potential_impact) and test_parameters (name/endpoint/location/values/source); every confirmed finding must be saved with Markdown reproduction_steps; internal hosts/services/footholds/credentials/pivots go through `cortex_sync_internal_scope`.',
54
60
  '- Summaries / audit: `cortex_internal_overview`, `cortex_graph_summary`, `cortex_audit_completeness`.',
61
+ '- Mini-program reversing (小程序逆向 / wxapkg): when the user asks about a WeChat mini program, scan first with `cortex_scan_miniapps`, then `cortex_list_miniapps` → `cortex_unpack_miniapp` → `cortex_get_miniapp_overview` → `cortex_search_miniapp` / `cortex_read_miniapp_file`; persist extracted domains/interfaces/secrets/test parameters with `cortex_import_miniapp_assets`. Only authorized packages; package content stays local; never execute unpacked JS/HTML. Report the real unpack status (unpacked / unsupported / failed) instead of assuming success. The tools expose the unpacked workspace: `cortex_search_miniapp` covers every text file and `cortex_read_miniapp_file` pages through any text file (max 200 lines / 100 KiB per call — pass `offset` to continue), while binaries are never returned. If the user names a mini program directly, resolve it via `cortex_list_miniapps` (match app_name or appid), unpack when needed, then read/search — no need to ask for internal ids.',
55
62
  'Do not skip these calls merely because no skill was loaded. If a tool call fails, report the failure; never claim a record was saved without a successful result.',
56
63
  'Examples: “列出 Cortex 里的高危漏洞”, “把刚才确认的漏洞写入 Cortex”, “把 127.0.0.1:8080 的接口同步到资产库”, “把 VULN-… 标记为已确认并追加复测结论”.',
57
64
  ].join('\n'),
package/locale/en.json CHANGED
@@ -1,6 +1,6 @@
1
- {
2
- "meta": {
3
- "title": "Cortex",
4
- "description": "The original Cortex workbench inside DeepSeek Harness: vulnerability board, internal operations, memory nebula graph, assets, snapshots and retest, plus 12 native cortex_* agent tools. No MCP needed."
5
- }
6
- }
1
+ {
2
+ "meta": {
3
+ "title": "Cortex",
4
+ "description": "The original Cortex workbench in DeepSeek Harness: vulnerability board, internal operations, Memory Nebula, assets, snapshots, retest, Web Fuzzer and mini-program reversing (wxapkg detect/decrypt/unpack/analyze), with 22 native cortex_* agent tools and no MCP."
5
+ }
6
+ }
package/locale/zh.json CHANGED
@@ -1,6 +1,6 @@
1
- {
2
- "meta": {
3
- "title": "Cortex",
4
- "description": "在 DeepSeek Harness 内使用原版 Cortex 工作台:漏洞看板、内网作战、记忆星云图谱、资产库、认知快照与漏洞复测;内置 12 个 cortex_* Agent 工具,无需 MCP。"
5
- }
6
- }
1
+ {
2
+ "meta": {
3
+ "title": "Cortex",
4
+ "description": "在 DeepSeek Harness 内使用原版 Cortex 工作台:漏洞看板、内网作战、记忆星云图谱、资产库、认知快照、漏洞复测、Web Fuzzer 与小程序逆向(wxapkg 解密/解包/分析),内置 22 个 cortex_* Agent 工具,无需 MCP。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,110 +1,115 @@
1
- {
2
- "name": "@lievclouds/dsh-cortex",
3
- "version": "0.5.4",
4
- "description": "Cortex workbench for DeepSeek Harness: the original Cortex UI (vulnerabilities, assets, internal operations, Memory Nebula, snapshots, retest) as a native DSH panel, plus 12 native agent tools so DSH conversations use the local Cortex SQLite without MCP.",
5
- "type": "module",
6
- "main": "index.js",
7
- "license": "MIT",
8
- "author": {
9
- "name": "Revcloudi",
10
- "url": "https://github.com/Revcloudi"
11
- },
12
- "homepage": "https://github.com/Revcloudi/Cortex/tree/main/dsh-cortex#readme",
13
- "repository": {
14
- "type": "git",
15
- "url": "git+https://github.com/Revcloudi/Cortex.git",
16
- "directory": "dsh-cortex"
17
- },
18
- "bugs": {
19
- "url": "https://github.com/Revcloudi/Cortex/issues"
20
- },
21
- "publishConfig": {
22
- "access": "public"
23
- },
24
- "keywords": [
25
- "dsh-plugin",
26
- "deepseek-harness",
27
- "dsh",
28
- "cordis",
29
- "cortex",
30
- "pentest",
31
- "vulnerability-management",
32
- "asset-management",
33
- "sqlite",
34
- "nebula"
35
- ],
36
- "exports": {
37
- ".": "./index.js",
38
- "./client": "./client.js",
39
- "./cordis.patch.yml": "./cordis.patch.yml",
40
- "./locale/*.json": "./locale/*.json",
41
- "./icon": "./icon.svg",
42
- "./package.json": "./package.json"
43
- },
44
- "files": [
45
- "index.js",
46
- "client.js",
47
- "src",
48
- "cordis.patch.yml",
49
- "locale",
50
- "icon.svg",
51
- "README.md",
52
- "LICENSE"
53
- ],
54
- "engines": {
55
- "node": ">=22.19.0"
56
- },
57
- "scripts": {
58
- "build": "node scripts/build-host-graph.mjs && node scripts/build-cortex-ui.mjs",
59
- "build:graph": "node scripts/build-host-graph.mjs",
60
- "build:client": "node scripts/build-cortex-ui.mjs",
61
- "smoke": "node scripts/smoke-cortex-ui.mjs",
62
- "test": "node --test",
63
- "test:host": "node --test \"test/host/**/*.test.mjs\"",
64
- "pack:check": "npm pack --dry-run",
65
- "publish:npm": "npm publish --access public"
66
- },
67
- "dsh": {
68
- "bundle": {
69
- "patch": "./cordis.patch.yml"
70
- },
71
- "client": {
72
- "platform": "web",
73
- "immediately": true,
74
- "inject": [
75
- "@deepseek-ai/dsh-client-ui-sidebar",
76
- "@deepseek-ai/dsh-client-locale"
77
- ]
78
- }
79
- },
80
- "devDependencies": {
81
- "@fontsource-variable/inter": "5.3.0",
82
- "@fontsource-variable/jetbrains-mono": "5.3.0",
83
- "@radix-ui/react-dialog": "1.3.1",
84
- "@radix-ui/react-select": "^2.4.1",
85
- "@radix-ui/react-tabs": "1.2.1",
86
- "@radix-ui/react-tooltip": "1.4.1",
87
- "autoprefixer": "10.6.2",
88
- "class-variance-authority": "0.7.1",
89
- "clsx": "2.1.1",
90
- "esbuild": "0.28.2",
91
- "jsdom": "^31.0.0",
92
- "lucide-react": "1.65.0",
93
- "postcss": "8.5.32",
94
- "react": "18.3.1",
95
- "react-dom": "18.3.1",
96
- "react-force-graph-3d": "^1.29.2",
97
- "react-markdown": "^10.1.0",
98
- "react-syntax-highlighter": "^16.1.2",
99
- "remark-gfm": "^4.0.1",
100
- "sonner": "2.0.8",
101
- "tailwind-merge": "3.7.0",
102
- "tailwindcss": "3.4.19",
103
- "three": "^0.187.0",
104
- "zod": "4.6.7"
105
- },
106
- "dependencies": {
107
- "docx": "^9.15.3",
108
- "pdfkit": "^0.20.2"
109
- }
110
- }
1
+ {
2
+ "name": "@lievclouds/dsh-cortex",
3
+ "version": "1.0.0",
4
+ "description": "Cortex workbench for DeepSeek Harness: the original Cortex UI (vulnerabilities, assets, internal operations, Memory Nebula, snapshots, retest, Web Fuzzer, mini-program reversing) as a native DSH panel, plus 22 native agent tools so DSH conversations use the local Cortex SQLite without MCP.",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "license": "MIT",
8
+ "author": {
9
+ "name": "Revcloudi",
10
+ "url": "https://github.com/Revcloudi"
11
+ },
12
+ "homepage": "https://github.com/Revcloudi/Cortex/tree/main/dsh-cortex#readme",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/Revcloudi/Cortex.git",
16
+ "directory": "dsh-cortex"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/Revcloudi/Cortex/issues"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "keywords": [
25
+ "asset-management",
26
+ "cordis",
27
+ "cortex",
28
+ "deepseek-harness",
29
+ "dsh",
30
+ "dsh-plugin",
31
+ "fuzzer",
32
+ "miniapp",
33
+ "nebula",
34
+ "pentest",
35
+ "reverse-engineering",
36
+ "sqlite",
37
+ "vulnerability-management",
38
+ "wechat-mini-program",
39
+ "wxapkg"
40
+ ],
41
+ "exports": {
42
+ ".": "./index.js",
43
+ "./client": "./client.js",
44
+ "./cordis.patch.yml": "./cordis.patch.yml",
45
+ "./locale/*.json": "./locale/*.json",
46
+ "./icon": "./icon.svg",
47
+ "./package.json": "./package.json"
48
+ },
49
+ "files": [
50
+ "index.js",
51
+ "client.js",
52
+ "src",
53
+ "cordis.patch.yml",
54
+ "locale",
55
+ "icon.svg",
56
+ "README.md",
57
+ "LICENSE"
58
+ ],
59
+ "engines": {
60
+ "node": ">=22.19.0"
61
+ },
62
+ "scripts": {
63
+ "build": "node scripts/build-host-graph.mjs && node scripts/build-cortex-ui.mjs",
64
+ "build:graph": "node scripts/build-host-graph.mjs",
65
+ "build:client": "node scripts/build-cortex-ui.mjs",
66
+ "smoke": "node scripts/smoke-cortex-ui.mjs",
67
+ "test": "node --test",
68
+ "test:host": "node --test \"test/host/**/*.test.mjs\"",
69
+ "pack:check": "npm pack --dry-run",
70
+ "publish:npm": "npm publish --access public"
71
+ },
72
+ "dsh": {
73
+ "bundle": {
74
+ "patch": "./cordis.patch.yml"
75
+ },
76
+ "client": {
77
+ "platform": "web",
78
+ "immediately": true,
79
+ "inject": [
80
+ "@deepseek-ai/dsh-client-ui-sidebar",
81
+ "@deepseek-ai/dsh-client-locale"
82
+ ]
83
+ }
84
+ },
85
+ "devDependencies": {
86
+ "@fontsource-variable/inter": "5.3.0",
87
+ "@fontsource-variable/jetbrains-mono": "5.3.0",
88
+ "@radix-ui/react-dialog": "1.3.1",
89
+ "@radix-ui/react-select": "^2.4.1",
90
+ "@radix-ui/react-tabs": "1.2.1",
91
+ "@radix-ui/react-tooltip": "1.4.1",
92
+ "autoprefixer": "10.6.2",
93
+ "class-variance-authority": "0.7.1",
94
+ "clsx": "2.1.1",
95
+ "esbuild": "0.28.2",
96
+ "jsdom": "^31.0.0",
97
+ "lucide-react": "1.65.0",
98
+ "postcss": "8.5.32",
99
+ "react": "18.3.1",
100
+ "react-dom": "18.3.1",
101
+ "react-force-graph-3d": "^1.29.2",
102
+ "react-markdown": "^10.1.0",
103
+ "react-syntax-highlighter": "^16.1.2",
104
+ "remark-gfm": "^4.0.1",
105
+ "sonner": "2.0.8",
106
+ "tailwind-merge": "3.7.0",
107
+ "tailwindcss": "3.4.19",
108
+ "three": "^0.187.0",
109
+ "zod": "4.6.7"
110
+ },
111
+ "dependencies": {
112
+ "docx": "^9.15.3",
113
+ "pdfkit": "^0.20.2"
114
+ }
115
+ }
@@ -138,3 +138,11 @@
138
138
  */
139
139
 
140
140
 
141
+
142
+ /* Mini-program search / file-path fields keep a static look while focused:
143
+ * no outline, ring, shadow or border-colour change. */
144
+ .dshc-cortex-root .dshc-flat-input:focus,
145
+ .dshc-cortex-root .dshc-flat-input:focus-visible {
146
+ outline: none !important;
147
+ box-shadow: none !important;
148
+ }
@@ -30,6 +30,24 @@ const RetestPage = lazy(async () => {
30
30
  return { default: module.RetestPage }
31
31
  })
32
32
 
33
+ /**
34
+ * The dedicated Web Fuzzer page stays out of the first-screen bundle for the
35
+ * same reason as the retest lab: it is a desktop-only workbench.
36
+ */
37
+ const FuzzerPage = lazy(async () => {
38
+ const module = await import('@renderer/pages/FuzzerPage')
39
+ return { default: module.FuzzerPage }
40
+ })
41
+
42
+ /**
43
+ * Mini-program reverse engineering is desktop-only too (it reads local WeChat
44
+ * caches through the Host), so it stays out of the first-screen bundle.
45
+ */
46
+ const MiniappPage = lazy(async () => {
47
+ const module = await import('@renderer/pages/MiniappPage')
48
+ return { default: module.MiniappPage }
49
+ })
50
+
33
51
  export default function App() {
34
52
  const [page, setPage] = useState<AppPage>('dashboard')
35
53
  const [dataVersion, setDataVersion] = useState(0)
@@ -96,6 +114,14 @@ export default function App() {
96
114
  <Suspense fallback={<RetestPageFallback />}>
97
115
  <RetestPage dataVersion={dataVersion} />
98
116
  </Suspense>
117
+ ) : page === 'fuzzer' ? (
118
+ <Suspense fallback={<RetestPageFallback />}>
119
+ <FuzzerPage dataVersion={dataVersion} />
120
+ </Suspense>
121
+ ) : page === 'miniapps' ? (
122
+ <Suspense fallback={<MiniappPageFallback />}>
123
+ <MiniappPage dataVersion={dataVersion} />
124
+ </Suspense>
99
125
  ) : page === 'ai-config' && !IS_WEB_RUNTIME ? (
100
126
  <AiConfigPage />
101
127
  ) : null}
@@ -159,3 +185,16 @@ function RetestPageFallback() {
159
185
  </div>
160
186
  )
161
187
  }
188
+
189
+ /** Light-weight fallback while the desktop-only mini-program chunk loads. */
190
+ function MiniappPageFallback() {
191
+ const { t } = useI18n()
192
+ return (
193
+ <div className="flex h-full w-full items-center justify-center">
194
+ <div className="flex items-center gap-2.5 text-[12px] text-muted-foreground">
195
+ <Loader2 aria-hidden="true" className="h-4 w-4 animate-spin" />
196
+ {t('common.loading')}
197
+ </div>
198
+ </div>
199
+ )
200
+ }
@@ -0,0 +1,161 @@
1
+ import { useCallback, useEffect, useState, type ReactNode } from 'react'
2
+ import { ChevronDown, ChevronRight, File, Folder, Loader2 } from 'lucide-react'
3
+ import {
4
+ getMiniappTree,
5
+ toMiniappErrorMessage,
6
+ type MiniappTreeEntry
7
+ } from '@renderer/lib/miniappApi'
8
+ import { formatBytes } from '@renderer/lib/format'
9
+ import { useI18n } from '@renderer/i18n'
10
+ import { cn } from '@renderer/lib/utils'
11
+
12
+ interface FileTreeProps {
13
+ miniappId: string
14
+ /** Called with the relative path when a file leaf is clicked. */
15
+ onOpenFile: (path: string) => void
16
+ }
17
+
18
+ interface DirState {
19
+ entries: MiniappTreeEntry[] | null
20
+ loading: boolean
21
+ error: string | null
22
+ }
23
+
24
+ /**
25
+ * Lazy directory tree of an unpacked mini program.
26
+ *
27
+ * Only the root directory is fetched on mount; every expanded directory is
28
+ * fetched exactly once and then cached for the lifetime of the mount. The
29
+ * detail sheet keys the component by source id so switching sources resets the
30
+ * cache instead of showing another app's tree.
31
+ */
32
+ export function FileTree({ miniappId, onOpenFile }: FileTreeProps) {
33
+ const { t } = useI18n()
34
+ const [dirs, setDirs] = useState<Record<string, DirState>>({})
35
+ const [expanded, setExpanded] = useState<Record<string, boolean>>({})
36
+
37
+ const loadDir = useCallback(
38
+ async (path: string) => {
39
+ setDirs((prev) => ({
40
+ ...prev,
41
+ [path]: { entries: prev[path]?.entries ?? null, loading: true, error: null }
42
+ }))
43
+ try {
44
+ const result = await getMiniappTree(miniappId, path)
45
+ setDirs((prev) => ({
46
+ ...prev,
47
+ [path]: { entries: result.entries ?? [], loading: false, error: null }
48
+ }))
49
+ } catch (cause) {
50
+ setDirs((prev) => ({
51
+ ...prev,
52
+ [path]: { entries: null, loading: false, error: toMiniappErrorMessage(cause) }
53
+ }))
54
+ }
55
+ },
56
+ [miniappId]
57
+ )
58
+
59
+ useEffect(() => {
60
+ setDirs({})
61
+ setExpanded({})
62
+ void loadDir('')
63
+ }, [loadDir])
64
+
65
+ const toggleDir = (path: string) => {
66
+ if (expanded[path] === true) {
67
+ setExpanded((prev) => ({ ...prev, [path]: false }))
68
+ return
69
+ }
70
+ setExpanded((prev) => ({ ...prev, [path]: true }))
71
+ const state = dirs[path]
72
+ if (state === undefined || (state.entries === null && !state.loading)) void loadDir(path)
73
+ }
74
+
75
+ const renderDir = (path: string, depth: number): ReactNode => {
76
+ const state = dirs[path]
77
+ if (state === undefined) return null
78
+ const indent = { paddingLeft: `${8 + depth * 14}px` }
79
+ if (state.loading && state.entries === null) {
80
+ return (
81
+ <li className="flex items-center gap-1.5 py-1 text-[11px] text-muted-foreground" style={indent}>
82
+ <Loader2 aria-hidden="true" className="h-3 w-3 animate-spin" />
83
+ {t('miniapp.file.loading')}
84
+ </li>
85
+ )
86
+ }
87
+ if (state.error !== null) {
88
+ return (
89
+ <li className="py-1 pr-2 text-[11px] text-red-700" style={indent}>
90
+ {t('miniapp.file.treeError', { error: state.error })}
91
+ </li>
92
+ )
93
+ }
94
+ const entries = state.entries ?? []
95
+ if (entries.length === 0) {
96
+ return (
97
+ <li className="py-1 text-[11px] text-muted-foreground" style={indent}>
98
+ {t('miniapp.file.empty')}
99
+ </li>
100
+ )
101
+ }
102
+ return entries.map((entry) =>
103
+ entry.type === 'dir' ? (
104
+ <li key={entry.path}>
105
+ <button
106
+ type="button"
107
+ aria-expanded={expanded[entry.path] === true}
108
+ onClick={() => toggleDir(entry.path)}
109
+ className="flex w-full items-center gap-1.5 py-1 pr-2 text-left text-[11.5px] text-foreground transition-colors hover:bg-sidebar focus-visible:bg-accent-soft focus-visible:outline-none"
110
+ style={indent}
111
+ >
112
+ {expanded[entry.path] === true ? (
113
+ <ChevronDown aria-hidden="true" className="h-3 w-3 shrink-0 text-muted-foreground" />
114
+ ) : (
115
+ <ChevronRight aria-hidden="true" className="h-3 w-3 shrink-0 text-muted-foreground" />
116
+ )}
117
+ <Folder aria-hidden="true" className="h-3 w-3 shrink-0 text-muted-foreground" />
118
+ <span className="truncate font-mono">{entry.name}</span>
119
+ </button>
120
+ {expanded[entry.path] === true ? (
121
+ <ul>{renderDir(entry.path, depth + 1)}</ul>
122
+ ) : null}
123
+ </li>
124
+ ) : (
125
+ <li key={entry.path}>
126
+ <button
127
+ type="button"
128
+ data-miniapp-file={entry.path}
129
+ onClick={() => onOpenFile(entry.path)}
130
+ className="flex w-full items-center gap-1.5 py-1 pr-2 text-left text-[11.5px] text-muted-foreground transition-colors hover:bg-sidebar hover:text-foreground focus-visible:bg-accent-soft focus-visible:text-foreground focus-visible:outline-none"
131
+ style={indent}
132
+ >
133
+ <File aria-hidden="true" className="ml-4 h-3 w-3 shrink-0" />
134
+ <span className="truncate font-mono">{entry.name}</span>
135
+ {entry.size > 0 ? (
136
+ <span className="ml-auto shrink-0 pl-2 font-mono text-[10px] tabular-nums text-muted-foreground/70">
137
+ {formatBytes(entry.size)}
138
+ </span>
139
+ ) : null}
140
+ </button>
141
+ </li>
142
+ )
143
+ )
144
+ }
145
+
146
+ return (
147
+ <div
148
+ data-testid="miniapp-file-tree"
149
+ className={cn(
150
+ 'flex h-full w-full min-h-0 flex-col overflow-hidden rounded-md border border-border bg-background'
151
+ )}
152
+ >
153
+ <div className="shrink-0 border-b border-border px-3 py-1.5 font-mono text-[10px] uppercase tracking-[0.14em] text-muted-foreground">
154
+ {t('miniapp.file.root')}
155
+ </div>
156
+ <ul className="min-h-0 flex-1 overflow-y-auto py-1 [scrollbar-gutter:stable]">
157
+ {renderDir('', 0)}
158
+ </ul>
159
+ </div>
160
+ )
161
+ }