dsh-remote-vs 0.0.0-stage → 0.8.3

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 (200) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +385 -0
  3. package/README.md +347 -2
  4. package/cordis.patch.yml +259 -0
  5. package/icon.svg +7 -0
  6. package/lib/client.js +62602 -0
  7. package/lib/index.js +4287 -0
  8. package/lib/p2-bridge-refs.js +10 -0
  9. package/lib/p2-bridge.js +16 -0
  10. package/lib/p2-router-fs.js +252 -0
  11. package/lib/p2-router-refs.js +466 -0
  12. package/lib/p2-router-spill.js +62 -0
  13. package/lib/p2-router-subprocess.js +64 -0
  14. package/lib/p2-shell-bash.js +24 -0
  15. package/lib/ssh-proc-arI5lm4W.js +632 -0
  16. package/lib/types/changes/recorder.d.ts +240 -0
  17. package/lib/types/changes/status.d.ts +201 -0
  18. package/lib/types/changes/text-diff.d.ts +65 -0
  19. package/lib/types/client/CodeEditor.d.ts +125 -0
  20. package/lib/types/client/Dialog.d.ts +14 -0
  21. package/lib/types/client/DirectoryFlow.d.ts +30 -0
  22. package/lib/types/client/EditorPane.d.ts +30 -0
  23. package/lib/types/client/HostsView.d.ts +10 -0
  24. package/lib/types/client/NativeExplorer.d.ts +39 -0
  25. package/lib/types/client/RemoteDirectoryPicker.d.ts +16 -0
  26. package/lib/types/client/RemoteEditorView.d.ts +20 -0
  27. package/lib/types/client/RemoteExplorer.d.ts +18 -0
  28. package/lib/types/client/RemoteRoot.d.ts +16 -0
  29. package/lib/types/client/SearchView.d.ts +14 -0
  30. package/lib/types/client/SettingsHosts.d.ts +15 -0
  31. package/lib/types/client/TerminalView.d.ts +16 -0
  32. package/lib/types/client/WorkspaceWizard.d.ts +16 -0
  33. package/lib/types/client/WorkspacesView.d.ts +14 -0
  34. package/lib/types/client/XTerm.d.ts +15 -0
  35. package/lib/types/client/api.d.ts +162 -0
  36. package/lib/types/client/changes/ChangesDock.d.ts +35 -0
  37. package/lib/types/client/changes/model.d.ts +36 -0
  38. package/lib/types/client/changes/undo-affordance.d.ts +48 -0
  39. package/lib/types/client/changes/undo-context.d.ts +25 -0
  40. package/lib/types/client/composer.d.ts +165 -0
  41. package/lib/types/client/i18n.d.ts +17 -0
  42. package/lib/types/client/index.d.ts +14 -0
  43. package/lib/types/client/remote-file-open-target.d.ts +247 -0
  44. package/lib/types/client/remote-file-open.d.ts +35 -0
  45. package/lib/types/client/reveal.d.ts +15 -0
  46. package/lib/types/client/scm/DiffView.d.ts +24 -0
  47. package/lib/types/client/scm/HistoryPanel.d.ts +12 -0
  48. package/lib/types/client/scm/RepoPicker.d.ts +24 -0
  49. package/lib/types/client/scm/ScmPanel.d.ts +15 -0
  50. package/lib/types/client/scm/actions.d.ts +170 -0
  51. package/lib/types/client/scm/diff-model.d.ts +29 -0
  52. package/lib/types/client/scm/stage-queue.d.ts +99 -0
  53. package/lib/types/client/scm/status-map.d.ts +157 -0
  54. package/lib/types/client/scm/untracked-dirs.d.ts +72 -0
  55. package/lib/types/client/selection-popup.d.ts +61 -0
  56. package/lib/types/client/store.d.ts +437 -0
  57. package/lib/types/client/theme.d.ts +15 -0
  58. package/lib/types/client/ui.d.ts +46 -0
  59. package/lib/types/client/upload-plan.d.ts +78 -0
  60. package/lib/types/client/upload-source.d.ts +46 -0
  61. package/lib/types/client/use-remote-chat-target.d.ts +17 -0
  62. package/lib/types/client/vsc/FindWidget.d.ts +81 -0
  63. package/lib/types/client/vsc/change-marks.d.ts +150 -0
  64. package/lib/types/client/vsc/editor-langs.d.ts +5 -0
  65. package/lib/types/client/vsc/editor-palette.d.ts +37 -0
  66. package/lib/types/client/vsc/editor-theme.d.ts +32 -0
  67. package/lib/types/client/vsc/fallback-highlight.d.ts +26 -0
  68. package/lib/types/client/vsc/kit.d.ts +195 -0
  69. package/lib/types/client/vsc/markdown.d.ts +11 -0
  70. package/lib/types/client/vsc/reveal-line.d.ts +38 -0
  71. package/lib/types/client/vsc/tokenize.d.ts +104 -0
  72. package/lib/types/deploy.d.ts +94 -0
  73. package/lib/types/fs-ops.d.ts +8 -0
  74. package/lib/types/git-format.d.ts +34 -0
  75. package/lib/types/git-ops.d.ts +183 -0
  76. package/lib/types/http.d.ts +14 -0
  77. package/lib/types/importers.d.ts +20 -0
  78. package/lib/types/index.d.ts +87 -0
  79. package/lib/types/keygen.d.ts +19 -0
  80. package/lib/types/loopback.d.ts +7 -0
  81. package/lib/types/p2/bridge-refs.d.ts +21 -0
  82. package/lib/types/p2/bridge.d.ts +24 -0
  83. package/lib/types/p2/file-refs.d.ts +73 -0
  84. package/lib/types/p2/helper-rpc.d.ts +63 -0
  85. package/lib/types/p2/in-flight-gate.d.ts +25 -0
  86. package/lib/types/p2/router-fs.d.ts +92 -0
  87. package/lib/types/p2/router-refs.d.ts +45 -0
  88. package/lib/types/p2/router-spill.d.ts +11 -0
  89. package/lib/types/p2/router-subprocess.d.ts +23 -0
  90. package/lib/types/p2/shell-bash.d.ts +16 -0
  91. package/lib/types/p2/ssh-proc.d.ts +22 -0
  92. package/lib/types/p2/worlds.d.ts +179 -0
  93. package/lib/types/paths.d.ts +12 -0
  94. package/lib/types/pool.d.ts +105 -0
  95. package/lib/types/protocol.d.ts +452 -0
  96. package/lib/types/routes.d.ts +27 -0
  97. package/lib/types/search-ops.d.ts +68 -0
  98. package/lib/types/sftp.d.ts +90 -0
  99. package/lib/types/store.d.ts +66 -0
  100. package/lib/types/terminal.d.ts +51 -0
  101. package/lib/types/timeouts.d.ts +83 -0
  102. package/lib/types/tools.d.ts +41 -0
  103. package/lib/worlds-PUumRIh7.js +18765 -0
  104. package/locale/en.json +6 -0
  105. package/locale/zh.json +6 -0
  106. package/package.json +189 -4
  107. package/src/changes/recorder.ts +902 -0
  108. package/src/changes/status.ts +448 -0
  109. package/src/changes/text-diff.ts +377 -0
  110. package/src/client/CodeEditor.tsx +923 -0
  111. package/src/client/Dialog.tsx +76 -0
  112. package/src/client/DirectoryFlow.tsx +431 -0
  113. package/src/client/EditorPane.tsx +512 -0
  114. package/src/client/HostsView.tsx +312 -0
  115. package/src/client/NativeExplorer.tsx +121 -0
  116. package/src/client/RemoteDirectoryPicker.tsx +157 -0
  117. package/src/client/RemoteEditorView.tsx +54 -0
  118. package/src/client/RemoteExplorer.tsx +1398 -0
  119. package/src/client/RemoteRoot.tsx +389 -0
  120. package/src/client/SearchView.tsx +407 -0
  121. package/src/client/SettingsHosts.tsx +136 -0
  122. package/src/client/TerminalView.tsx +308 -0
  123. package/src/client/WorkspaceWizard.tsx +199 -0
  124. package/src/client/WorkspacesView.tsx +93 -0
  125. package/src/client/XTerm.tsx +308 -0
  126. package/src/client/api.ts +331 -0
  127. package/src/client/changes/ChangesDock.tsx +348 -0
  128. package/src/client/changes/changes.css +230 -0
  129. package/src/client/changes/model.ts +347 -0
  130. package/src/client/changes/undo-affordance.ts +389 -0
  131. package/src/client/changes/undo-context.ts +34 -0
  132. package/src/client/composer.ts +436 -0
  133. package/src/client/css.d.ts +7 -0
  134. package/src/client/i18n.ts +702 -0
  135. package/src/client/index.tsx +286 -0
  136. package/src/client/remote-file-open-target.ts +553 -0
  137. package/src/client/remote-file-open.ts +261 -0
  138. package/src/client/reveal.ts +33 -0
  139. package/src/client/scm/DiffView.tsx +570 -0
  140. package/src/client/scm/HistoryPanel.tsx +353 -0
  141. package/src/client/scm/RepoPicker.tsx +238 -0
  142. package/src/client/scm/ScmPanel.tsx +857 -0
  143. package/src/client/scm/actions.ts +460 -0
  144. package/src/client/scm/diff-model.ts +206 -0
  145. package/src/client/scm/stage-queue.ts +264 -0
  146. package/src/client/scm/status-map.ts +381 -0
  147. package/src/client/scm/untracked-dirs.ts +139 -0
  148. package/src/client/selection-popup.ts +157 -0
  149. package/src/client/store.tsx +1670 -0
  150. package/src/client/theme.ts +54 -0
  151. package/src/client/ui.tsx +136 -0
  152. package/src/client/upload-plan.ts +153 -0
  153. package/src/client/upload-source.ts +145 -0
  154. package/src/client/use-remote-chat-target.ts +153 -0
  155. package/src/client/vsc/FindWidget.tsx +446 -0
  156. package/src/client/vsc/change-marks.ts +586 -0
  157. package/src/client/vsc/editor-langs.ts +137 -0
  158. package/src/client/vsc/editor-palette.ts +107 -0
  159. package/src/client/vsc/editor-theme.ts +93 -0
  160. package/src/client/vsc/fallback-highlight.ts +145 -0
  161. package/src/client/vsc/kit.tsx +777 -0
  162. package/src/client/vsc/markdown.tsx +172 -0
  163. package/src/client/vsc/reveal-line.ts +142 -0
  164. package/src/client/vsc/tokenize.ts +569 -0
  165. package/src/client/vsc/vsc.css +2669 -0
  166. package/src/deploy.ts +481 -0
  167. package/src/fs-ops.ts +57 -0
  168. package/src/git-format.ts +205 -0
  169. package/src/git-ops.ts +693 -0
  170. package/src/http.ts +55 -0
  171. package/src/importers.ts +141 -0
  172. package/src/index.ts +442 -0
  173. package/src/keygen.ts +77 -0
  174. package/src/loopback.ts +47 -0
  175. package/src/p2/bridge-refs.ts +27 -0
  176. package/src/p2/bridge.ts +32 -0
  177. package/src/p2/file-refs.ts +392 -0
  178. package/src/p2/helper-rpc.ts +394 -0
  179. package/src/p2/in-flight-gate.ts +63 -0
  180. package/src/p2/router-fs.ts +327 -0
  181. package/src/p2/router-refs.ts +166 -0
  182. package/src/p2/router-spill.ts +75 -0
  183. package/src/p2/router-subprocess.ts +92 -0
  184. package/src/p2/shell-bash.ts +30 -0
  185. package/src/p2/ssh-proc.ts +802 -0
  186. package/src/p2/worlds.ts +735 -0
  187. package/src/paths.ts +50 -0
  188. package/src/pool.ts +811 -0
  189. package/src/protocol.ts +460 -0
  190. package/src/routes.ts +802 -0
  191. package/src/search-ops.ts +220 -0
  192. package/src/sftp.ts +677 -0
  193. package/src/store.ts +364 -0
  194. package/src/terminal.ts +291 -0
  195. package/src/timeouts.ts +122 -0
  196. package/src/tools.ts +320 -0
  197. package/vendor/dsh-remote-helpers/dsh-ssh-helper-0.2.1-alpha.2-linux-x64.helper.json +16 -0
  198. package/vendor/dsh-remote-helpers/dsh-ssh-helper-0.2.1-alpha.2-linux-x64.tar.gz +4 -0
  199. package/vendor/dsh-remote-helpers/dsh-ssh-helper-0.2.1-alpha.2-linux-x64.tar.gz.sha256 +1 -0
  200. package/vendor/dsh-remote-helpers/glibc-ceiling.patch +31 -0
