dsh-coding-sidebar 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.
Files changed (277) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +524 -0
  3. package/cordis.patch.yml +49 -0
  4. package/lib/client-editor.js +48229 -0
  5. package/lib/client-mermaid.js +200745 -0
  6. package/lib/client-registry.js +21867 -0
  7. package/lib/client-terminal.js +9361 -0
  8. package/lib/client.js +21867 -0
  9. package/lib/index.js +4556 -0
  10. package/lib/invariant.js +22 -0
  11. package/lib/types/agent-opens.d.ts +65 -0
  12. package/lib/types/agent-pty.d.ts +226 -0
  13. package/lib/types/browser-probe.d.ts +15 -0
  14. package/lib/types/bundle-route.d.ts +12 -0
  15. package/lib/types/client/BrowserView.d.ts +39 -0
  16. package/lib/types/client/DiffTab.d.ts +6 -0
  17. package/lib/types/client/DiffView.d.ts +51 -0
  18. package/lib/types/client/EditorHost.d.ts +13 -0
  19. package/lib/types/client/FileTree.d.ts +38 -0
  20. package/lib/types/client/FreeWindow.d.ts +29 -0
  21. package/lib/types/client/GitView.d.ts +10 -0
  22. package/lib/types/client/MarkdownHtml.d.ts +32 -0
  23. package/lib/types/client/OrphanedTab.d.ts +11 -0
  24. package/lib/types/client/PdfView.d.ts +6 -0
  25. package/lib/types/client/RenderBoundary.d.ts +32 -0
  26. package/lib/types/client/SandboxStatusBar.d.ts +10 -0
  27. package/lib/types/client/SideCardSection.d.ts +74 -0
  28. package/lib/types/client/SideChatView.d.ts +16 -0
  29. package/lib/types/client/Sidebar.d.ts +6 -0
  30. package/lib/types/client/SubagentView.d.ts +16 -0
  31. package/lib/types/client/TabBar.d.ts +54 -0
  32. package/lib/types/client/TerminalView.d.ts +24 -0
  33. package/lib/types/client/TextEditor.d.ts +10 -0
  34. package/lib/types/client/TreePanel.d.ts +24 -0
  35. package/lib/types/client/UploadOverlay.d.ts +20 -0
  36. package/lib/types/client/add-plugin-modal.d.ts +15 -0
  37. package/lib/types/client/api.d.ts +275 -0
  38. package/lib/types/client/binary-download.d.ts +12 -0
  39. package/lib/types/client/breakpoints.d.ts +17 -0
  40. package/lib/types/client/browser.d.ts +60 -0
  41. package/lib/types/client/builtins/index.d.ts +18 -0
  42. package/lib/types/client/builtins/tabs.d.ts +11 -0
  43. package/lib/types/client/builtins/viewers.d.ts +3 -0
  44. package/lib/types/client/chunk-loader.d.ts +131 -0
  45. package/lib/types/client/chunks/editor.d.ts +9 -0
  46. package/lib/types/client/chunks/mermaid.d.ts +10 -0
  47. package/lib/types/client/chunks/terminal.d.ts +9 -0
  48. package/lib/types/client/cm-themes.d.ts +24 -0
  49. package/lib/types/client/conversation-draft.d.ts +14 -0
  50. package/lib/types/client/desktop-env.d.ts +35 -0
  51. package/lib/types/client/editor-load.d.ts +66 -0
  52. package/lib/types/client/frame-batcher.d.ts +24 -0
  53. package/lib/types/client/icons.d.ts +59 -0
  54. package/lib/types/client/image-types.d.ts +3 -0
  55. package/lib/types/client/ime-guard.d.ts +36 -0
  56. package/lib/types/client/index.d.ts +25 -0
  57. package/lib/types/client/intercept.d.ts +46 -0
  58. package/lib/types/client/lang.d.ts +14 -0
  59. package/lib/types/client/layout-push.d.ts +15 -0
  60. package/lib/types/client/lazy-chunk.d.ts +25 -0
  61. package/lib/types/client/link-intercept.d.ts +42 -0
  62. package/lib/types/client/locales-ar.d.ts +18 -0
  63. package/lib/types/client/locales-de.d.ts +4 -0
  64. package/lib/types/client/locales-fr.d.ts +10 -0
  65. package/lib/types/client/locales-hi.d.ts +17 -0
  66. package/lib/types/client/locales-id.d.ts +15 -0
  67. package/lib/types/client/locales-it.d.ts +8 -0
  68. package/lib/types/client/locales-ja.d.ts +17 -0
  69. package/lib/types/client/locales-ko.d.ts +9 -0
  70. package/lib/types/client/locales-nl.d.ts +16 -0
  71. package/lib/types/client/locales-pl.d.ts +19 -0
  72. package/lib/types/client/locales-pt.d.ts +1 -0
  73. package/lib/types/client/locales-ru.d.ts +13 -0
  74. package/lib/types/client/locales-sv.d.ts +1 -0
  75. package/lib/types/client/locales-th.d.ts +17 -0
  76. package/lib/types/client/locales-tr.d.ts +17 -0
  77. package/lib/types/client/locales-vi.d.ts +17 -0
  78. package/lib/types/client/locales-zh-HK.d.ts +32 -0
  79. package/lib/types/client/locales-zh-MO.d.ts +32 -0
  80. package/lib/types/client/locales-zh-TW.d.ts +32 -0
  81. package/lib/types/client/locales.d.ts +452 -0
  82. package/lib/types/client/markdown-html.d.ts +96 -0
  83. package/lib/types/client/markdown-images.d.ts +45 -0
  84. package/lib/types/client/markdown-labels.d.ts +38 -0
  85. package/lib/types/client/md-toc.d.ts +4 -0
  86. package/lib/types/client/mermaid-blocks.d.ts +47 -0
  87. package/lib/types/client/mermaid-sanitize.d.ts +2 -0
  88. package/lib/types/client/mermaid.d.ts +12 -0
  89. package/lib/types/client/open-when-sized.d.ts +20 -0
  90. package/lib/types/client/open-with-settings.d.ts +4 -0
  91. package/lib/types/client/open-with.d.ts +87 -0
  92. package/lib/types/client/openpath-intercept.d.ts +85 -0
  93. package/lib/types/client/paths.d.ts +43 -0
  94. package/lib/types/client/pdf-types.d.ts +2 -0
  95. package/lib/types/client/pinned.d.ts +86 -0
  96. package/lib/types/client/plugin-settings.d.ts +8 -0
  97. package/lib/types/client/plugins-shared.d.ts +27 -0
  98. package/lib/types/client/plugins-tabs.d.ts +3 -0
  99. package/lib/types/client/plugins-viewers.d.ts +3 -0
  100. package/lib/types/client/prefs.d.ts +41 -0
  101. package/lib/types/client/produced-files.d.ts +29 -0
  102. package/lib/types/client/selection-payload.d.ts +27 -0
  103. package/lib/types/client/service.d.ts +439 -0
  104. package/lib/types/client/settings-nav-icon.d.ts +19 -0
  105. package/lib/types/client/shell-presets.d.ts +48 -0
  106. package/lib/types/client/sidechat-transcript.d.ts +103 -0
  107. package/lib/types/client/split-pane.d.ts +36 -0
  108. package/lib/types/client/state.d.ts +432 -0
  109. package/lib/types/client/subagent-detect.d.ts +54 -0
  110. package/lib/types/client/subagent-jobs.d.ts +63 -0
  111. package/lib/types/client/tab-content-memo.d.ts +40 -0
  112. package/lib/types/client/terminal-font.d.ts +78 -0
  113. package/lib/types/client/terminal-links.d.ts +119 -0
  114. package/lib/types/client/theme.d.ts +45 -0
  115. package/lib/types/client/titlebar-strip.d.ts +23 -0
  116. package/lib/types/client/upload.d.ts +69 -0
  117. package/lib/types/client/wco.d.ts +47 -0
  118. package/lib/types/config.d.ts +64 -0
  119. package/lib/types/context-types.d.ts +605 -0
  120. package/lib/types/fs-operations.d.ts +28 -0
  121. package/lib/types/fs-search.d.ts +24 -0
  122. package/lib/types/fs-tree.d.ts +51 -0
  123. package/lib/types/git.d.ts +124 -0
  124. package/lib/types/html-route.d.ts +59 -0
  125. package/lib/types/index.d.ts +47 -0
  126. package/lib/types/invariant.d.ts +15 -0
  127. package/lib/types/jobs-routes.d.ts +45 -0
  128. package/lib/types/open-external.d.ts +24 -0
  129. package/lib/types/path-security.d.ts +20 -0
  130. package/lib/types/prefs-shared.d.ts +236 -0
  131. package/lib/types/pty-deps.d.ts +78 -0
  132. package/lib/types/pty-manager.d.ts +168 -0
  133. package/lib/types/sidechat-core.d.ts +172 -0
  134. package/lib/types/sidechat-routes.d.ts +30 -0
  135. package/lib/types/subagent-activity.d.ts +44 -0
  136. package/lib/types/subagent-live-route.d.ts +44 -0
  137. package/lib/types/tools.d.ts +34 -0
  138. package/lib/types/trust-fence.d.ts +24 -0
  139. package/lib/types/wire.d.ts +38 -0
  140. package/package.json +172 -0
  141. package/scripts/install.ps1 +306 -0
  142. package/scripts/install.sh +287 -0
  143. package/src/agent-opens.ts +265 -0
  144. package/src/agent-pty.ts +527 -0
  145. package/src/browser-probe.ts +26 -0
  146. package/src/bundle-route.ts +129 -0
  147. package/src/client/BrowserView.tsx +289 -0
  148. package/src/client/DiffTab.tsx +114 -0
  149. package/src/client/DiffView.tsx +305 -0
  150. package/src/client/EditorHost.tsx +558 -0
  151. package/src/client/FileTree.tsx +669 -0
  152. package/src/client/FreeWindow.tsx +337 -0
  153. package/src/client/GitView.tsx +717 -0
  154. package/src/client/MarkdownHtml.tsx +296 -0
  155. package/src/client/OrphanedTab.tsx +27 -0
  156. package/src/client/PdfView.tsx +110 -0
  157. package/src/client/RenderBoundary.tsx +49 -0
  158. package/src/client/SandboxStatusBar.tsx +60 -0
  159. package/src/client/SideCardSection.module.css +960 -0
  160. package/src/client/SideCardSection.tsx +1163 -0
  161. package/src/client/SideChatView.module.css +437 -0
  162. package/src/client/SideChatView.tsx +645 -0
  163. package/src/client/Sidebar.tsx +1370 -0
  164. package/src/client/SubagentView.module.css +547 -0
  165. package/src/client/SubagentView.tsx +883 -0
  166. package/src/client/TabBar.tsx +365 -0
  167. package/src/client/TerminalView.tsx +424 -0
  168. package/src/client/TextEditor.tsx +468 -0
  169. package/src/client/TreePanel.tsx +275 -0
  170. package/src/client/UploadOverlay.tsx +62 -0
  171. package/src/client/add-plugin-modal.tsx +199 -0
  172. package/src/client/api.ts +357 -0
  173. package/src/client/binary-download.tsx +23 -0
  174. package/src/client/breakpoints.ts +61 -0
  175. package/src/client/browser.ts +167 -0
  176. package/src/client/builtins/index.ts +37 -0
  177. package/src/client/builtins/tabs.tsx +340 -0
  178. package/src/client/builtins/viewers.tsx +125 -0
  179. package/src/client/chunk-loader.ts +358 -0
  180. package/src/client/chunks/editor.tsx +9 -0
  181. package/src/client/chunks/mermaid.tsx +10 -0
  182. package/src/client/chunks/terminal.tsx +9 -0
  183. package/src/client/cm-themes.ts +129 -0
  184. package/src/client/conversation-draft.ts +29 -0
  185. package/src/client/css-modules.d.ts +5 -0
  186. package/src/client/desktop-env.ts +72 -0
  187. package/src/client/editor-load.ts +92 -0
  188. package/src/client/frame-batcher.ts +56 -0
  189. package/src/client/icons.tsx +152 -0
  190. package/src/client/image-types.ts +8 -0
  191. package/src/client/ime-guard.ts +47 -0
  192. package/src/client/index.tsx +427 -0
  193. package/src/client/intercept.tsx +188 -0
  194. package/src/client/lang.ts +225 -0
  195. package/src/client/layout-push.ts +24 -0
  196. package/src/client/layout.css +90 -0
  197. package/src/client/lazy-chunk.tsx +89 -0
  198. package/src/client/link-intercept.ts +73 -0
  199. package/src/client/locales-ar.ts +368 -0
  200. package/src/client/locales-de.ts +353 -0
  201. package/src/client/locales-fr.ts +360 -0
  202. package/src/client/locales-hi.ts +367 -0
  203. package/src/client/locales-id.ts +365 -0
  204. package/src/client/locales-it.ts +358 -0
  205. package/src/client/locales-ja.ts +367 -0
  206. package/src/client/locales-ko.ts +359 -0
  207. package/src/client/locales-nl.ts +365 -0
  208. package/src/client/locales-pl.ts +369 -0
  209. package/src/client/locales-pt.ts +350 -0
  210. package/src/client/locales-ru.ts +365 -0
  211. package/src/client/locales-sv.ts +350 -0
  212. package/src/client/locales-th.ts +367 -0
  213. package/src/client/locales-tr.ts +367 -0
  214. package/src/client/locales-vi.ts +367 -0
  215. package/src/client/locales-zh-HK.ts +382 -0
  216. package/src/client/locales-zh-MO.ts +382 -0
  217. package/src/client/locales-zh-TW.ts +382 -0
  218. package/src/client/locales.ts +894 -0
  219. package/src/client/markdown-html.ts +331 -0
  220. package/src/client/markdown-images.ts +137 -0
  221. package/src/client/markdown-labels.tsx +52 -0
  222. package/src/client/md-toc.tsx +127 -0
  223. package/src/client/mermaid-blocks.ts +110 -0
  224. package/src/client/mermaid-sanitize.ts +93 -0
  225. package/src/client/mermaid.tsx +406 -0
  226. package/src/client/open-when-sized.ts +43 -0
  227. package/src/client/open-with-settings.tsx +135 -0
  228. package/src/client/open-with.ts +221 -0
  229. package/src/client/openpath-intercept.ts +143 -0
  230. package/src/client/paths.ts +63 -0
  231. package/src/client/pdf-types.ts +4 -0
  232. package/src/client/pinned.ts +183 -0
  233. package/src/client/plugin-settings.ts +43 -0
  234. package/src/client/plugins-shared.ts +29 -0
  235. package/src/client/plugins-tabs.ts +96 -0
  236. package/src/client/plugins-viewers.ts +30 -0
  237. package/src/client/prefs.ts +210 -0
  238. package/src/client/produced-files.ts +120 -0
  239. package/src/client/selection-payload.ts +86 -0
  240. package/src/client/service.ts +859 -0
  241. package/src/client/settings-nav-icon.ts +45 -0
  242. package/src/client/shell-presets.ts +83 -0
  243. package/src/client/sidebar.module.css +2788 -0
  244. package/src/client/sidechat-transcript.ts +321 -0
  245. package/src/client/split-pane.tsx +331 -0
  246. package/src/client/state.ts +1497 -0
  247. package/src/client/subagent-detect.ts +137 -0
  248. package/src/client/subagent-jobs.ts +161 -0
  249. package/src/client/tab-content-memo.ts +55 -0
  250. package/src/client/terminal-font.ts +224 -0
  251. package/src/client/terminal-links.ts +194 -0
  252. package/src/client/theme.ts +110 -0
  253. package/src/client/titlebar-strip.ts +37 -0
  254. package/src/client/upload.ts +187 -0
  255. package/src/client/wco.ts +120 -0
  256. package/src/config.ts +154 -0
  257. package/src/context-types.ts +611 -0
  258. package/src/fs-operations.ts +95 -0
  259. package/src/fs-search.ts +113 -0
  260. package/src/fs-tree.ts +158 -0
  261. package/src/git.ts +446 -0
  262. package/src/html-route.ts +106 -0
  263. package/src/index.ts +1291 -0
  264. package/src/invariant.ts +32 -0
  265. package/src/jobs-routes.ts +257 -0
  266. package/src/open-external.ts +90 -0
  267. package/src/path-security.ts +75 -0
  268. package/src/prefs-shared.ts +280 -0
  269. package/src/pty-deps.ts +240 -0
  270. package/src/pty-manager.ts +376 -0
  271. package/src/sidechat-core.ts +468 -0
  272. package/src/sidechat-routes.ts +344 -0
  273. package/src/subagent-activity.ts +91 -0
  274. package/src/subagent-live-route.ts +93 -0
  275. package/src/tools.ts +482 -0
  276. package/src/trust-fence.ts +84 -0
  277. package/src/wire.ts +100 -0
