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,717 @@
1
+ /**
2
+ * The source-control panel: status list (staged vs unstaged), stage/unstage,
3
+ * commit with a message box, branch switch, and a VSCode-like history — rows
4
+ * carry branch decorations, author and relative time. Clicking a changed
5
+ * file or a history row opens a dedicated diff TAB (see {@link DiffTab}),
6
+ * placed below the git pane on first use. File rows and history rows open a
7
+ * right-click context menu with advanced operations (open in editor, discard,
8
+ * revert, cherry-pick, copy paths/hashes). Refresh is manual + on mount/
9
+ * focus. While visible it polls lightweight porcelain state so model-authored
10
+ * file changes appear without a manual refresh.
11
+ */
12
+ import { useCallback, useEffect, useRef, useState, type MouseEvent, type ReactNode } from 'react'
13
+ import {
14
+ Button, IconBranchOutline16, IconCodeOutline16, IconCopyOutline16, IconRefreshOutline16,
15
+ IconTrashOutline16, Input, Menu, Modal, writeClipboard,
16
+ } from '@deepseek-ai/dsh-client-ui-primitives'
17
+ import type { GitLogEntry, GitStatusEntry, GitStatusResult, GitWorktree, SessionScope } from './api.ts'
18
+ import { api } from './api.ts'
19
+ import { isWithinWorkspace, relativeTo } from './paths.ts'
20
+ import { resolveSidebarPath } from './produced-files.ts'
21
+ import { relativeTime, t } from './locales.ts'
22
+ import type { SidebarTab } from './state.ts'
23
+ import css from './sidebar.module.css'
24
+
25
+ /** The XY status letters a row badge shows (X = index, Y = worktree). */
26
+ function badgeOf(entry: GitStatusEntry): string {
27
+ const index = entry.xy[0]
28
+ const worktree = entry.xy[1]
29
+ if (index !== undefined && index !== ' ' && index !== '?') return index
30
+ if (worktree !== undefined && worktree !== ' ' && worktree !== '?') return worktree
31
+ return '?'
32
+ }
33
+
34
+ /** Whether the entry carries STAGED (index) changes — the X letter is set. */
35
+ function isStagedEntry(entry: GitStatusEntry): boolean {
36
+ const index = entry.xy[0]
37
+ return index !== undefined && index !== ' ' && index !== '?'
38
+ }
39
+
40
+ /** Whether the entry carries UNSTAGED (worktree) changes — the Y letter is set
41
+ * (untracked `??` counts as unstaged: it is a worktree-only change). A file
42
+ * with both letters set ('MM') lands in BOTH sections. */
43
+ function isUnstagedEntry(entry: GitStatusEntry): boolean {
44
+ if (entry.xy === '??') return true
45
+ const worktree = entry.xy[1]
46
+ return worktree !== undefined && worktree !== ' ' && worktree !== '?'
47
+ }
48
+
49
+ /** Whether the entry is untracked (`??`): git diff never includes it. */
50
+ function isUntracked(entry: GitStatusEntry): boolean {
51
+ return badgeOf(entry) === '?'
52
+ }
53
+
54
+ /** The last path segment (tab title for a file's diff). */
55
+ function baseName(path: string): string {
56
+ const at = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
57
+ return at === -1 ? path : path.slice(at + 1)
58
+ }
59
+
60
+ /** The ref names of one log row's decorations (`HEAD -> main` → `main`), deduped. */
61
+ function refNames(refs: string): string[] {
62
+ return [...new Set(
63
+ refs
64
+ .split(',')
65
+ .map(ref => ref.trim())
66
+ .filter(ref => ref !== '')
67
+ .map(ref => (ref.includes(' -> ') ? ref.slice(ref.indexOf(' -> ') + 4) : ref))
68
+ .map(ref => (ref.startsWith('tag: ') ? ref.slice(5) : ref)),
69
+ )]
70
+ }
71
+
72
+ /** The pending destructive action (discard / revert / cherry-pick), gated by a confirm modal. */
73
+ interface ConfirmState {
74
+ title: string
75
+ description: string
76
+ confirmLabel: string
77
+ onConfirm: () => Promise<unknown>
78
+ }
79
+
80
+ /** History batch size: the log loads lazily in pages so a long history never
81
+ * floods the panel at once (the end of the log is reached by paging). */
82
+ const LOG_BATCH = 20
83
+
84
+ export function GitView(props: {
85
+ scope: SessionScope
86
+ onOpenFile: (path: string) => void
87
+ /** Open a diff tab (the shell places it below the git pane on first use). */
88
+ onOpenDiff: (tab: SidebarTab) => void
89
+ /** Poll only while the tab is actually visible. */
90
+ visible: boolean
91
+ }) {
92
+ const { scope, onOpenFile, onOpenDiff, visible } = props
93
+ const [status, setStatus] = useState<GitStatusResult | null>(null)
94
+ const [worktrees, setWorktrees] = useState<GitWorktree[]>([])
95
+ const [selectedWorktree, setSelectedWorktree] = useState<string | undefined>()
96
+ const [repoRoot, setRepoRoot] = useState<string | undefined>(undefined)
97
+ const [loading, setLoading] = useState(true)
98
+ const [error, setError] = useState<string | null>(null)
99
+ const [branchNames, setBranchNames] = useState<string[]>([])
100
+ const [logEntries, setLogEntries] = useState<GitLogEntry[]>([])
101
+ const [commitMsg, setCommitMsg] = useState('')
102
+ const [busy, setBusy] = useState(false)
103
+ const [commitError, setCommitError] = useState<string | null>(null)
104
+ /** Whether the history was fully paged (a batch shorter than LOG_BATCH). */
105
+ const [logEnded, setLogEnded] = useState(false)
106
+ const [logLoadingMore, setLogLoadingMore] = useState(false)
107
+
108
+ /** The open file-row context menu (cursor position for the portaled Menu). */
109
+ const [fileMenu, setFileMenu] = useState<{ entry: GitStatusEntry; staged: boolean; x: number; y: number } | null>(null)
110
+ /** The open history-row context menu. */
111
+ const [historyMenu, setHistoryMenu] = useState<{ entry: GitLogEntry; x: number; y: number } | null>(null)
112
+ /** The pending destructive action awaiting confirmation. */
113
+ const [confirm, setConfirm] = useState<ConfirmState | null>(null)
114
+ const refreshInFlight = useRef(false)
115
+ /** Monotonic request id: a manual worktree switch invalidates any older poll
116
+ * before it can publish state from the previous checkout. */
117
+ const refreshGeneration = useRef(0)
118
+ const worktreeChosenByUser = useRef(false)
119
+ /** selectedWorktree read inside refresh without re-creating the callback:
120
+ * avoids a spurious full refresh on every auto-select (the very state
121
+ * change refresh writes back via setSelectedWorktree would recreate the
122
+ * callback and re-trigger the mount effect — an N→N+1 fetch loop). */
123
+ const selectedRef = useRef<string | undefined>(undefined)
124
+ useEffect(() => { selectedRef.current = selectedWorktree }, [selectedWorktree])
125
+
126
+ const gitScope: SessionScope = repoRoot === undefined ? scope : { ...scope, repoRoot }
127
+
128
+ /** Publish a complete checkout-derived view. Status, branch choices and
129
+ * history are one consistency unit: never mix rows from two worktrees. */
130
+ const refreshTarget = useCallback(async (
131
+ target: string | undefined,
132
+ options: { loading: boolean; generation: number },
133
+ ): Promise<void> => {
134
+ if (options.loading) setLoading(true)
135
+ setError(null)
136
+ try {
137
+ const [statusResult, branchResult, logResult] = await Promise.all([
138
+ api.gitStatus(gitScope, target),
139
+ api.gitBranch(gitScope, target).catch(() => ({ current: '', names: [] as string[] })),
140
+ api.gitLog(gitScope, LOG_BATCH, 0, target).catch(() => [] as GitLogEntry[]),
141
+ ])
142
+ if (options.generation !== refreshGeneration.current) return
143
+ setStatus(statusResult)
144
+ if (statusResult.root !== undefined && statusResult.root !== repoRoot) setRepoRoot(statusResult.root)
145
+ setBranchNames(branchResult.names)
146
+ setLogEntries(logResult)
147
+ setLogEnded(logResult.length < LOG_BATCH)
148
+ } catch (reason) {
149
+ if (options.generation === refreshGeneration.current) {
150
+ setError(reason instanceof Error ? reason.message : String(reason))
151
+ }
152
+ } finally {
153
+ if (options.loading && options.generation === refreshGeneration.current) setLoading(false)
154
+ }
155
+ }, [scope.sessionId, scope.cwd, repoRoot])
156
+
157
+ const refresh = useCallback(async (silent = false): Promise<void> => {
158
+ if (refreshInFlight.current) return
159
+ refreshInFlight.current = true
160
+ let generation = refreshGeneration.current
161
+ try {
162
+ const listed = await api.gitWorktrees(scope)
163
+ if (generation !== refreshGeneration.current) return
164
+ setWorktrees(listed)
165
+ const selectedStillExists = listed.some(entry => entry.path === selectedRef.current)
166
+ let target = selectedStillExists ? selectedRef.current : listed.find(entry => entry.current)?.path
167
+ // DSH and other coding agents commonly create one linked checkout while
168
+ // the session remains rooted at the clean primary checkout. Select that
169
+ // checkout automatically only when the choice is unambiguous.
170
+ const current = listed.find(entry => entry.current)
171
+ const dirtyLinked = listed.filter(entry => !entry.current && entry.changes > 0)
172
+ if (!worktreeChosenByUser.current) {
173
+ target = (current?.changes ?? 0) === 0 && dirtyLinked.length === 1
174
+ ? dirtyLinked[0]!.path
175
+ : current?.path
176
+ }
177
+ const targetChanged = target !== selectedRef.current
178
+ if (targetChanged) {
179
+ // Changing the automatically selected checkout invalidates any direct
180
+ // target refresh that may still be resolving for the previous one.
181
+ generation = refreshGeneration.current += 1
182
+ selectedRef.current = target
183
+ setSelectedWorktree(target)
184
+ // Remove rows owned by the previous checkout immediately: keeping them
185
+ // interactive while the target changes could apply a destructive action
186
+ // to the new checkout with stale history from the old one.
187
+ setStatus(null)
188
+ setBranchNames([])
189
+ setLogEntries([])
190
+ setLogEnded(false)
191
+ setLogLoadingMore(false)
192
+ }
193
+ // A poll may update status alone only while staying on the same checkout.
194
+ // Any automatic selection change refreshes the complete derived view.
195
+ if (silent && !targetChanged) {
196
+ const statusResult = await api.gitStatus(gitScope, target)
197
+ if (generation === refreshGeneration.current) setStatus(statusResult)
198
+ return
199
+ }
200
+ await refreshTarget(target, { loading: !silent, generation })
201
+ } catch (reason) {
202
+ if (generation === refreshGeneration.current) {
203
+ setError(reason instanceof Error ? reason.message : String(reason))
204
+ if (!silent) setLoading(false)
205
+ }
206
+ } finally {
207
+ refreshInFlight.current = false
208
+ }
209
+ }, [scope.sessionId, scope.cwd, refreshTarget])
210
+
211
+ useEffect(() => {
212
+ refreshGeneration.current += 1
213
+ refreshInFlight.current = false
214
+ worktreeChosenByUser.current = false
215
+ selectedRef.current = undefined
216
+ setSelectedWorktree(undefined)
217
+ }, [scope.sessionId, scope.cwd])
218
+ useEffect(() => { void refresh() }, [refresh])
219
+
220
+ /** A user choice invalidates any older poll and atomically refreshes every
221
+ * checkout-derived surface before destructive history actions can run. */
222
+ const chooseWorktree = (target: string): void => {
223
+ worktreeChosenByUser.current = true
224
+ selectedRef.current = target
225
+ setSelectedWorktree(target)
226
+ setStatus(null)
227
+ setBranchNames([])
228
+ setLogEntries([])
229
+ setLogEnded(false)
230
+ setLogLoadingMore(false)
231
+ const generation = refreshGeneration.current += 1
232
+ void refreshTarget(target, { loading: true, generation })
233
+ }
234
+ /** Switching the selected child repository must invalidate every
235
+ * target-derived surface (status/history/log) before the asynchronous
236
+ * refresh resolves; otherwise stale rows remain actionable while their
237
+ * handlers already address the new repository. Mirrors chooseWorktree. */
238
+ const chooseRepo = (target: string): void => {
239
+ setRepoRoot(target)
240
+ setStatus(null)
241
+ setBranchNames([])
242
+ setLogEntries([])
243
+ setLogEnded(false)
244
+ setLogLoadingMore(false)
245
+ // Re-list worktrees for the selected child (a workspace container's
246
+ // own worktree list is empty); keep the current linked-checkout choice
247
+ // unless it does not belong to the new repository.
248
+ const generation = refreshGeneration.current += 1
249
+ void refreshTarget(selectedRef.current ?? '', { loading: true, generation })
250
+ }
251
+ useEffect(() => {
252
+ if (!visible) return
253
+ const timer = window.setInterval(() => { void refresh(true) }, 2_000)
254
+ return () => { window.clearInterval(timer) }
255
+ }, [visible, refresh])
256
+
257
+ /** Append the next history page (lazy: only when the user asks for more). */
258
+ const loadMoreLog = async (): Promise<void> => {
259
+ if (logLoadingMore || logEnded) return
260
+ const generation = refreshGeneration.current
261
+ const target = selectedRef.current
262
+ setLogLoadingMore(true)
263
+ try {
264
+ const next = await api.gitLog(gitScope, LOG_BATCH, logEntries.length, target)
265
+ // A worktree switch clears the old history and increments generation.
266
+ // Never append a late page from that checkout into the new one.
267
+ if (generation !== refreshGeneration.current || target !== selectedRef.current) return
268
+ setLogEntries(entries => [...entries, ...next])
269
+ if (next.length < LOG_BATCH) setLogEnded(true)
270
+ } catch (reason) {
271
+ if (generation === refreshGeneration.current && target === selectedRef.current) {
272
+ setCommitError(`${t('historyLoadError')}: ${reason instanceof Error ? reason.message : String(reason)}`)
273
+ }
274
+ } finally {
275
+ if (generation === refreshGeneration.current && target === selectedRef.current) setLogLoadingMore(false)
276
+ }
277
+ }
278
+
279
+ /** The diff tab for one changed file (one tab per path+side; same id = focused). */
280
+ const openWorktreeDiff = (entry: GitStatusEntry, staged: boolean): void => {
281
+ onOpenDiff({
282
+ id: `diff:w:${encodeURIComponent(selectedWorktree ?? '')}:${staged ? 's' : 'u'}:${entry.path}`,
283
+ type: 'diff',
284
+ title: baseName(entry.path),
285
+ diff: { kind: 'worktree', path: entry.path, staged, untracked: isUntracked(entry), worktree: selectedWorktree, repoRoot },
286
+ })
287
+ }
288
+
289
+ /** The diff tab for one commit (one tab per commit). */
290
+ const openCommitDiff = (entry: GitLogEntry): void => {
291
+ onOpenDiff({
292
+ id: `diff:c:${encodeURIComponent(selectedWorktree ?? '')}:${entry.hashFull}`,
293
+ type: 'diff',
294
+ title: `${entry.hash} ${entry.subject}`,
295
+ diff: { kind: 'commit', hash: entry.hash, hashFull: entry.hashFull, subject: entry.subject, worktree: selectedWorktree, repoRoot },
296
+ })
297
+ }
298
+
299
+ const stageEntry = async (entry: GitStatusEntry, staged: boolean): Promise<void> => {
300
+ setBusy(true)
301
+ try {
302
+ if (staged) await api.gitUnstage(gitScope, entry.path, selectedWorktree)
303
+ else await api.gitStage(gitScope, entry.path, selectedWorktree)
304
+ await refresh()
305
+ } finally {
306
+ setBusy(false)
307
+ }
308
+ }
309
+
310
+ const stageAll = async (staged: boolean): Promise<void> => {
311
+ setBusy(true)
312
+ try {
313
+ if (staged) await api.gitUnstage(gitScope, undefined, selectedWorktree)
314
+ else await api.gitStage(gitScope, undefined, selectedWorktree)
315
+ await refresh()
316
+ } finally {
317
+ setBusy(false)
318
+ }
319
+ }
320
+
321
+ const commit = async (): Promise<void> => {
322
+ const message = commitMsg.trim()
323
+ if (message === '' || busy) return
324
+ setBusy(true)
325
+ setCommitError(null)
326
+ try {
327
+ await api.gitCommit(gitScope, message, selectedWorktree)
328
+ setCommitMsg('')
329
+ await refresh()
330
+ } catch (reason) {
331
+ setCommitError(reason instanceof Error ? reason.message : String(reason))
332
+ } finally {
333
+ setBusy(false)
334
+ }
335
+ }
336
+
337
+ const checkout = async (branch: string): Promise<void> => {
338
+ if (branch === status?.branch || busy) return
339
+ setBusy(true)
340
+ setCommitError(null)
341
+ try {
342
+ await api.gitCheckout(gitScope, branch, selectedWorktree)
343
+ await refresh()
344
+ } catch (reason) {
345
+ setCommitError(`${t('checkoutError')}: ${reason instanceof Error ? reason.message : String(reason)}`)
346
+ } finally {
347
+ setBusy(false)
348
+ }
349
+ }
350
+
351
+ /** Run one destructive operation after the confirm modal, then refresh. */
352
+ const runConfirmed = (confirmState: ConfirmState): void => {
353
+ setConfirm({ ...confirmState, onConfirm: async () => {
354
+ setBusy(true)
355
+ setCommitError(null)
356
+ try {
357
+ await confirmState.onConfirm()
358
+ await refresh()
359
+ } catch (reason) {
360
+ setCommitError(reason instanceof Error ? reason.message : String(reason))
361
+ } finally {
362
+ setBusy(false)
363
+ }
364
+ } })
365
+ }
366
+
367
+ /** Copy `text` to the clipboard (best-effort; no visual feedback needed — the menu closes). */
368
+ const copy = (text: string): void => {
369
+ void writeClipboard(text)
370
+ }
371
+
372
+ const openFileMenu = (event: MouseEvent, entry: GitStatusEntry, staged: boolean): void => {
373
+ event.preventDefault()
374
+ event.stopPropagation()
375
+ setFileMenu({ entry, staged, x: event.clientX, y: event.clientY })
376
+ }
377
+
378
+ const openHistoryMenu = (event: MouseEvent, entry: GitLogEntry): void => {
379
+ event.preventDefault()
380
+ event.stopPropagation()
381
+ setHistoryMenu({ entry, x: event.clientX, y: event.clientY })
382
+ }
383
+
384
+ const stagedEntries = (status?.entries ?? []).filter(isStagedEntry)
385
+ const unstagedEntries = (status?.entries ?? []).filter(isUnstagedEntry)
386
+
387
+ const renderEntry = (entry: GitStatusEntry, staged: boolean): ReactNode => {
388
+ return (
389
+ <div key={`${staged ? 's' : 'u'}:${entry.path}`} className={css.gitRow}>
390
+ <button
391
+ type="button"
392
+ className={css.gitRowMain}
393
+ title={entry.path}
394
+ onClick={() => { openWorktreeDiff(entry, staged) }}
395
+ onContextMenu={(event) => { openFileMenu(event, entry, staged) }}
396
+ >
397
+ <span className={css.gitBadge}>{badgeOf(entry)}</span>
398
+ <span className={css.gitName}>{entry.path}</span>
399
+ </button>
400
+ <button
401
+ type="button"
402
+ className={css.iconButton}
403
+ aria-label={staged ? t('unstage') : t('stage')}
404
+ title={staged ? t('unstage') : t('stage')}
405
+ disabled={busy}
406
+ onClick={() => { void stageEntry(entry, staged) }}
407
+ >
408
+ {staged ? <IconTrashOutline16 /> : <IconBranchOutline16 />}
409
+ </button>
410
+ </div>
411
+ )
412
+ }
413
+
414
+ return (
415
+ <div className={css.git}>
416
+ {worktrees.length > 1 && (
417
+ <div className={css.gitWorktreeRow}>
418
+ <span className={css.gitWorktreeLabel}>{t('worktree')}</span>
419
+ <select
420
+ className={css.gitBranchSelect}
421
+ value={selectedWorktree ?? ''}
422
+ title={selectedWorktree}
423
+ disabled={busy}
424
+ onChange={(event) => { chooseWorktree(event.target.value) }}
425
+ >
426
+ {worktrees.map(entry => (
427
+ <option key={entry.path} value={entry.path}>
428
+ {entry.branch} · {baseName(entry.path)} ({entry.changes})
429
+ </option>
430
+ ))}
431
+ </select>
432
+ </div>
433
+ )}
434
+ <div className={css.gitHeader}>
435
+ {(status?.repositories?.length ?? 0) > 1 && (
436
+ <select
437
+ className={css.gitBranchSelect}
438
+ value={repoRoot ?? ''}
439
+ title={repoRoot}
440
+ onChange={(event) => { chooseRepo(event.target.value) }}
441
+ disabled={busy}
442
+ >
443
+ {status!.repositories!.map(root => <option key={root} value={root}>{baseName(root)}</option>)}
444
+ </select>
445
+ )}
446
+ <select
447
+ className={css.gitBranchSelect}
448
+ value={status?.branch ?? ''}
449
+ onChange={(event) => { void checkout(event.target.value) }}
450
+ disabled={busy || (status !== null && !status.isRepo)}
451
+ >
452
+ {(status?.branch ?? '') !== '' && <option value={status!.branch}>{status!.branch}</option>}
453
+ {branchNames.filter(name => name !== status?.branch).map(name => <option key={name} value={name}>{name}</option>)}
454
+ </select>
455
+ <button
456
+ type="button"
457
+ className={css.iconButton}
458
+ aria-label={t('refresh')}
459
+ title={t('refresh')}
460
+ onClick={() => { void refresh() }}
461
+ >
462
+ <IconRefreshOutline16 size={14} />
463
+ </button>
464
+ </div>
465
+
466
+ {loading && <div className={css.gitPlaceholder}>{t('loading')}</div>}
467
+ {!loading && error !== null && <div className={css.gitError}>{error}</div>}
468
+ {!loading && status !== null && !status.isRepo && (
469
+ <div className={css.gitPlaceholder}>{t('notRepo')}</div>
470
+ )}
471
+
472
+ {status !== null && status.isRepo && (
473
+ <>
474
+ {status.truncated === true && (
475
+ <div className={css.gitEmpty}>{t('statusTruncated')}</div>
476
+ )}
477
+ <div className={css.gitSection}>
478
+ <div className={css.gitSectionHeader}>
479
+ <span>{t('staged')} ({stagedEntries.length})</span>
480
+ {stagedEntries.length > 0 && (
481
+ <button type="button" className={css.gitLink} disabled={busy} onClick={() => { void stageAll(true) }}>
482
+ {t('unstageAll')}
483
+ </button>
484
+ )}
485
+ </div>
486
+ {stagedEntries.length === 0 && <div className={css.gitEmpty}>{t('noChanges')}</div>}
487
+ {stagedEntries.map(entry => renderEntry(entry, true))}
488
+ </div>
489
+ <div className={css.gitSection}>
490
+ <div className={css.gitSectionHeader}>
491
+ <span>{t('unstaged')} ({unstagedEntries.length})</span>
492
+ {unstagedEntries.length > 0 && (
493
+ <button type="button" className={css.gitLink} disabled={busy} onClick={() => { void stageAll(false) }}>
494
+ {t('stageAll')}
495
+ </button>
496
+ )}
497
+ </div>
498
+ {unstagedEntries.length === 0 && <div className={css.gitEmpty}>{t('noChanges')}</div>}
499
+ {unstagedEntries.map(entry => renderEntry(entry, false))}
500
+ </div>
501
+
502
+ <div className={css.gitCommit}>
503
+ <Input
504
+ className={css.gitCommitInput}
505
+ placeholder={t('commitPlaceholder')}
506
+ value={commitMsg}
507
+ disabled={busy}
508
+ onChange={(event) => { setCommitMsg(event.target.value); setCommitError(null) }}
509
+ onKeyDown={(event) => {
510
+ if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') void commit()
511
+ }}
512
+ />
513
+ <button
514
+ type="button"
515
+ className={css.gitCommitButton}
516
+ disabled={busy || commitMsg.trim() === '' || stagedEntries.length === 0}
517
+ onClick={() => { void commit() }}
518
+ >
519
+ {t('commit')}
520
+ </button>
521
+ </div>
522
+ {commitError !== null && <div className={css.gitError}>{commitError}</div>}
523
+
524
+ <div className={css.gitSection}>
525
+ <div className={css.gitSectionHeader}><span>{t('history')}</span></div>
526
+ {logEntries.map(entry => (
527
+ <div
528
+ key={entry.hashFull}
529
+ role="button"
530
+ tabIndex={0}
531
+ className={css.gitLogRow}
532
+ title={`${entry.author} · ${entry.date}\n${entry.hashFull}`}
533
+ onClick={() => { openCommitDiff(entry) }}
534
+ onKeyDown={(event) => {
535
+ if (event.key === 'Enter' || event.key === ' ') {
536
+ event.preventDefault()
537
+ openCommitDiff(entry)
538
+ }
539
+ }}
540
+ onContextMenu={(event) => { openHistoryMenu(event, entry) }}
541
+ >
542
+ <span className={css.gitLogLine1}>
543
+ <span className={css.gitLogHash}>{entry.hash}</span>
544
+ <span className={css.gitLogSubject}>{entry.subject}</span>
545
+ </span>
546
+ <span className={css.gitLogLine2}>
547
+ {refNames(entry.refs).map(ref => (
548
+ <span key={ref} className={css.gitLogRef}>{ref}</span>
549
+ ))}
550
+ <span className={css.gitLogMeta}>{entry.author} · {relativeTime(entry.date)}</span>
551
+ </span>
552
+ </div>
553
+ ))}
554
+ {!logEnded && (
555
+ <button
556
+ type="button"
557
+ className={css.gitLogMore}
558
+ disabled={logLoadingMore || busy}
559
+ onClick={() => { void loadMoreLog() }}
560
+ >
561
+ {logLoadingMore ? t('loading') : t('loadMore')}
562
+ </button>
563
+ )}
564
+ </div>
565
+
566
+ {/*
567
+ The one shared file-row context menu, positioned at the right-click
568
+ cursor (portal so the panel's overflow clip cannot crop it).
569
+ */}
570
+ <Menu
571
+ open={fileMenu !== null}
572
+ onClose={() => { setFileMenu(null) }}
573
+ items={[
574
+ // A linked worktree outside the session workspace cannot be
575
+ // opened in the editor: the host's workspace fence rejects
576
+ // every path under it. Hide the action for that checkout so
577
+ // the menu does not offer a no-op that confuses the user.
578
+ ...(fileMenu !== null && isWithinWorkspace(scope.cwd ?? '', resolveSidebarPath(repoRoot ?? selectedWorktree ?? scope.cwd, fileMenu.entry.path))
579
+ ? [{ id: 'open', label: t('openEditor'), icon: <IconCodeOutline16 size={14} /> }]
580
+ : []),
581
+ fileMenu?.staged === true
582
+ ? { id: 'stage', label: t('unstage'), icon: <IconTrashOutline16 size={14} /> }
583
+ : { id: 'stage', label: t('stage'), icon: <IconBranchOutline16 size={14} /> },
584
+ ...(fileMenu !== null && !isUntracked(fileMenu.entry)
585
+ ? [{ id: 'discard', label: t('discard'), icon: <IconTrashOutline16 size={14} />, danger: true }]
586
+ : []),
587
+ { type: 'separator', id: 'sep1' },
588
+ { id: 'relative', label: t('copyRelative'), icon: <IconCopyOutline16 size={14} /> },
589
+ { id: 'absolute', label: t('copyAbsolute'), icon: <IconCopyOutline16 size={14} /> },
590
+ ]}
591
+ onSelect={(id) => {
592
+ const target = fileMenu
593
+ if (target === null) return
594
+ setFileMenu(null)
595
+ if (id === 'open') {
596
+ const resolved = resolveSidebarPath(repoRoot ?? selectedWorktree ?? scope.cwd, target.entry.path)
597
+ // Defense-in-depth: the menu hides this action when the
598
+ // resolved path escapes the session workspace, but a
599
+ // racing repo switch could still reach here with a path
600
+ // the host would reject. No-op in that case.
601
+ if (!isWithinWorkspace(scope.cwd ?? '', resolved)) return
602
+ onOpenFile(resolved)
603
+ return
604
+ }
605
+ if (id === 'stage') {
606
+ void stageEntry(target.entry, target.staged)
607
+ return
608
+ }
609
+ if (id === 'discard') {
610
+ runConfirmed({
611
+ title: t('discardTitle'),
612
+ description: t('discardDesc', { path: target.entry.path }),
613
+ confirmLabel: t('discard'),
614
+ onConfirm: () => api.gitDiscard(gitScope, target.entry.path, selectedWorktree),
615
+ })
616
+ return
617
+ }
618
+ if (id === 'relative') {
619
+ copy(relativeTo(repoRoot ?? selectedWorktree ?? scope.cwd ?? '', target.entry.path))
620
+ return
621
+ }
622
+ if (id === 'absolute') copy(resolveSidebarPath(repoRoot ?? selectedWorktree ?? scope.cwd, target.entry.path))
623
+ }}
624
+ portal
625
+ align="start"
626
+ getAnchorRect={() => (fileMenu === null ? null : new DOMRect(fileMenu.x, fileMenu.y, 0, 0))}
627
+ anchor={<span />}
628
+ />
629
+
630
+ {/* The shared history-row context menu. */}
631
+ <Menu
632
+ open={historyMenu !== null}
633
+ onClose={() => { setHistoryMenu(null) }}
634
+ items={[
635
+ { id: 'view', label: t('viewCommitDiff') },
636
+ { id: 'copyShort', label: t('copyShortHash'), icon: <IconCopyOutline16 size={14} /> },
637
+ { id: 'copyFull', label: t('copyFullHash'), icon: <IconCopyOutline16 size={14} /> },
638
+ { id: 'copySubject', label: t('copySubject'), icon: <IconCopyOutline16 size={14} /> },
639
+ { type: 'separator', id: 'sep2' },
640
+ { id: 'revert', label: t('revertCommit'), danger: true },
641
+ { id: 'cherryPick', label: t('cherryPickCommit'), danger: true },
642
+ ]}
643
+ onSelect={(id) => {
644
+ const target = historyMenu
645
+ if (target === null) return
646
+ setHistoryMenu(null)
647
+ if (id === 'view') {
648
+ openCommitDiff(target.entry)
649
+ return
650
+ }
651
+ if (id === 'copyShort') {
652
+ copy(target.entry.hash)
653
+ return
654
+ }
655
+ if (id === 'copyFull') {
656
+ copy(target.entry.hashFull)
657
+ return
658
+ }
659
+ if (id === 'copySubject') {
660
+ copy(target.entry.subject)
661
+ return
662
+ }
663
+ if (id === 'revert') {
664
+ runConfirmed({
665
+ title: t('revertTitle'),
666
+ description: t('revertDesc', { subject: target.entry.subject }),
667
+ confirmLabel: t('revertCommit'),
668
+ onConfirm: () => api.gitRevert(gitScope, target.entry.hashFull, selectedWorktree),
669
+ })
670
+ return
671
+ }
672
+ if (id === 'cherryPick') {
673
+ runConfirmed({
674
+ title: t('cherryPickTitle'),
675
+ description: t('cherryPickDesc', { subject: target.entry.subject }),
676
+ confirmLabel: t('cherryPickCommit'),
677
+ onConfirm: () => api.gitCherryPick(gitScope, target.entry.hashFull, selectedWorktree),
678
+ })
679
+ }
680
+ }}
681
+ portal
682
+ align="start"
683
+ getAnchorRect={() => (historyMenu === null ? null : new DOMRect(historyMenu.x, historyMenu.y, 0, 0))}
684
+ anchor={<span />}
685
+ />
686
+
687
+ {/* Destructive actions land here first: Cancel / Confirm. */}
688
+ <Modal
689
+ open={confirm !== null}
690
+ onClose={() => { setConfirm(null) }}
691
+ title={confirm?.title ?? ''}
692
+ closeLabel={t('cancel')}
693
+ footer={(
694
+ <>
695
+ <Button variant="outline" onClick={() => { setConfirm(null) }}>{t('cancel')}</Button>
696
+ <Button
697
+ variant="primary"
698
+ disabled={busy}
699
+ onClick={() => {
700
+ const pending = confirm
701
+ if (pending === null) return
702
+ setConfirm(null)
703
+ void pending.onConfirm()
704
+ }}
705
+ >
706
+ {confirm?.confirmLabel ?? ''}
707
+ </Button>
708
+ </>
709
+ )}
710
+ >
711
+ <p className={css.gitConfirmDesc}>{confirm?.description}</p>
712
+ </Modal>
713
+ </>
714
+ )}
715
+ </div>
716
+ )
717
+ }