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,261 @@
1
+ /**
2
+ * Click interception for file references that belong to a REMOTE workspace.
3
+ *
4
+ * DSH opens every file reference through `ctx.sidebarRight.openResource()`
5
+ * (chat `openFile`, deliverables, `@file`, the files tree — one entry point),
6
+ * and its preview reads the address path against the SESSION WORKSPACE ROOT.
7
+ * In a remote session that root is an EMPTY local alias directory, and a model
8
+ * usually quotes paths relative to the directory it actually worked in, so the
9
+ * click either misses by one segment or dies on the host ENOENT — the exact
10
+ * failure users hit on the final-answer file chips.
11
+ *
12
+ * This module wraps that one method: an address that resolves to one of our
13
+ * bindings is opened in OUR editor instead (centre 「远程编辑器」 tab, with
14
+ * the workspace selected); everything else — local sessions, non-file
15
+ * addresses, unresolved sessions — falls through to the original method
16
+ * synchronously, so the native behaviour is preserved untouched.
17
+ *
18
+ * The wrapper is deliberately fail-safe: resolution is synchronous first (the
19
+ * native path is taken whenever this is not clearly a remote click), and the
20
+ * async part (session-cwd probe + remote existence check) can only ever land
21
+ * the click in our editor or fall back to the native open. It never throws out
22
+ * of `openResource`, which callers invoke inside their own try/catch.
23
+ */
24
+ import type { Context } from '@deepseek-ai/cordis'
25
+ import { api } from './api.ts'
26
+ import {
27
+ parseFileAddress,
28
+ resolveRemoteOpenTarget,
29
+ suffixFindCommand,
30
+ uniqueSuffixMatch,
31
+ type ParsedFileAddress,
32
+ type RemoteOpenBinding,
33
+ } from './remote-file-open-target.ts'
34
+ import { isEditorCentered, openRemoteFileInBinding, remoteOpenBindings } from './store.tsx'
35
+ import { workspacePathOf } from './use-remote-chat-target.ts'
36
+
37
+ interface SidebarRightLike {
38
+ openResource?: (address: string, options?: unknown) => unknown
39
+ }
40
+
41
+ /** Milliseconds one optional round trip may cost before the click proceeds. */
42
+ const CWD_PROBE_MS = 1_500
43
+ /** Milliseconds one remote existence probe may cost. */
44
+ const EXIST_PROBE_MS = 4_000
45
+
46
+ /** Bound services: original method per sidebarRight instance (idempotent). */
47
+ const originals = new WeakMap<object, (address: string, options?: unknown) => unknown>()
48
+
49
+ function withDeadline<T>(promise: Promise<T>, ms: number): Promise<T | undefined> {
50
+ return new Promise<T | undefined>((resolve, reject) => {
51
+ const timer = setTimeout(() => resolve(undefined), ms)
52
+ timer.unref?.()
53
+ promise.then(
54
+ (value) => {
55
+ clearTimeout(timer)
56
+ resolve(value)
57
+ },
58
+ (error: unknown) => {
59
+ clearTimeout(timer)
60
+ reject(error)
61
+ },
62
+ )
63
+ })
64
+ }
65
+
66
+ /** The `line` a caller asked to reveal, from either option spelling. */
67
+ function lineOf(options: unknown): number | undefined {
68
+ if (typeof options !== 'object' || options === null) return undefined
69
+ const record = options as { line?: unknown; params?: { line?: unknown } }
70
+ const value = record.line ?? record.params?.line
71
+ return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : undefined
72
+ }
73
+
74
+ /**
75
+ * Probe candidates in order and return the first one that exists.
76
+ *
77
+ * Every click pays at most one remote stat when the first candidate is right;
78
+ * the probe is skipped entirely only when the binding has a single candidate
79
+ * AND that candidate is the workspace-root spelling (the common, healthy
80
+ * case) — see the caller for the fallback search when every probe misses.
81
+ *
82
+ * @returns the chosen remote path, or `undefined` when none exists.
83
+ */
84
+ async function firstExisting(
85
+ alias: string,
86
+ candidates: readonly string[],
87
+ ): Promise<string | undefined> {
88
+ for (const candidate of candidates) {
89
+ const exists = await withDeadline(api.fileOp.stat(alias, candidate), EXIST_PROBE_MS).then(
90
+ () => true,
91
+ () => false,
92
+ )
93
+ if (exists === true) return candidate
94
+ }
95
+ return undefined
96
+ }
97
+
98
+ /**
99
+ * Last-resort lookup for a clicked PROJECT-relative path (the model `cd`ed
100
+ * inside individual shell commands, so the session cwd never moved): search
101
+ * the binding's remote tree for a unique file whose path ends with the
102
+ * clicked spelling. Only issued after every direct candidate missed.
103
+ */
104
+ async function searchSuffix(binding: RemoteOpenBinding, relative: string): Promise<string | undefined> {
105
+ const command = suffixFindCommand(binding.remotePath, relative)
106
+ if (command === undefined) return undefined
107
+ const result = await withDeadline(api.exec(binding.alias, command, 6_000), 7_000).catch(() => undefined)
108
+ if (result === undefined) return undefined
109
+ return uniqueSuffixMatch(binding.remotePath, relative, result.stdout)
110
+ }
111
+
112
+ /** Open one resolved click; returns false when it must stay native. */
113
+ function interceptSync(ctx: Context, parsed: ParsedFileAddress, address: string, options: unknown): boolean {
114
+ const table = remoteOpenBindings()
115
+ if (table.bindings.length === 0) return false
116
+ const line = lineOf(options)
117
+
118
+ if (parsed.scope === 'absolute') {
119
+ const target = resolveRemoteOpenTarget({ address, table })
120
+ if (target === undefined) return false
121
+ return openRemoteFileInBinding(target.binding.id, target.candidates[0], line === undefined ? {} : { line })
122
+ }
123
+
124
+ const sessionWorkspacePath = workspacePathOf(ctx, parsed.sessionId)
125
+ if (sessionWorkspacePath === undefined) return false
126
+ const baseTarget = resolveRemoteOpenTarget({ address, table, sessionWorkspacePath })
127
+ if (baseTarget === undefined) return false
128
+
129
+ // The spelling the model used, normalized for the fallback search (only a
130
+ // plain relative path can be searched for by suffix).
131
+ const rawPath = parsed.path.replace(/\\/g, '/')
132
+ const relative = rawPath.startsWith('/') || /^[A-Za-z]:\//.test(rawPath) || rawPath.startsWith('//')
133
+ ? undefined
134
+ : rawPath.replace(/^(?:\.\/)+/, '')
135
+
136
+ // Fire-and-forget: the session-cwd probe only ever ADDS a candidate.
137
+ void (async () => {
138
+ let target = baseTarget
139
+ try {
140
+ const probe = await withDeadline(api.sessionCwd(parsed.sessionId), CWD_PROBE_MS)
141
+ const cwd = probe?.cwd
142
+ if (typeof cwd === 'string' && cwd !== '') {
143
+ target = resolveRemoteOpenTarget({ address, table, sessionWorkspacePath, sessionCwd: cwd }) ?? baseTarget
144
+ }
145
+ } catch {
146
+ // Keep the workspace-root target.
147
+ }
148
+ const open = (remotePath: string): void => {
149
+ openRemoteFileInBinding(target.binding.id, remotePath, line === undefined ? {} : { line })
150
+ }
151
+ const existing = await firstExisting(target.binding.alias, target.candidates)
152
+ if (existing !== undefined) {
153
+ open(existing)
154
+ return
155
+ }
156
+ // Nothing at the direct spellings: the model may have quoted a
157
+ // project-relative path (its `cd` lived inside shell commands only).
158
+ if (relative !== undefined) {
159
+ const found = await searchSuffix(target.binding, relative)
160
+ if (found !== undefined) {
161
+ open(found)
162
+ return
163
+ }
164
+ }
165
+ // Still nothing: open the primary spelling so the editor shows the remote
166
+ // read error (named against the remote workspace, not the empty alias).
167
+ open(target.candidates[0])
168
+ })().catch((error: unknown) => {
169
+ // The editor open itself failed: keep the click a no-op rather than
170
+ // surfacing an exception inside another plugin's service call, but say so
171
+ // in the console (`[dsh-remote-vs]`) so a field report is diagnosable.
172
+ console.warn('[dsh-remote-vs] remote file click could not open the remote editor:', error)
173
+ })
174
+ return true
175
+ }
176
+
177
+ /**
178
+ * Install the click interceptor on the shared `sidebarRight` service.
179
+ *
180
+ * The client plugin's `inject` list is prefetch-only, so the service may not
181
+ * exist when ours applies: poll briefly (same pattern as the explorer surface)
182
+ * and stop on dispose. A host without `sidebarRight` simply never matches.
183
+ *
184
+ * @param ctx - client root context.
185
+ * @returns disposer restoring the original method.
186
+ */
187
+ export function installRemoteFileOpen(ctx: Context): () => void {
188
+ let disposed = false
189
+ let timer: ReturnType<typeof setInterval> | undefined
190
+
191
+ const stopTimer = (): void => {
192
+ if (timer !== undefined) {
193
+ clearInterval(timer)
194
+ timer = undefined
195
+ }
196
+ }
197
+
198
+ const attempt = (): boolean => {
199
+ if (disposed) return true
200
+ let service: object | undefined
201
+ try {
202
+ service = (ctx as unknown as { get(name: string): unknown }).get('sidebarRight') as object | undefined
203
+ } catch {
204
+ return false
205
+ }
206
+ if (service === undefined) return false
207
+ const side = service as SidebarRightLike
208
+ if (typeof side.openResource !== 'function') return false
209
+ if (originals.has(service)) {
210
+ stopTimer()
211
+ return true
212
+ }
213
+ const original = side.openResource.bind(side)
214
+ const wrapper = (address: string, options?: unknown): unknown => {
215
+ try {
216
+ // Cheap, synchronous gate first: only a file address of a session in
217
+ // one of our bindings is intercepted. Everything else keeps the native
218
+ // call (and its synchronous throw semantics) untouched.
219
+ if (!isEditorCentered()) return original(address, options)
220
+ const parsed = parseFileAddress(address)
221
+ if (parsed === undefined) return original(address, options)
222
+ return interceptSync(ctx, parsed, address, options) === true ? undefined : original(address, options)
223
+ } catch {
224
+ return original(address, options)
225
+ }
226
+ }
227
+ try {
228
+ side.openResource = wrapper
229
+ originals.set(service, original)
230
+ } catch {
231
+ return false
232
+ }
233
+ stopTimer()
234
+ return true
235
+ }
236
+
237
+ if (!attempt()) {
238
+ timer = setInterval(() => { if (attempt()) stopTimer() }, 500)
239
+ timer.unref?.()
240
+ }
241
+
242
+ return (): void => {
243
+ disposed = true
244
+ stopTimer()
245
+ let service: object | undefined
246
+ try {
247
+ service = (ctx as unknown as { get(name: string): unknown }).get('sidebarRight') as object | undefined
248
+ } catch {
249
+ service = undefined
250
+ }
251
+ if (service === undefined) return
252
+ const original = originals.get(service)
253
+ if (original === undefined) return
254
+ try {
255
+ ;(service as SidebarRightLike).openResource = original
256
+ } catch {
257
+ // Ignore: the service is being torn down.
258
+ }
259
+ originals.delete(service)
260
+ }
261
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Reveal the plugin's conversation view tab (对话 / 轨迹 / …).
3
+ *
4
+ * The editor lives in a `conversation.view` slot, and the shell renders ONLY
5
+ * the selected view — there is no public service to select one (the shell's
6
+ * own header owns `selectView`). So when a file or diff is opened from the
7
+ * sidebar while that editor tab is not the active view, the plugin asks the
8
+ * shell's tab by clicking it, exactly as a user would.
9
+ *
10
+ * This is intentionally defensive: the lookup is label-based, skips our own
11
+ * surfaces, and a miss is reported to the caller (which then tells the user
12
+ * where the file opened) rather than throwing.
13
+ */
14
+
15
+ /** Click the conversation-view tab whose label matches, if it is on screen. */
16
+ export function revealViewTab(label: string): boolean {
17
+ if (typeof document === 'undefined' || label === '') return false
18
+ const candidates = document.querySelectorAll<HTMLElement>('button, [role="tab"], [role="button"]')
19
+ for (const node of candidates) {
20
+ if (node.closest('.rvs-root') !== null) continue
21
+ const text = (node.textContent ?? '').trim()
22
+ if (text !== label) continue
23
+ // Only act on something that behaves like a tab: a small control whose
24
+ // closest ancestor row holds the other view labels too.
25
+ try {
26
+ node.click()
27
+ return true
28
+ } catch {
29
+ return false
30
+ }
31
+ }
32
+ return false
33
+ }