@@ -0,0 +1,348 @@
1
+ /**
2
+ * The session change panel above the composer (the Cursor-style review list).
3
+ *
4
+ * Placement is the point: `conversation.input.dock` is the one full-width seat
5
+ * the shell renders directly above the composer card — the todo, goal and queue
6
+ * panels already live there — so the list is visible while the user types the
7
+ * next instruction instead of being scrolled away in the conversation.
8
+ *
9
+ * Data comes from the plugin's own ledger (`/api/dsh-remote/changes`), not from
10
+ * DSH's `workspaceChanges`: that recorder cannot see a routed session at all
11
+ * (its git baseline is parsed with host path semantics and its scratch index is
12
+ * written with `node:fs`), which the probe measured as 58 remote sessions with
13
+ * zero change events.
14
+ *
15
+ * Clicking a row opens the file in the remote editor and hands the editor the
16
+ * same hunks, so the changed lines are painted in place.
17
+ */
18
+
19
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
20
+ import { api } from '../api.ts'
21
+ import {
22
+ applyChangeSet,
23
+ bootstrap,
24
+ followSessionWorkspace,
25
+ openRemoteFileInBinding,
26
+ refreshAll,
27
+ remoteOpenBindings,
28
+ sessionRepoSubdir,
29
+ setFollowSession,
30
+ useExplorer,
31
+ workspaceMatchesSession,
32
+ } from '../store.tsx'
33
+ import { bindingForChangedFile } from '../remote-file-open-target.ts'
34
+ import { useRemoteChatTarget } from '../use-remote-chat-target.ts'
35
+ import type { RemoteChangedFile, RemoteChangesSummary } from '../../protocol.ts'
36
+ import { EMPTY_CHANGE_SET, changeSetFromHunks, type ChangeSet } from './model.ts'
37
+ import { setUndoContext } from './undo-context.ts'
38
+ import { FileIcon } from '../vsc/kit.tsx'
39
+ import './changes.css'
40
+
41
+ /** How often an open panel re-reads the ledger (the host refreshes it per turn). */
42
+ const POLL_MS = 3000
43
+
44
+ /**
45
+ * How many polls between two follow retries (5 × 3 s = one probe every 15 s).
46
+ *
47
+ * The follow is one cheap HTTP call and it is retried until it resolves, so it
48
+ * must NOT ride the ledger's 3 s cadence: a session whose workspace cannot be
49
+ * resolved would otherwise cost a probe per poll, forever.
50
+ */
51
+ const FOLLOW_RETRY_TICKS = 5
52
+
53
+ /**
54
+ * Props the composer dock hands us.
55
+ *
56
+ * Typed narrowly on purpose: the slot's full owner/standard prop surface comes
57
+ * from `dsh-client-ui-conversation`, and depending on that package for types
58
+ * drags a second DSH chain into this repo (same reason `conversation.view` is
59
+ * soft-typed in index.tsx). Everything else is read from the plugin's own store.
60
+ */
61
+ export interface ChangesDockProps {
62
+ /** `SessionSnapshot` (the client contract names it `sessionId`, not `id`). */
63
+ session?: { sessionId?: string; id?: string }
64
+ sessionId?: string
65
+ }
66
+
67
+ /** One row's status, spelled out for the tooltip (the row itself shows counts). */
68
+ function statusWord(file: RemoteChangedFile, t: (key: string) => string): string {
69
+ if (file.status === 'A') return t('changes.statusAdded')
70
+ if (file.status === 'D') return t('changes.statusDeleted')
71
+ if (file.status === 'R') return t('changes.statusRenamed')
72
+ return t('changes.statusModified')
73
+ }
74
+
75
+ /** Split a display path into its directory prefix and file name. */
76
+ function splitDisplay(display: string): { dir: string; name: string } {
77
+ const at = display.lastIndexOf('/')
78
+ return at < 0 ? { dir: '', name: display } : { dir: display.slice(0, at + 1), name: display.slice(at + 1) }
79
+ }
80
+
81
+ export function ChangesDock(props: ChangesDockProps): React.ReactNode {
82
+ const explorer = useExplorer()
83
+ // The dock is a second surface of the same store as the explorer, and it can
84
+ // mount without the explorer ever being opened — without this the workspace
85
+ // list would be empty and a click could not resolve its binding.
86
+ useEffect(() => {
87
+ if (explorer.ctx !== undefined) bootstrap(explorer.ctx)
88
+ }, [explorer.ctx])
89
+ const t = explorer.t
90
+ const sessionId = props.sessionId ?? props.session?.sessionId ?? props.session?.id
91
+ /**
92
+ * The last answer, TAGGED with the session it belongs to.
93
+ *
94
+ * One panel serves whichever session is on screen, so an untagged summary
95
+ * survives a session switch and keeps describing the previous conversation —
96
+ * which is exactly how a stale "computing…" from another session appeared in
97
+ * a session whose own ledger had not even been created yet.
98
+ */
99
+ const [data, setData] = useState<{ id: string; summary: RemoteChangesSummary | null } | null>(null)
100
+ const [error, setError] = useState<string | undefined>(undefined)
101
+ // Collapsed by default: the header already carries the file count and the totals,
102
+ // and the list is one click away — a turn that touched twenty files should not
103
+ // push the composer off the screen.
104
+ const [collapsed, setCollapsed] = useState(true)
105
+ const [busy, setBusy] = useState<string | undefined>(undefined)
106
+ const alive = useRef(true)
107
+ const currentSession = useRef(sessionId)
108
+ currentSession.current = sessionId
109
+ // The Source Control panel's repository directory, resolved at call time: it is
110
+ // the user's explicit answer to "which repository in this workspace", and it
111
+ // outranks the host's one-level guess. A ref (not a value) so a change made
112
+ // while this panel is open is picked up by the next poll.
113
+ //
114
+ // It is stored PER BINDING, so it may only be sent while the sidebar's
115
+ // selection can be PROVEN to be THIS session's binding (see the store's
116
+ // `sessionRepoSubdir`). Otherwise the host is told to `cd` into
117
+ // `session root + another workspace's sub-directory`, answers `session
118
+ // directory is missing on the host` (exit 90), the panel never gets a summary
119
+ // and repeats the same wrong guess on the next poll — the reported loop. `''`
120
+ // is always a truthful instruction (session root + the host's own
121
+ // unique-candidate probe); a foreign directory never is.
122
+ const repoSubdirOf = (): string => sessionRepoSubdir(sessionId)
123
+
124
+ const load = useCallback(async (force = false): Promise<void> => {
125
+ if (sessionId === undefined || sessionId === '') return
126
+ const asked = sessionId
127
+ try {
128
+ const next = await api.changes.list(asked, force, repoSubdirOf())
129
+ // Two guards: the panel may be gone, and the user may have switched
130
+ // sessions while this was in flight — a late answer for the old session
131
+ // must not be filed under the new one.
132
+ if (!alive.current || currentSession.current !== asked) return
133
+ setData({ id: asked, summary: next })
134
+ setError(undefined)
135
+ } catch (problem) {
136
+ if (!alive.current || currentSession.current !== asked) return
137
+ setError(problem instanceof Error ? problem.message : String(problem))
138
+ }
139
+ }, [sessionId])
140
+ const summary = data !== null && data.id === sessionId ? data.summary : null
141
+
142
+ useEffect(() => {
143
+ alive.current = true
144
+ void load()
145
+ const timer = window.setInterval(() => { void load() }, POLL_MS)
146
+ return () => {
147
+ alive.current = false
148
+ window.clearInterval(timer)
149
+ }
150
+ }, [load])
151
+
152
+ // Keep the revert affordance pointed at the workspace this panel lists. It is
153
+ // written on every summary so a rebound workspace cannot leave a stale root
154
+ // behind, and cleared on unmount so an affordance in another session's editor
155
+ // can never act on this one.
156
+ useEffect(() => {
157
+ if (sessionId === undefined || summary === null || summary.root === '') return
158
+ setUndoContext({ sessionId, alias: summary.alias, root: summary.root })
159
+ }, [sessionId, summary])
160
+
161
+ useEffect(() => () => { setUndoContext(undefined) }, [])
162
+
163
+ /**
164
+ * Register this session and start the follow — with no help from the sidebar.
165
+ *
166
+ * The follow used to be driven only by surfaces that already HELD evidence
167
+ * (the sidebar's workspace membership, this panel's own summary). Before the
168
+ * panel's FIRST successful scan neither exists, so the session's workspace
169
+ * stayed unknown while the panel still had to name a repository directory:
170
+ * it sent the selected workspace's sub-directory, the host answered exit 90,
171
+ * no summary arrived, and the next poll repeated it. The store now resolves
172
+ * the session's own cwd (available from the first paint) and the panel
173
+ * retries on a SLOW timer, so a resolution that needs the workspace table to
174
+ * land is picked up without a probe per poll.
175
+ */
176
+ useEffect(() => {
177
+ setFollowSession(sessionId)
178
+ if (sessionId === undefined || sessionId === '') return
179
+ void followSessionWorkspace({ sessionId })
180
+ let ticks = 0
181
+ const timer = window.setInterval(() => {
182
+ ticks += 1
183
+ if (ticks % FOLLOW_RETRY_TICKS !== 0) return
184
+ // Resolved — or the user picked a workspace by hand, which stands: stop
185
+ // probing entirely rather than re-asking a settled question.
186
+ if (workspaceMatchesSession()) {
187
+ window.clearInterval(timer)
188
+ return
189
+ }
190
+ void followSessionWorkspace({ sessionId })
191
+ }, POLL_MS)
192
+ return () => { window.clearInterval(timer) }
193
+ }, [sessionId])
194
+
195
+ // The binding table arrives asynchronously (the bootstrap above), and a follow
196
+ // attempted before it landed resolves nothing. Retry the moment it does,
197
+ // instead of waiting out the slow timer.
198
+ useEffect(() => {
199
+ if (sessionId === undefined || sessionId === '') return
200
+ void followSessionWorkspace({ sessionId })
201
+ }, [sessionId, explorer.ready, explorer.workspaces, explorer.aliasPaths])
202
+
203
+ /**
204
+ * Follow the session's workspace from the panel's own evidence.
205
+ *
206
+ * The panel holds the host's alias + remote root for THIS session, so it can
207
+ * point the sidebar (and the editor's workspace selector, which reads the same
208
+ * store) at the right binding even when the cwd probe found nothing — and it
209
+ * is the surface that is mounted whenever a conversation is. Only the session
210
+ * actually on screen may steer the selector, and only once per session (the
211
+ * store's policy), so a parked dock cannot move it.
212
+ */
213
+ const chatTarget = useRemoteChatTarget(explorer.ctx)
214
+ useEffect(() => {
215
+ if (sessionId === undefined || summary === null || summary.root === '') return
216
+ if (chatTarget.sessionId === undefined || chatTarget.sessionId !== sessionId) return
217
+ void followSessionWorkspace({ sessionId, alias: summary.alias, remoteRoot: summary.root })
218
+ }, [sessionId, summary, chatTarget.sessionId])
219
+
220
+ /** Open one listed file in the remote editor with its change marks. */
221
+ const openChanged = useCallback(async (file: RemoteChangedFile): Promise<void> => {
222
+ if (sessionId === undefined || summary === null) return
223
+ setBusy(file.path)
224
+ try {
225
+ const diff = await api.changes.diff(sessionId, file.path, repoSubdirOf())
226
+ if (!alive.current) return
227
+ const stamp = `${file.path}@${summary.at}@${summary.turn}`
228
+ const set: ChangeSet = diff !== null && diff.kind === 'text'
229
+ ? changeSetFromHunks(diff.hunks, stamp)
230
+ : { ...EMPTY_CHANGE_SET, stamp }
231
+ // The store may still be loading its workspace table on a cold page, so a
232
+ // miss is worth one refresh before giving up on the click. Read through
233
+ // the snapshot accessor, not the render-time props, so the retry actually
234
+ // sees what the load wrote.
235
+ //
236
+ // Resolved by the session's REMOTE root, never by the alias alone: one
237
+ // alias carries several bindings (P710 and Z700 both live on
238
+ // `ssh_xiray_new`), and the alias-only form picked whichever came first —
239
+ // the file opened while the tree and the selector stayed on the other
240
+ // workspace. That was the reported bug.
241
+ let binding = bindingForChangedFile(remoteOpenBindings(), summary, file.path)
242
+ if (binding === undefined) {
243
+ refreshAll()
244
+ await new Promise(resolve => { window.setTimeout(resolve, 300) })
245
+ binding = bindingForChangedFile(remoteOpenBindings(), summary, file.path)
246
+ }
247
+ if (binding === undefined) {
248
+ explorer.info(t('changes.noWorkspace'))
249
+ return
250
+ }
251
+ openRemoteFileInBinding(binding.id, file.path, set.hunks[0] === undefined ? {} : { line: set.hunks[0] })
252
+ // The tab id is deterministic (store.openFile builds it the same way), so
253
+ // the marks can be attached without waiting for the file read to settle.
254
+ applyChangeSet(`file:${binding.alias}:${file.path}`, set)
255
+ } catch (problem) {
256
+ if (alive.current) explorer.info(`${t('changes.diffFailed')}: ${problem instanceof Error ? problem.message : String(problem)}`)
257
+ } finally {
258
+ if (alive.current) setBusy(undefined)
259
+ }
260
+ }, [explorer, sessionId, summary, t])
261
+
262
+ const files = summary?.files ?? []
263
+ const activeTab = explorer.tabs.find(candidate => candidate.id === explorer.activeTabId)
264
+ const visible = useMemo(() => {
265
+ if (summary === null) return false
266
+ return files.length > 0 || summary.pending === true || summary.unavailable !== undefined
267
+ }, [files.length, summary])
268
+
269
+ if (sessionId === undefined || sessionId === '') return null
270
+ if (!visible && error === undefined) return null
271
+
272
+ const stale = activeTab?.changeStale === true
273
+ const title = files.length > 0 ? `${t('changes.title')} · ${files.length}` : t('changes.title')
274
+
275
+ return (
276
+ <div className="rvs-changes" data-collapsed={collapsed ? '1' : '0'}>
277
+ <div className="rvs-changes-head">
278
+ <button
279
+ type="button"
280
+ className="rvs-changes-toggle"
281
+ aria-expanded={!collapsed}
282
+ onClick={() => { setCollapsed(value => !value) }}
283
+ title={t('changes.toggle')}
284
+ >
285
+ <span className="rvs-changes-chevron" aria-hidden="true">{collapsed ? '▸' : '▾'}</span>
286
+ <span className="rvs-changes-title">{title}</span>
287
+ </button>
288
+ {files.length > 0 && (
289
+ <span className="rvs-changes-totals">
290
+ <span className="rvs-chg-add">+{summary?.added ?? 0}</span>
291
+ <span className="rvs-chg-del">−{summary?.deleted ?? 0}</span>
292
+ </span>
293
+ )}
294
+ {summary?.pending === true && <span className="rvs-changes-pending">{t('changes.pending')}</span>}
295
+ <span className="rvs-changes-spacer" />
296
+ {stale && <span className="rvs-changes-stale" title={t('changes.staleHint')}>{t('changes.stale')}</span>}
297
+ <button
298
+ type="button"
299
+ className="rvs-changes-refresh"
300
+ onClick={() => { void load(true) }}
301
+ title={t('changes.refresh')}
302
+ >⟳</button>
303
+ </div>
304
+ {!collapsed && (
305
+ <div className="rvs-changes-body">
306
+ {error !== undefined && <div className="rvs-changes-note rvs-changes-error">{t('changes.failed')}: {error}</div>}
307
+ {summary?.unavailable !== undefined && (
308
+ <div className="rvs-changes-note">{t('changes.unavailable')} · {summary.unavailable}</div>
309
+ )}
310
+ {files.length === 0 && summary?.unavailable === undefined && summary?.pending !== true && (
311
+ <div className="rvs-changes-note">{t('changes.empty')}</div>
312
+ )}
313
+ {files.map(file => {
314
+ const { dir, name } = splitDisplay(file.display)
315
+ return (
316
+ <button
317
+ type="button"
318
+ key={file.path}
319
+ className={`rvs-chg-row${busy === file.path ? ' rvs-chg-loading' : ''}`}
320
+ title={`${file.path}\n${statusWord(file, t)}`}
321
+ onClick={() => { void openChanged(file) }}
322
+ >
323
+ <span className="rvs-chg-icon" aria-hidden="true"><FileIcon path={file.display} size={14} /></span>
324
+ <span className="rvs-chg-path">
325
+ <span className="rvs-chg-dir">{dir}</span>
326
+ <span className="rvs-chg-name">{name}</span>
327
+ </span>
328
+ <span className="rvs-chg-counts">
329
+ {file.oversized === true
330
+ ? <span className="rvs-chg-note">{t('changes.oversized')}</span>
331
+ : file.binary === true
332
+ ? <span className="rvs-chg-note">{t('changes.binary')}</span>
333
+ : (
334
+ <>
335
+ <span className="rvs-chg-add">+{file.added}</span>
336
+ <span className="rvs-chg-del">−{file.deleted}</span>
337
+ </>
338
+ )}
339
+ </span>
340
+ </button>
341
+ )
342
+ })}
343
+ {summary?.truncated === true && <div className="rvs-changes-note">{t('changes.truncated')}</div>}
344
+ </div>
345
+ )}
346
+ </div>
347
+ )
348
+ }
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Session change panel (above the composer).
3
+ *
4
+ * Scoped under `.rvs-changes` — this surface renders OUTSIDE `.rvs-root` (it is
5
+ * a composer dock, not part of the explorer), so it cannot borrow that block's
6
+ * custom properties and states every colour with a shell token plus a literal
7
+ * fallback. Light theme is the `body:not([data-ds-dark-theme])` override, the
8
+ * same convention the rest of this plugin's stylesheet uses.
9
+ */
10
+
11
+ .rvs-changes {
12
+ --rvs-chg-fg: var(--dsw-alias-label-primary, #e6e6e6);
13
+ --rvs-chg-fg2: var(--dsw-alias-label-secondary, #b9b9b9);
14
+ --rvs-chg-fg3: var(--dsw-alias-label-tertiary, #8b8b8b);
15
+ --rvs-chg-border: var(--dsw-alias-border-l1, rgba(127, 127, 127, .28));
16
+ --rvs-chg-hover: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, .14));
17
+ --rvs-chg-surface: var(--dsw-alias-bg-layer-2, rgba(127, 127, 127, .07));
18
+ --rvs-chg-add: #4ec9b0;
19
+ --rvs-chg-del: #f14c4c;
20
+ --rvs-chg-mod: #e2b714;
21
+ --rvs-chg-info: #58a6ff;
22
+
23
+ box-sizing: border-box;
24
+ /*
25
+ * 与输入卡左右对齐。
26
+ *
27
+ * 这个面板是 composerStack 的直接子元素,而输入卡住在自己的包装层里
28
+ * (`.uV2eYG_root`:左右各 `--dsh-composer-side-clearance`),卡片本身
29
+ * `width:100%; max-width: var(--dsh-composer-card-max-width)` 且居中。所以这里
30
+ * 必须自己把那圈内缩和上限抄一遍,否则就是「面板通栏、输入卡内缩」。
31
+ * 两个变量都由 shell 定义在 `.wSkVaW_body` 上(嵌入态有另一组值),回退值只
32
+ * 兜住宿主没有这套变量的老版本。
33
+ */
34
+ width: calc(100% - 2 * var(--dsh-composer-side-clearance, 16px));
35
+ max-width: var(--dsh-composer-card-max-width, 920px);
36
+ /* composerStack 本身已有 gap,这里不要再叠下边距。 */
37
+ margin: 0 auto;
38
+ border: 1px solid var(--rvs-chg-border);
39
+ border-radius: 8px;
40
+ background-color: var(--rvs-chg-surface);
41
+ color: var(--rvs-chg-fg);
42
+ font-size: 12px;
43
+ line-height: 20px;
44
+ overflow: hidden;
45
+ }
46
+
47
+ .rvs-changes-head {
48
+ display: flex;
49
+ align-items: center;
50
+ gap: 8px;
51
+ min-height: 28px;
52
+ padding: 2px 6px 2px 4px;
53
+ }
54
+
55
+ .rvs-changes-toggle {
56
+ display: inline-flex;
57
+ align-items: center;
58
+ gap: 4px;
59
+ min-width: 0;
60
+ padding: 2px 4px;
61
+ border: none;
62
+ border-radius: 5px;
63
+ background: transparent;
64
+ color: inherit;
65
+ font: inherit;
66
+ cursor: pointer;
67
+ }
68
+
69
+ .rvs-changes-toggle:hover { background-color: var(--rvs-chg-hover); }
70
+
71
+ .rvs-changes-chevron {
72
+ width: 10px;
73
+ color: var(--rvs-chg-fg3);
74
+ font-size: 10px;
75
+ }
76
+
77
+ .rvs-changes-title {
78
+ overflow: hidden;
79
+ text-overflow: ellipsis;
80
+ white-space: nowrap;
81
+ }
82
+
83
+ .rvs-changes-totals,
84
+ .rvs-chg-counts {
85
+ display: inline-flex;
86
+ gap: 6px;
87
+ font-variant-numeric: tabular-nums;
88
+ font-size: 11px;
89
+ }
90
+
91
+ .rvs-chg-add { color: var(--rvs-chg-add); }
92
+ .rvs-chg-del { color: var(--rvs-chg-del); }
93
+
94
+ .rvs-changes-pending,
95
+ .rvs-changes-stale {
96
+ color: var(--rvs-chg-fg3);
97
+ font-size: 11px;
98
+ white-space: nowrap;
99
+ }
100
+
101
+ .rvs-changes-stale { color: var(--rvs-chg-mod); }
102
+
103
+ .rvs-changes-spacer { flex: 1 1 auto; }
104
+
105
+ .rvs-changes-nav {
106
+ display: inline-flex;
107
+ gap: 2px;
108
+ }
109
+
110
+ .rvs-changes-nav button,
111
+ .rvs-changes-refresh {
112
+ width: 20px;
113
+ height: 20px;
114
+ padding: 0;
115
+ border: none;
116
+ border-radius: 4px;
117
+ background: transparent;
118
+ color: var(--rvs-chg-fg2);
119
+ font-size: 12px;
120
+ line-height: 20px;
121
+ text-align: center;
122
+ cursor: pointer;
123
+ }
124
+
125
+ .rvs-changes-nav button:hover,
126
+ .rvs-changes-refresh:hover {
127
+ background-color: var(--rvs-chg-hover);
128
+ color: var(--rvs-chg-fg);
129
+ }
130
+
131
+ .rvs-changes-body {
132
+ display: flex;
133
+ flex-direction: column;
134
+ max-height: 216px;
135
+ overflow-y: auto;
136
+ padding: 0 4px 4px;
137
+ }
138
+
139
+ /*
140
+ * One file per row: type icon, path, counts at the far right.
141
+ *
142
+ * Every part is nowrap and the path is the only flexible one, so a long remote
143
+ * path ellipsizes in the middle of the row instead of pushing the counts out of
144
+ * view or wrapping the row into a paragraph (which is what a row of inline
145
+ * <button>s does without this rule).
146
+ */
147
+ .rvs-chg-row {
148
+ display: flex;
149
+ flex: 0 0 auto;
150
+ align-items: center;
151
+ gap: 6px;
152
+ width: 100%;
153
+ min-height: 24px;
154
+ padding: 1px 6px;
155
+ border: none;
156
+ border-radius: 5px;
157
+ background: transparent;
158
+ color: inherit;
159
+ font: inherit;
160
+ line-height: 18px;
161
+ text-align: left;
162
+ white-space: nowrap;
163
+ cursor: pointer;
164
+ }
165
+
166
+ .rvs-chg-row:hover { background-color: var(--rvs-chg-hover); }
167
+
168
+ .rvs-chg-row.rvs-chg-loading { opacity: .55; }
169
+
170
+ /* File-type icon: the host's icon theme when the shell provides one, else the
171
+ plugin's own SVG set (see vsc/kit.tsx FileIcon). */
172
+ .rvs-chg-icon {
173
+ display: inline-flex;
174
+ flex: 0 0 auto;
175
+ align-items: center;
176
+ justify-content: center;
177
+ width: 16px;
178
+ height: 16px;
179
+ }
180
+
181
+ .rvs-chg-path {
182
+ flex: 1 1 auto;
183
+ min-width: 0;
184
+ overflow: hidden;
185
+ text-overflow: ellipsis;
186
+ white-space: nowrap;
187
+ /* Paths are remote POSIX spellings: keep the leading slash where it belongs
188
+ even in a container whose direction the host skin may have changed. */
189
+ unicode-bidi: plaintext;
190
+ }
191
+
192
+ .rvs-chg-dir { color: var(--rvs-chg-fg3); }
193
+
194
+ .rvs-chg-name { color: var(--rvs-chg-fg); }
195
+
196
+ .rvs-chg-counts {
197
+ display: inline-flex;
198
+ flex: 0 0 auto;
199
+ gap: 8px;
200
+ margin-left: auto;
201
+ font-size: 11px;
202
+ font-variant-numeric: tabular-nums;
203
+ }
204
+
205
+ /* "binary" / "too large": the two cases where no line counts exist. */
206
+ .rvs-chg-note {
207
+ color: var(--rvs-chg-fg3);
208
+ font-size: 11px;
209
+ }
210
+
211
+ .rvs-changes-note {
212
+ padding: 4px 6px;
213
+ color: var(--rvs-chg-fg3);
214
+ font-size: 11px;
215
+ }
216
+
217
+ .rvs-changes-error { color: var(--rvs-chg-del); }
218
+
219
+ body:not([data-ds-dark-theme]) .rvs-changes {
220
+ --rvs-chg-fg: var(--dsw-alias-label-primary, #1f2328);
221
+ --rvs-chg-fg2: var(--dsw-alias-label-secondary, #4d5358);
222
+ --rvs-chg-fg3: var(--dsw-alias-label-tertiary, #6e7781);
223
+ --rvs-chg-border: var(--dsw-alias-border-l1, rgba(31, 35, 40, .16));
224
+ --rvs-chg-hover: var(--dsw-alias-interactive-bg-hover, rgba(31, 35, 40, .07));
225
+ --rvs-chg-surface: var(--dsw-alias-bg-layer-2, rgba(31, 35, 40, .04));
226
+ --rvs-chg-add: #1a7f37;
227
+ --rvs-chg-del: #cf222e;
228
+ --rvs-chg-mod: #9a6700;
229
+ --rvs-chg-info: #0969da;
230
+ }