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,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
|
+
}
|