@@ -0,0 +1,289 @@
1
+ /**
2
+ * The built-in browser tab: an address bar plus a sandboxed iframe.
3
+ *
4
+ * Security model (see browser.ts + the sandbox tokens below): the iframe is
5
+ * ALWAYS sandboxed without `allow-same-origin` (opaque origin — the visited
6
+ * page can never sit on the GUI's origin, read its storage, or reach
7
+ * /sidebar/api) and without `allow-top-navigation` (a page must not hijack
8
+ * the GUI). The address bar only accepts http(s) and refuses loopback /
9
+ * the GUI's own origin. The side card setting "关闭浏览器沙箱" drops the
10
+ * sandbox attribute entirely for fully trusted sites — the visited page then
11
+ * runs with the GUI's own origin and full session access, so a persistent
12
+ * warning bar renders while it is off.
13
+ *
14
+ * The URL is persisted onto the tab (path/title via the patchTab reducer)
15
+ * so a reload restores the visited page; the back/forward stack only tracks
16
+ * address-bar navigations (in-frame link clicks are cross-origin and
17
+ * invisible — a documented limitation).
18
+ */
19
+ import { useEffect, useState } from 'react'
20
+ import {
21
+ IconChevronLeftOutline14,
22
+ IconChevronRightOutline14,
23
+ IconLinkOutline14,
24
+ IconRefreshOutline14,
25
+ IconWarningOutline16,
26
+ } from '@deepseek-ai/dsh-client-ui-primitives'
27
+ import { VscLinkExternal } from 'react-icons/vsc'
28
+ import { api } from './api.ts'
29
+ import { embeddabilityOf, isAllowedLoopbackUrl, normalizeBrowserUrl } from './browser.ts'
30
+ import { patchTab } from './state.ts'
31
+ import { SandboxStatusBar } from './SandboxStatusBar.tsx'
32
+ import { t } from './locales.ts'
33
+ import type { TabComponentProps } from './service.ts'
34
+ import css from './sidebar.module.css'
35
+
36
+ /**
37
+ * The browser iframe sandbox tokens. NO allow-same-origin (opaque origin —
38
+ * no GUI storage/API access), NO allow-top-navigation (a browsed page must
39
+ * not hijack the GUI). allow-forms/allow-popups/allow-downloads/allow-modals
40
+ * keep login flows working; allow-popups-to-escape-sandbox lets OAuth
41
+ * popups open as normal tabs (they are cross-origin to the GUI either way).
42
+ */
43
+ export const BROWSER_IFRAME_SANDBOX =
44
+ 'allow-scripts allow-forms allow-popups allow-downloads allow-modals allow-popups-to-escape-sandbox'
45
+
46
+ /** allow-same-origin appended for explicitly allowlisted local addresses. */
47
+ const BROWSER_IFRAME_SANDBOX_SAME_ORIGIN =
48
+ `${BROWSER_IFRAME_SANDBOX} allow-same-origin`
49
+
50
+ /**
51
+ * The sandbox tokens for one URL: allowlisted loopback addresses (local dev
52
+ * servers the user explicitly trusts) additionally get `allow-same-origin`
53
+ * so Vite/module/HMR pipelines that need a real origin work; every other
54
+ * site keeps the opaque-origin sandbox. `allow-same-origin` does NOT give
55
+ * the page access to the GUI — it stays cross-origin to it and to every
56
+ * other site — but it does give it its OWN origin privileges (localStorage,
57
+ * fetch without CORS), so it is only granted for the explicit allowlist.
58
+ *
59
+ * The GUI itself is the one hard exception: even when its own host is
60
+ * allowlisted (a bare-host entry covers every port, so the GUI origin
61
+ * matches), a page at the GUI's exact origin must never get
62
+ * `allow-same-origin` — that would make it same-origin with its parent and
63
+ * hand it the GUI's storage/API (and the ability to shed the sandbox). The
64
+ * GUI keeps the opaque-origin sandbox no matter what the allowlist says.
65
+ */
66
+ export function iframeSandboxFor(url: string | undefined, allowedLoopback: string, selfOrigin?: string): string | undefined {
67
+ if (url === undefined) return undefined
68
+ if (selfOrigin !== undefined) {
69
+ let parsed: URL
70
+ try {
71
+ parsed = new URL(url)
72
+ } catch {
73
+ return BROWSER_IFRAME_SANDBOX
74
+ }
75
+ if (parsed.origin === selfOrigin) return BROWSER_IFRAME_SANDBOX
76
+ }
77
+ return isAllowedLoopbackUrl(url, allowedLoopback)
78
+ ? BROWSER_IFRAME_SANDBOX_SAME_ORIGIN
79
+ : BROWSER_IFRAME_SANDBOX
80
+ }
81
+
82
+ export function BrowserView(props: TabComponentProps) {
83
+ const { store, tab } = props
84
+ // The current address (initialized from the persisted tab.path so a
85
+ // reload restores the visited page).
86
+ const [url, setUrl] = useState<string | undefined>(tab.path)
87
+ const [input, setInput] = useState<string>(tab.path ?? '')
88
+ /** Blocked/invalid hint shown under the address bar (null = none). */
89
+ const [message, setMessage] = useState<string | null>(null)
90
+ /** Address-bar navigation history (in-frame clicks are not tracked). */
91
+ const [history, setHistory] = useState<string[]>(tab.path !== undefined ? [tab.path] : [])
92
+ const [cursor, setCursor] = useState<number>(tab.path !== undefined ? 0 : -1)
93
+ /** Bumped on reload to remount the iframe (also remounts on sandbox flip). */
94
+ const [reloadKey, setReloadKey] = useState(0)
95
+ /** TEMPORARY sandbox unlock for THIS surface only (never writes the global
96
+ * side card setting; lasts until the tab unmounts or the user restores). */
97
+ const [localUnlock, setLocalUnlock] = useState(false)
98
+ const noSandbox = store.getPrefs().browserNoSandbox === true || localUnlock
99
+ /** A site that refuses to be embedded (X-Frame-Options / frame-ancestors):
100
+ * the probe verdict shown instead of the blank iframe. */
101
+ const [embedBlocked, setEmbedBlocked] = useState<string | null>(null)
102
+ /** The user asked to load the refused site anyway (keeps the plain iframe). */
103
+ const [forceEmbed, setForceEmbed] = useState(false)
104
+
105
+ // Probe every navigation (address bar, history, restored path): when the
106
+ // target forbids embedding, show the reason + open-in-browser instead of
107
+ // the browser's cryptic "refused to connect" blank frame. A failed probe
108
+ // (unreachable) keeps the plain iframe.
109
+ useEffect(() => {
110
+ if (url === undefined) return
111
+ let cancelled = false
112
+ setEmbedBlocked(null)
113
+ setForceEmbed(false)
114
+ void api.browserProbe(url).then((probe) => {
115
+ if (!cancelled && embeddabilityOf(probe) === 'blocked') setEmbedBlocked(url)
116
+ }).catch(() => { /* unreachable: keep the plain iframe */ })
117
+ return () => { cancelled = true }
118
+ }, [url])
119
+
120
+ const persist = (nextUrl: string): void => {
121
+ let host = nextUrl
122
+ try { host = new URL(nextUrl).hostname } catch { /* keep the URL as title */ }
123
+ store.reduce(state => patchTab(state, tab.id, { path: nextUrl, title: host }))
124
+ }
125
+
126
+ const navigateTo = (raw: string): void => {
127
+ const result = normalizeBrowserUrl(raw, window.location.origin, store.getPrefs().browserAllowedLoopback)
128
+ if (result.kind === 'ok') {
129
+ const next = result.url
130
+ setUrl(next)
131
+ setInput(next)
132
+ setMessage(null)
133
+ // Push onto the stack, dropping any stale forward entries.
134
+ setHistory(previous => [...previous.slice(0, cursor + 1), next])
135
+ setCursor(previous => previous + 1)
136
+ setReloadKey(key => key + 1)
137
+ persist(next)
138
+ return
139
+ }
140
+ setMessage(result.kind === 'invalid'
141
+ ? t('browserInvalid')
142
+ : result.reason === 'scheme' ? t('browserBlockedScheme')
143
+ : t('browserBlockedLoopback'))
144
+ }
145
+
146
+ const goBack = (): void => {
147
+ if (cursor <= 0) return
148
+ const next = history[cursor - 1]!
149
+ setCursor(cursor - 1)
150
+ setUrl(next)
151
+ setInput(next)
152
+ setReloadKey(key => key + 1)
153
+ }
154
+
155
+ const goForward = (): void => {
156
+ if (cursor >= history.length - 1) return
157
+ const next = history[cursor + 1]!
158
+ setCursor(cursor + 1)
159
+ setUrl(next)
160
+ setInput(next)
161
+ setReloadKey(key => key + 1)
162
+ }
163
+
164
+ return (
165
+ <div className={css.browser}>
166
+ <div className={css.browserBar}>
167
+ <button
168
+ type="button"
169
+ className={css.iconButton}
170
+ aria-label={t('browserBack')}
171
+ title={t('browserBack')}
172
+ disabled={cursor <= 0}
173
+ onClick={goBack}
174
+ >
175
+ <IconChevronLeftOutline14 />
176
+ </button>
177
+ <button
178
+ type="button"
179
+ className={css.iconButton}
180
+ aria-label={t('browserForward')}
181
+ title={t('browserForward')}
182
+ disabled={cursor >= history.length - 1}
183
+ onClick={goForward}
184
+ >
185
+ <IconChevronRightOutline14 />
186
+ </button>
187
+ <button
188
+ type="button"
189
+ className={css.iconButton}
190
+ aria-label={t('refresh')}
191
+ title={t('refresh')}
192
+ onClick={() => { setReloadKey(key => key + 1) }}
193
+ >
194
+ <IconRefreshOutline14 />
195
+ </button>
196
+ <input
197
+ className={css.browserInput}
198
+ value={input}
199
+ placeholder={t('browserPlaceholder')}
200
+ spellCheck={false}
201
+ onChange={event => { setInput(event.target.value) }}
202
+ onKeyDown={event => {
203
+ if (event.key === 'Enter') navigateTo(input)
204
+ }}
205
+ />
206
+ <button
207
+ type="button"
208
+ className={css.iconButton}
209
+ aria-label={t('browserGo')}
210
+ title={t('browserGo')}
211
+ onClick={() => { navigateTo(input) }}
212
+ >
213
+ <IconLinkOutline14 />
214
+ </button>
215
+ <button
216
+ type="button"
217
+ className={css.iconButton}
218
+ aria-label={t('browserOpenExternal')}
219
+ title={t('browserOpenExternal')}
220
+ disabled={url === undefined}
221
+ onClick={() => {
222
+ if (url !== undefined) window.open(url, '_blank', 'noopener')
223
+ }}
224
+ >
225
+ <VscLinkExternal size={15} />
226
+ </button>
227
+ </div>
228
+ {message !== null && <div className={css.browserMessage}>{message}</div>}
229
+ <SandboxStatusBar
230
+ sandboxed={!noSandbox}
231
+ local={localUnlock}
232
+ dangerCopy={t('browserNoSandboxWarning')}
233
+ onUnlock={() => { setLocalUnlock(true) }}
234
+ onRestore={() => { setLocalUnlock(false) }}
235
+ />
236
+ {url === undefined ? (
237
+ <div className={css.browserStart}>{t('browserStart')}</div>
238
+ ) : embedBlocked !== null && !forceEmbed ? (
239
+ <BrowserEmbedBlocked
240
+ url={embedBlocked}
241
+ onOpenInBrowser={() => { window.open(embedBlocked, '_blank', 'noopener') }}
242
+ onLoadAnyway={() => { setForceEmbed(true) }}
243
+ />
244
+ ) : (
245
+ <iframe
246
+ key={`${reloadKey}:${noSandbox ? 'ns' : 'sb'}`}
247
+ className={css.browserFrame}
248
+ src={url}
249
+ sandbox={noSandbox ? undefined : iframeSandboxFor(url, store.getPrefs().browserAllowedLoopback, window.location.origin)}
250
+ referrerPolicy="no-referrer"
251
+ allow=""
252
+ title={url}
253
+ />
254
+ )}
255
+ </div>
256
+ )
257
+ }
258
+
259
+ /**
260
+ * The embed-refusal panel: shown when the probed site forbids being
261
+ * displayed inside other pages (X-Frame-Options / frame-ancestors) — the
262
+ * iframe would only show the browser's "refused to connect" blank. Explains
263
+ * the reason and offers the real-browser open plus a load-anyway escape.
264
+ * Exported so the copy and the actions are testable without a DOM.
265
+ */
266
+ export function BrowserEmbedBlocked(props: {
267
+ url: string
268
+ onOpenInBrowser: () => void
269
+ onLoadAnyway: () => void
270
+ }) {
271
+ const { url, onOpenInBrowser, onLoadAnyway } = props
272
+ let host = url
273
+ try { host = new URL(url).hostname } catch { /* keep the raw URL */ }
274
+ return (
275
+ <div className={css.browserBlocked}>
276
+ <IconWarningOutline16 size={16} />
277
+ <div className={css.browserBlockedTitle}>{t('browserEmbedBlocked', { host })}</div>
278
+ <div className={css.browserBlockedDesc}>{t('browserEmbedBlockedDesc')}</div>
279
+ <div className={css.browserBlockedActions}>
280
+ <button type="button" className={css.browserBlockedButton} onClick={onOpenInBrowser}>
281
+ {t('browserOpenExternal')}
282
+ </button>
283
+ <button type="button" className={css.browserBlockedButton} onClick={onLoadAnyway}>
284
+ {t('browserEmbedAnyway')}
285
+ </button>
286
+ </div>
287
+ </div>
288
+ )
289
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The diff tab: one change opened from the git panel, like VSCode's diff
3
+ * editor. A worktree ref loads the file's unified diff (`git diff`, staged or
4
+ * not; untracked files — which git diff never covers — render as a full-file
5
+ * addition from their content), a commit ref loads the commit's full patch
6
+ * (`git.show`-style). The header carries a refresh button because the tab
7
+ * stays mounted while the git panel's staging/discard operations change the
8
+ * very content it shows.
9
+ */
10
+ import { useCallback, useEffect, useState } from 'react'
11
+ import { IconRefreshOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
12
+ import type { SessionScope } from './api.ts'
13
+ import { api } from './api.ts'
14
+ import type { SidebarDiffRef } from './state.ts'
15
+ import { DiffView } from './DiffView.tsx'
16
+ import { t } from './locales.ts'
17
+ import { resolveSidebarPath } from './produced-files.ts'
18
+ import css from './sidebar.module.css'
19
+
20
+ /** The loaded diff surface (untracked content rendered as a full addition). */
21
+ interface DiffData {
22
+ diff: string
23
+ untracked?: string
24
+ }
25
+
26
+ export function DiffTab(props: { sessionId: string; cwd: string | undefined; diff: SidebarDiffRef }) {
27
+ const { sessionId, cwd, diff } = props
28
+ const [loading, setLoading] = useState(true)
29
+ const [error, setError] = useState<string | null>(null)
30
+ const [data, setData] = useState<DiffData | null>(null)
31
+ const [tick, setTick] = useState(0)
32
+
33
+ const refresh = useCallback((): void => { setTick(value => value + 1) }, [])
34
+
35
+ useEffect(() => {
36
+ let cancelled = false
37
+ const scope: SessionScope = { sessionId, cwd, ...(diff.repoRoot !== undefined ? { repoRoot: diff.repoRoot } : {}) }
38
+ setLoading(true)
39
+ setError(null)
40
+ setData(null)
41
+ const load = async (): Promise<void> => {
42
+ try {
43
+ if (diff.kind === 'commit') {
44
+ const result = await api.gitCommitDiff(scope, diff.hashFull, diff.worktree)
45
+ if (!cancelled) setData({ diff: result.diff })
46
+ return
47
+ }
48
+ let result = await api.gitDiff(scope, diff.path, diff.staged, diff.worktree)
49
+ if (result.diff === '') {
50
+ // The requested side is empty — try the OTHER side once: the ref
51
+ // may predate the staged-flag fix, or the change moved sides (a
52
+ // file staged after its tab opened). Both sides empty means the
53
+ // file genuinely has no text changes.
54
+ const other = await api.gitDiff(scope, diff.path, !diff.staged, diff.worktree)
55
+ if (other.diff !== '') result = other
56
+ }
57
+ if (result.diff !== '') {
58
+ if (!cancelled) setData({ diff: result.diff })
59
+ return
60
+ }
61
+ // Empty diff: an untracked file (git diff never lists it) falls back
62
+ // to a full-file addition; anything else is a genuine no-text-change.
63
+ if (diff.untracked === true && !diff.staged) {
64
+ // A child-repo path is relative to diff.repoRoot, not the session
65
+ // cwd or the linked-worktree root; resolve against whichever the
66
+ // diff ref carries so the untracked fallback reads the right file.
67
+ const text = await api.fsRead(scope, resolveSidebarPath(diff.repoRoot ?? diff.worktree ?? cwd, diff.path))
68
+ if (!cancelled) {
69
+ setData(text.kind === 'text' ? { diff: '', untracked: text.content } : { diff: '' })
70
+ }
71
+ return
72
+ }
73
+ if (!cancelled) setData({ diff: '' })
74
+ } catch (reason) {
75
+ if (!cancelled) setError(reason instanceof Error ? reason.message : String(reason))
76
+ } finally {
77
+ if (!cancelled) setLoading(false)
78
+ }
79
+ }
80
+ void load()
81
+ return () => { cancelled = true }
82
+ }, [sessionId, cwd, diff, tick])
83
+
84
+ return (
85
+ <div className={css.gitDiffTab}>
86
+ <div className={css.gitDiffTabHeader}>
87
+ <span className={css.gitDiffTabTitle} title={diff.kind === 'worktree' ? diff.path : `${diff.hash} ${diff.subject}`}>
88
+ {diff.kind === 'worktree' ? diff.path : `${diff.hash} ${diff.subject}`}
89
+ </span>
90
+ <button
91
+ type="button"
92
+ className={css.iconButton}
93
+ aria-label={t('refresh')}
94
+ title={t('refresh')}
95
+ onClick={refresh}
96
+ >
97
+ <IconRefreshOutline16 size={14} />
98
+ </button>
99
+ </div>
100
+ {loading && <div className={css.gitPlaceholder}>{t('loading')}</div>}
101
+ {!loading && error !== null && <div className={css.gitError}>{t('diffLoadError')}: {error}</div>}
102
+ {!loading && error === null && data !== null && (
103
+ <>
104
+ {data.untracked !== undefined
105
+ ? <DiffView diff="" untrackedPath={diff.kind === 'worktree' ? diff.path : ''} untrackedContent={data.untracked} />
106
+ : <DiffView diff={data.diff} />}
107
+ {data.diff === '' && data.untracked === undefined && (
108
+ <div className={css.gitEmpty}>{t('diffEmpty')}</div>
109
+ )}
110
+ </>
111
+ )}
112
+ </div>
113
+ )
114
+ }