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.
- package/LICENSE +21 -0
- package/README.en.md +385 -0
- package/README.md +347 -2
- package/cordis.patch.yml +259 -0
- package/icon.svg +7 -0
- package/lib/client.js +62602 -0
- package/lib/index.js +4287 -0
- package/lib/p2-bridge-refs.js +10 -0
- package/lib/p2-bridge.js +16 -0
- package/lib/p2-router-fs.js +252 -0
- package/lib/p2-router-refs.js +466 -0
- package/lib/p2-router-spill.js +62 -0
- package/lib/p2-router-subprocess.js +64 -0
- package/lib/p2-shell-bash.js +24 -0
- package/lib/ssh-proc-arI5lm4W.js +632 -0
- package/lib/types/changes/recorder.d.ts +240 -0
- package/lib/types/changes/status.d.ts +201 -0
- package/lib/types/changes/text-diff.d.ts +65 -0
- package/lib/types/client/CodeEditor.d.ts +125 -0
- package/lib/types/client/Dialog.d.ts +14 -0
- package/lib/types/client/DirectoryFlow.d.ts +30 -0
- package/lib/types/client/EditorPane.d.ts +30 -0
- package/lib/types/client/HostsView.d.ts +10 -0
- package/lib/types/client/NativeExplorer.d.ts +39 -0
- package/lib/types/client/RemoteDirectoryPicker.d.ts +16 -0
- package/lib/types/client/RemoteEditorView.d.ts +20 -0
- package/lib/types/client/RemoteExplorer.d.ts +18 -0
- package/lib/types/client/RemoteRoot.d.ts +16 -0
- package/lib/types/client/SearchView.d.ts +14 -0
- package/lib/types/client/SettingsHosts.d.ts +15 -0
- package/lib/types/client/TerminalView.d.ts +16 -0
- package/lib/types/client/WorkspaceWizard.d.ts +16 -0
- package/lib/types/client/WorkspacesView.d.ts +14 -0
- package/lib/types/client/XTerm.d.ts +15 -0
- package/lib/types/client/api.d.ts +162 -0
- package/lib/types/client/changes/ChangesDock.d.ts +35 -0
- package/lib/types/client/changes/model.d.ts +36 -0
- package/lib/types/client/changes/undo-affordance.d.ts +48 -0
- package/lib/types/client/changes/undo-context.d.ts +25 -0
- package/lib/types/client/composer.d.ts +165 -0
- package/lib/types/client/i18n.d.ts +17 -0
- package/lib/types/client/index.d.ts +14 -0
- package/lib/types/client/remote-file-open-target.d.ts +247 -0
- package/lib/types/client/remote-file-open.d.ts +35 -0
- package/lib/types/client/reveal.d.ts +15 -0
- package/lib/types/client/scm/DiffView.d.ts +24 -0
- package/lib/types/client/scm/HistoryPanel.d.ts +12 -0
- package/lib/types/client/scm/RepoPicker.d.ts +24 -0
- package/lib/types/client/scm/ScmPanel.d.ts +15 -0
- package/lib/types/client/scm/actions.d.ts +170 -0
- package/lib/types/client/scm/diff-model.d.ts +29 -0
- package/lib/types/client/scm/stage-queue.d.ts +99 -0
- package/lib/types/client/scm/status-map.d.ts +157 -0
- package/lib/types/client/scm/untracked-dirs.d.ts +72 -0
- package/lib/types/client/selection-popup.d.ts +61 -0
- package/lib/types/client/store.d.ts +437 -0
- package/lib/types/client/theme.d.ts +15 -0
- package/lib/types/client/ui.d.ts +46 -0
- package/lib/types/client/upload-plan.d.ts +78 -0
- package/lib/types/client/upload-source.d.ts +46 -0
- package/lib/types/client/use-remote-chat-target.d.ts +17 -0
- package/lib/types/client/vsc/FindWidget.d.ts +81 -0
- package/lib/types/client/vsc/change-marks.d.ts +150 -0
- package/lib/types/client/vsc/editor-langs.d.ts +5 -0
- package/lib/types/client/vsc/editor-palette.d.ts +37 -0
- package/lib/types/client/vsc/editor-theme.d.ts +32 -0
- package/lib/types/client/vsc/fallback-highlight.d.ts +26 -0
- package/lib/types/client/vsc/kit.d.ts +195 -0
- package/lib/types/client/vsc/markdown.d.ts +11 -0
- package/lib/types/client/vsc/reveal-line.d.ts +38 -0
- package/lib/types/client/vsc/tokenize.d.ts +104 -0
- package/lib/types/deploy.d.ts +94 -0
- package/lib/types/fs-ops.d.ts +8 -0
- package/lib/types/git-format.d.ts +34 -0
- package/lib/types/git-ops.d.ts +183 -0
- package/lib/types/http.d.ts +14 -0
- package/lib/types/importers.d.ts +20 -0
- package/lib/types/index.d.ts +87 -0
- package/lib/types/keygen.d.ts +19 -0
- package/lib/types/loopback.d.ts +7 -0
- package/lib/types/p2/bridge-refs.d.ts +21 -0
- package/lib/types/p2/bridge.d.ts +24 -0
- package/lib/types/p2/file-refs.d.ts +73 -0
- package/lib/types/p2/helper-rpc.d.ts +63 -0
- package/lib/types/p2/in-flight-gate.d.ts +25 -0
- package/lib/types/p2/router-fs.d.ts +92 -0
- package/lib/types/p2/router-refs.d.ts +45 -0
- package/lib/types/p2/router-spill.d.ts +11 -0
- package/lib/types/p2/router-subprocess.d.ts +23 -0
- package/lib/types/p2/shell-bash.d.ts +16 -0
- package/lib/types/p2/ssh-proc.d.ts +22 -0
- package/lib/types/p2/worlds.d.ts +179 -0
- package/lib/types/paths.d.ts +12 -0
- package/lib/types/pool.d.ts +105 -0
- package/lib/types/protocol.d.ts +452 -0
- package/lib/types/routes.d.ts +27 -0
- package/lib/types/search-ops.d.ts +68 -0
- package/lib/types/sftp.d.ts +90 -0
- package/lib/types/store.d.ts +66 -0
- package/lib/types/terminal.d.ts +51 -0
- package/lib/types/timeouts.d.ts +83 -0
- package/lib/types/tools.d.ts +41 -0
- package/lib/worlds-PUumRIh7.js +18765 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +189 -4
- package/src/changes/recorder.ts +902 -0
- package/src/changes/status.ts +448 -0
- package/src/changes/text-diff.ts +377 -0
- package/src/client/CodeEditor.tsx +923 -0
- package/src/client/Dialog.tsx +76 -0
- package/src/client/DirectoryFlow.tsx +431 -0
- package/src/client/EditorPane.tsx +512 -0
- package/src/client/HostsView.tsx +312 -0
- package/src/client/NativeExplorer.tsx +121 -0
- package/src/client/RemoteDirectoryPicker.tsx +157 -0
- package/src/client/RemoteEditorView.tsx +54 -0
- package/src/client/RemoteExplorer.tsx +1398 -0
- package/src/client/RemoteRoot.tsx +389 -0
- package/src/client/SearchView.tsx +407 -0
- package/src/client/SettingsHosts.tsx +136 -0
- package/src/client/TerminalView.tsx +308 -0
- package/src/client/WorkspaceWizard.tsx +199 -0
- package/src/client/WorkspacesView.tsx +93 -0
- package/src/client/XTerm.tsx +308 -0
- package/src/client/api.ts +331 -0
- package/src/client/changes/ChangesDock.tsx +348 -0
- package/src/client/changes/changes.css +230 -0
- package/src/client/changes/model.ts +347 -0
- package/src/client/changes/undo-affordance.ts +389 -0
- package/src/client/changes/undo-context.ts +34 -0
- package/src/client/composer.ts +436 -0
- package/src/client/css.d.ts +7 -0
- package/src/client/i18n.ts +702 -0
- package/src/client/index.tsx +286 -0
- package/src/client/remote-file-open-target.ts +553 -0
- package/src/client/remote-file-open.ts +261 -0
- package/src/client/reveal.ts +33 -0
- package/src/client/scm/DiffView.tsx +570 -0
- package/src/client/scm/HistoryPanel.tsx +353 -0
- package/src/client/scm/RepoPicker.tsx +238 -0
- package/src/client/scm/ScmPanel.tsx +857 -0
- package/src/client/scm/actions.ts +460 -0
- package/src/client/scm/diff-model.ts +206 -0
- package/src/client/scm/stage-queue.ts +264 -0
- package/src/client/scm/status-map.ts +381 -0
- package/src/client/scm/untracked-dirs.ts +139 -0
- package/src/client/selection-popup.ts +157 -0
- package/src/client/store.tsx +1670 -0
- package/src/client/theme.ts +54 -0
- package/src/client/ui.tsx +136 -0
- package/src/client/upload-plan.ts +153 -0
- package/src/client/upload-source.ts +145 -0
- package/src/client/use-remote-chat-target.ts +153 -0
- package/src/client/vsc/FindWidget.tsx +446 -0
- package/src/client/vsc/change-marks.ts +586 -0
- package/src/client/vsc/editor-langs.ts +137 -0
- package/src/client/vsc/editor-palette.ts +107 -0
- package/src/client/vsc/editor-theme.ts +93 -0
- package/src/client/vsc/fallback-highlight.ts +145 -0
- package/src/client/vsc/kit.tsx +777 -0
- package/src/client/vsc/markdown.tsx +172 -0
- package/src/client/vsc/reveal-line.ts +142 -0
- package/src/client/vsc/tokenize.ts +569 -0
- package/src/client/vsc/vsc.css +2669 -0
- package/src/deploy.ts +481 -0
- package/src/fs-ops.ts +57 -0
- package/src/git-format.ts +205 -0
- package/src/git-ops.ts +693 -0
- package/src/http.ts +55 -0
- package/src/importers.ts +141 -0
- package/src/index.ts +442 -0
- package/src/keygen.ts +77 -0
- package/src/loopback.ts +47 -0
- package/src/p2/bridge-refs.ts +27 -0
- package/src/p2/bridge.ts +32 -0
- package/src/p2/file-refs.ts +392 -0
- package/src/p2/helper-rpc.ts +394 -0
- package/src/p2/in-flight-gate.ts +63 -0
- package/src/p2/router-fs.ts +327 -0
- package/src/p2/router-refs.ts +166 -0
- package/src/p2/router-spill.ts +75 -0
- package/src/p2/router-subprocess.ts +92 -0
- package/src/p2/shell-bash.ts +30 -0
- package/src/p2/ssh-proc.ts +802 -0
- package/src/p2/worlds.ts +735 -0
- package/src/paths.ts +50 -0
- package/src/pool.ts +811 -0
- package/src/protocol.ts +460 -0
- package/src/routes.ts +802 -0
- package/src/search-ops.ts +220 -0
- package/src/sftp.ts +677 -0
- package/src/store.ts +364 -0
- package/src/terminal.ts +291 -0
- package/src/timeouts.ts +122 -0
- package/src/tools.ts +320 -0
- package/vendor/dsh-remote-helpers/dsh-ssh-helper-0.2.1-alpha.2-linux-x64.helper.json +16 -0
- package/vendor/dsh-remote-helpers/dsh-ssh-helper-0.2.1-alpha.2-linux-x64.tar.gz +4 -0
- package/vendor/dsh-remote-helpers/dsh-ssh-helper-0.2.1-alpha.2-linux-x64.tar.gz.sha256 +1 -0
- 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
|
+
}
|