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,240 @@
1
+ /**
2
+ * Per-session change ledger for remote workspaces.
3
+ *
4
+ * DSH already records "what did this turn change" — but only for local
5
+ * sessions: `@deepseek-ai/dsh-workspace-changes` parses git's POSIX output with
6
+ * host path semantics and stages its scratch index through `node:fs`, so on a
7
+ * routed session the first baseline snapshot fails and the recorder returns
8
+ * before appending anything (measured: 58 remote-bound sessions, 390 fs
9
+ * writes, zero `workspace/changes` events).
10
+ *
11
+ * This ledger re-expresses the idea for a remote host, and one measurement
12
+ * decided its shape. A first version snapshotted the worktree with `git add
13
+ * --all` into a private index; on a monorepo (AOSP, ~1M files) that never
14
+ * finished — it was still hashing two minutes in, and the panel sat at
15
+ * "computing…" forever. Everything here is therefore **read-only and
16
+ * proportional to the files that changed**, never to the size of the
17
+ * repository:
18
+ *
19
+ * 1. `git status --porcelain -uno` names the tracked files that differ (1.7s
20
+ * measured), `git ls-files --others` names the new ones (11.3s measured,
21
+ * run as its own best-effort exec).
22
+ * 2. Only those paths are read (their current text, and `HEAD:<path>` for the
23
+ * ones the session started clean).
24
+ * 3. The diff itself is computed here, line by line, with no git call at all.
25
+ *
26
+ * Nothing in this file writes to the repository: no `git add`, no index, no
27
+ * object store, no temp files under the repo. "What changed" is answered by
28
+ * git's own stat cache; "what changed *since the session started*" is answered
29
+ * by keeping the pre-existing dirty files' contents at session start and
30
+ * treating an unchanged one as untouched.
31
+ */
32
+ import type { RemoteChangesDiff, RemoteChangesSummary, RemoteTimeouts } from '../protocol.ts';
33
+ import type { RemotePool } from '../pool.ts';
34
+ import type { RemoteWorlds } from '../p2/worlds.ts';
35
+ /** The two facts a turn boundary needs from a Session, kept cordis-free. */
36
+ export interface LedgerTarget {
37
+ sessionId: string;
38
+ /** Session working directory as the host sees it (an alias path for remote sessions). */
39
+ cwd: string;
40
+ }
41
+ /** Collaborators the ledger needs; all injectable so it can be unit-tested. */
42
+ export interface SessionChangesDeps {
43
+ pool: RemotePool;
44
+ worlds: RemoteWorlds;
45
+ /** Effective git budget, read per call (a `volatile()` edit applies live). */
46
+ timeouts: () => RemoteTimeouts;
47
+ log?: (level: 'info' | 'warn', message: string) => void;
48
+ /** Cap on listed files; the header totals stay whole. */
49
+ maxFiles?: number;
50
+ /** Cap on one file's text, in bytes. */
51
+ maxFileBytes?: number;
52
+ /** Cap on one refresh's total text, in bytes. */
53
+ maxTotalBytes?: number;
54
+ /** Budget for the untracked scan, which is an order of magnitude slower. */
55
+ untrackedBudgetMs?: number;
56
+ /** How often the untracked scan may run again (default five seconds). */
57
+ untrackedRefreshMs?: number;
58
+ /**
59
+ * Override the per-exec budget outright.
60
+ *
61
+ * The timeouts setting is clamped to a sane range, which is right for a Host
62
+ * but useless for a check that must not wait five seconds to see a watchdog
63
+ * fire.
64
+ */
65
+ execBudgetMs?: number;
66
+ }
67
+ export declare class SessionChanges {
68
+ private readonly ledgers;
69
+ private readonly deps;
70
+ private disposed;
71
+ constructor(deps: SessionChangesDeps);
72
+ /** Open (or reuse) the ledger for one session at the start of a turn. */
73
+ beginTurn(target: LedgerTarget): void;
74
+ /** Refresh the ledger at the end of a turn. */
75
+ endTurn(target: LedgerTarget, turn: number): void;
76
+ /** The current summary for one session, or null when it has no ledger. */
77
+ summary(sessionId: string): RemoteChangesSummary | null;
78
+ /**
79
+ * Compare one listed file across the whole session.
80
+ *
81
+ * The `before` side comes from the session baseline, which never moves. The
82
+ * `after` side is read from the host on every call rather than replayed from
83
+ * the last scan: a revert, a save from the editor or a shell command all
84
+ * change the file between scans, and replaying a cached copy against a fresh
85
+ * buffer is exactly how the painted lines end up at the wrong places.
86
+ *
87
+ * @param sessionId - live session owning the ledger.
88
+ * @param path - the file's remote absolute path (as listed in the summary).
89
+ * @returns hunks for decorations, or null when the session/file is unknown.
90
+ */
91
+ diff(sessionId: string, path: string): Promise<RemoteChangesDiff | null>;
92
+ /** Resolve once every queued refresh has settled, including coalesced work. */
93
+ settled(): Promise<void>;
94
+ /** Drop one session's ledger. */
95
+ forget(sessionId: string): void;
96
+ /** Release every ledger (plugin unmount). */
97
+ dispose(): void;
98
+ /**
99
+ * Force a fresh pass, ignoring the untracked cadence.
100
+ *
101
+ * The panel's refresh button calls this: the cadence exists so a slow scan
102
+ * does not run on every turn, but "I clicked refresh" means the user wants
103
+ * the truth now — including files that appeared seconds ago.
104
+ */
105
+ rescan(sessionId: string): void;
106
+ /**
107
+ * Self-heal a ledger whose data has gone stale.
108
+ *
109
+ * The panel polls `list`, and a poll is the one moment we know a human is
110
+ * looking at the answer. If the data is older than `maxAgeMs`, re-scan — so a
111
+ * turn boundary this Host never delivered costs a few seconds of staleness
112
+ * instead of a manual refresh.
113
+ *
114
+ * @returns whether a refresh was queued.
115
+ */
116
+ touch(sessionId: string, maxAgeMs?: number): boolean;
117
+ /**
118
+ * Point one ledger at a subdirectory of the session root.
119
+ *
120
+ * This is the Source Control panel's "repository directory": an explicit
121
+ * choice by the user, and therefore the authority whenever it is set. The
122
+ * automatic one-level probe is only a fallback for a container workspace that
123
+ * holds exactly one repository.
124
+ *
125
+ * @param subdir - workspace-relative directory, or undefined for the session
126
+ * root itself. Absolute paths and `..` are refused: they would move the
127
+ * ledger outside the workspace it was created for.
128
+ */
129
+ useRepoSubdir(sessionId: string, subdir: string | undefined): void;
130
+ /** Resolve the routing target once per session, caching the ledger. */
131
+ private ensure;
132
+ /** Queue a refresh, coalescing bursts (a turn can end while one is running). */
133
+ private schedule;
134
+ /**
135
+ * One refresh: the cheap scan first, then the slow one in the background.
136
+ *
137
+ * The split is not cosmetic. On the reference monorepo `git status -uno`
138
+ * answers in ~3s while `git ls-files --others` needs over a minute, so a
139
+ * single sequential refresh would leave the panel empty for that whole
140
+ * minute even though the tracked changes were known after three seconds.
141
+ * Phase one publishes, phase two adds the new files when (and if) it lands.
142
+ */
143
+ private refresh;
144
+ /** Recompute the panel's list from the scans currently known. */
145
+ private publish;
146
+ /**
147
+ * The cheap scan: tracked files whose working-tree bytes differ from the
148
+ * index (1.4s measured on the reference monorepo).
149
+ *
150
+ * A session working directory is often a CONTAINER for repositories rather
151
+ * than one itself (the reference host has `/home/xiray/dsh_workbase` holding
152
+ * `emv-toolbox-web`), and `git status` there answers exit 128 — "not a git
153
+ * repository (or any parent up to mount point /)". When that happens the
154
+ * ledger looks one level down once and adopts the repository it finds, which
155
+ * is the same idea the SCM panel's `repoSubdir` expresses by hand.
156
+ *
157
+ * @returns entries relative to the ledger's root, or undefined when no
158
+ * repository could be found (the ledger then reports why).
159
+ */
160
+ private scanTracked;
161
+ /**
162
+ * Look for a repository in the session root's immediate subdirectories.
163
+ *
164
+ * **Only an unambiguous answer is adopted.** A workspace that holds several
165
+ * repositories has no single right one, and silently picking the first would
166
+ * report changes from a project the session never touched — worse than saying
167
+ * "I don't know". The explicit repository directory the Source Control panel
168
+ * stores is the real answer, and this fallback exists only to spare the user
169
+ * from setting it when there is exactly one candidate.
170
+ *
171
+ * @returns the adopted root, or undefined when nothing (or too much) was found.
172
+ */
173
+ private adoptRepositoryBelow;
174
+ /**
175
+ * The slow scan: new files, in the background.
176
+ *
177
+ * The same command costs ~11s on a warm tree and over a minute on a cold one,
178
+ * and on the reference AOSP monorepo it has been measured past 60s with no
179
+ * answer at all. It therefore never runs inline with the tracked scan, never
180
+ * runs twice at once, and only runs again after {@link untrackedCadence}. A
181
+ * repository that cannot answer inside {@link untrackedBudget} is written off
182
+ * for the session (`untrackedGivenUp`) rather than retried every turn.
183
+ */
184
+ private kickUntracked;
185
+ /**
186
+ * How long before the untracked scan may run again.
187
+ *
188
+ * Self-tuning on purpose: the same command costs ~0.2s in a small repository
189
+ * and over a minute in a monorepo, so a fixed cadence is either wasteful or
190
+ * useless. Backing off to four times the last duration keeps small projects
191
+ * near-real-time while a big one settles into a scan every couple of minutes
192
+ * instead of one per turn.
193
+ */
194
+ private untrackedCadence;
195
+ /**
196
+ * Fold files that already existed when the session began into the baseline.
197
+ *
198
+ * The untracked walk is slow enough (a minute on a monorepo) that the baseline
199
+ * refresh publishes before it lands, so its first result arrives without a
200
+ * "before" side. Without this, every file the checkout already had — the
201
+ * user's own scratch files, a build's leftovers — would be announced as this
202
+ * session's work, complete with a full-file addition count.
203
+ *
204
+ * mtime decides: strictly older than the ledger's own start means the session
205
+ * cannot have created it, and its present content is therefore its baseline.
206
+ */
207
+ private absorbPreExisting;
208
+ /**
209
+ * Race one host call against its own budget.
210
+ *
211
+ * A budget handed to `exec` is a request, not a guarantee: the transport may
212
+ * keep the promise open long after the command should have been killed (the
213
+ * remote process can even be gone while the channel stays). Awaiting such a
214
+ * promise blocks the ledger's whole queue, so the panel shows "computing…"
215
+ * for as long as the user is willing to stare at it. This bounds that wait.
216
+ */
217
+ private withWatchdog;
218
+ /** Read a batch of files, capped per file and in total. */
219
+ private readPaths;
220
+ /** Run one command on the host, normalizing failure into a reason string. */
221
+ private run;
222
+ /** Turn a failed scan into something a user can act on. */
223
+ private reasonOf;
224
+ /** One exec's budget: the git setting, clamped so a hung host cannot pile up work. */
225
+ private execBudget;
226
+ /** The untracked scan gets its own, more generous budget. */
227
+ /**
228
+ * The untracked scan gets its own, much more generous budget.
229
+ *
230
+ * Three minutes is a deliberate choice for a monorepo: the walk is proportional
231
+ * to the whole checkout, not to the session, and the reference AOSP tree has
232
+ * been measured past a minute cold. Waiting is acceptable here because the
233
+ * scan runs in the background and the tracked changes are already on screen;
234
+ * giving up early is not, because the files a session creates with a shell
235
+ * command are visible ONLY through this scan.
236
+ */
237
+ private untrackedBudget;
238
+ private budget;
239
+ private warn;
240
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Remote command builders and reply parsers for the session change ledger.
3
+ *
4
+ * Why this module exists: `@deepseek-ai/dsh-workspace-changes` (and the first
5
+ * cut of this plugin) answer "what did this session change?" by staging the
6
+ * whole worktree (`git add --all`) and diffing two trees. On a monorepo the
7
+ * staging step alone ran past 40s without finishing, so the panel never left
8
+ * "正在统计". Measured on a real AOSP checkout with a warm cache, same repo:
9
+ *
10
+ * | command | time |
11
+ * | -------------------------------------------- | ------ |
12
+ * | `git diff-files --name-status -z` | 1.40s |
13
+ * | `git status --porcelain -z -uno` | 1.67s |
14
+ * | `git ls-files --others --exclude-standard -z`| 11.25s |
15
+ * | `git add --all` (the old baseline) | >40s |
16
+ *
17
+ * So the cheap path is: ask git *which* paths changed (status), read only those
18
+ * paths' bytes, and diff locally. Cost becomes proportional to what the session
19
+ * touched instead of to the size of the repository.
20
+ *
21
+ * Everything here is pure string in / string out — no pool, no fs, no DSH
22
+ * import — so the recorder owns all the IO and this half stays testable.
23
+ *
24
+ * ## Paths, quoting, exit codes
25
+ *
26
+ * Every path is a POSIX absolute path on the host and every shell word is
27
+ * single-quoted ({@link shq}); a path that reaches a shell unquoted is a bug.
28
+ * A command exits `0` when it produced a reply, `90` when its root directory
29
+ * does not exist (a stale alias), `92` when the host has no `base64`, and
30
+ * otherwise with git's own exit status. The per-file problems of the read
31
+ * commands (missing, unreadable, too big) are *not* failures: they are flags
32
+ * inside a successful reply.
33
+ *
34
+ * ## Read reply wire format
35
+ *
36
+ * One block per requested path, in request order:
37
+ *
38
+ * ```
39
+ * @@RVS-FILE <base64(path)> ok@@
40
+ * <base64(content)>
41
+ * @@RVS-FILE <base64(path)> big@@
42
+ * @@RVS-FILE <base64(path)> absent@@
43
+ * ```
44
+ *
45
+ * - Paths travel base64-encoded in the marker so spaces, quotes, CJK and even
46
+ * newlines cannot break the framing; content travels base64-encoded on the
47
+ * single line after the marker so NUL bytes and `\r\n` survive a text-only
48
+ * transport (one line, no wrapping — the shell strips base64's newlines).
49
+ * - `ok` is followed by exactly one content line. An empty file is an empty
50
+ * content line (a bare `\n` after the marker line).
51
+ * - `big` means "not transferred": the file alone is over `capBytes`, or the
52
+ * running total would pass `totalCapBytes`.
53
+ * - `absent` means the path is not there *or* could not be read (permissions,
54
+ * a directory, a dangling blob); the recorder treats both as "no content".
55
+ *
56
+ * Binary detection is deliberately done in {@link parseReads} (any NUL byte in
57
+ * the decoded content) rather than in the shell: one definition of "binary" for
58
+ * every caller, and no dependence on `grep`/`od` behaviour across hosts.
59
+ */
60
+ /** What happened to one path in this session. */
61
+ export type ChangeStatus = 'A' | 'M' | 'D' | 'R' | 'C' | 'T' | 'U' | '?';
62
+ /** One changed path, relative to the session root. */
63
+ export interface StatusEntry {
64
+ path: string;
65
+ status: ChangeStatus;
66
+ }
67
+ /** What one read asked for and what came back. */
68
+ export interface ReadResult {
69
+ /** Decoded text, or null when nothing usable was transferred. */
70
+ text: string | null;
71
+ /** Withheld because of a size cap (single file or the running total). */
72
+ truncated: boolean;
73
+ /** The content contains a NUL byte, so it is not text. */
74
+ binary: boolean;
75
+ /** Not there, or unreadable. */
76
+ absent: boolean;
77
+ /**
78
+ * Modification time in whole seconds (worktree reads only).
79
+ *
80
+ * A large file whose bytes were withheld can still be compared by mtime: two
81
+ * equal mtimes mean the session did not touch it, which is the difference
82
+ * between a clean list and one padded with every big file the checkout
83
+ * happens to carry. Zero when the host could not answer.
84
+ */
85
+ mtime?: number;
86
+ /** Size in bytes, when known (`0` for a missing path). */
87
+ size?: number;
88
+ }
89
+ /** Exit statuses of the read commands (git's own statuses pass through as-is). */
90
+ export declare const READ_EXIT: {
91
+ /** The reported root does not exist on the host (stale alias). */
92
+ readonly noRoot: 90;
93
+ /**
94
+ * No `base64` on the host. Probed up front because without it every read
95
+ * would come back empty and look like "this file has no content", which is a
96
+ * far worse failure than a clear refusal.
97
+ */
98
+ readonly noBase64: 92;
99
+ };
100
+ /**
101
+ * Tracked paths that changed, HEAD/index vs worktree, staged changes included.
102
+ *
103
+ * `-uno` is what keeps this cheap: untracked detection is the expensive half
104
+ * (11s above) and it is answered by {@link untrackedCommand} instead. `-- .`
105
+ * scopes the walk to the session root, so a session sitting in one package of
106
+ * a monorepo does not pay for the rest.
107
+ *
108
+ * @param root - remote absolute POSIX directory (the session cwd).
109
+ * @returns a `sh` script printing NUL-separated porcelain v1 records.
110
+ */
111
+ export declare function trackedStatusCommand(root: string): string;
112
+ /**
113
+ * New files git is not tracking yet (the ones a session usually creates).
114
+ *
115
+ * @param root - remote absolute POSIX directory (the session cwd).
116
+ * @returns a `sh` script printing NUL-separated paths.
117
+ */
118
+ export declare function untrackedCommand(root: string): string;
119
+ /**
120
+ * Parse `git status --porcelain -z -uno` output.
121
+ *
122
+ * Porcelain v1 with `-z` writes `XY <path>\0`, and for a rename or copy the
123
+ * source path follows as its own record: `R <new>\0<old>\0` (verified against
124
+ * git 2.x: the NEW path comes first). The source record is consumed, not parsed
125
+ * as its own entry, and the reported path is the NEW one — the only path the
126
+ * editor can open.
127
+ *
128
+ * The two status columns mean "index vs HEAD" (X) and "worktree vs index" (Y);
129
+ * one letter is reported, chosen for how the file reads in a session review:
130
+ * conflict > deleted > renamed > copied > added > typechange > modified.
131
+ *
132
+ * @param stdout - raw reply of {@link trackedStatusCommand}.
133
+ */
134
+ export declare function parseStatusZ(stdout: string): StatusEntry[];
135
+ /**
136
+ * Parse `git ls-files --others --exclude-standard -z` output into untracked
137
+ * entries. Git already filtered ignored files out, and its paths are never
138
+ * quoted in `-z` mode.
139
+ */
140
+ export declare function parseUntrackedZ(stdout: string): StatusEntry[];
141
+ /**
142
+ * Read several worktree files in one remote run.
143
+ *
144
+ * One `sh` script walks the paths in order and emits a {@link ReadResult} block
145
+ * per path. Sizes come from `wc -c` before anything is transferred, so a 4GB
146
+ * build artifact costs one `stat` instead of 5GB of base64; the running total
147
+ * stops the reply from growing past `totalCapBytes`, after which the remaining
148
+ * paths are reported `big` without being read.
149
+ *
150
+ * A symlink reports its target as content, because that is what git stores for
151
+ * mode 120000 — catting the link's target file would diff the wrong bytes.
152
+ *
153
+ * @param root - remote absolute POSIX directory (the session cwd).
154
+ * @param paths - paths relative to `root`.
155
+ * @param capBytes - per-file cap; larger files are reported `big`.
156
+ * @param totalCapBytes - cap on the sum of transferred content bytes.
157
+ */
158
+ export declare function readWorktreeCommand(root: string, paths: readonly string[], capBytes: number, totalCapBytes: number): string;
159
+ /**
160
+ * Read several `HEAD:<path>` blobs in one remote run — the "before" side of a
161
+ * file that was already committed and clean when the session started.
162
+ *
163
+ * `git cat-file -s` answers existence *and* size before any bytes move, so the
164
+ * caps are exact, and a path that is not in `HEAD` (a new file, a repository
165
+ * with no commit yet, a submodule gitlink) is reported `absent` rather than
166
+ * failing the whole read. `cat-file blob` is used instead of `show` on purpose:
167
+ * no textconv, no filters, no pager — the bytes git will diff.
168
+ *
169
+ * @param root - remote absolute POSIX directory (the session cwd).
170
+ * @param paths - paths relative to `root`.
171
+ * @param capBytes - per-file cap; larger blobs are reported `big`.
172
+ * @param totalCapBytes - cap on the sum of transferred content bytes.
173
+ */
174
+ export declare function readHeadCommand(root: string, paths: readonly string[], capBytes: number, totalCapBytes: number): string;
175
+ /**
176
+ * Name every immediate subdirectory that is a git repository.
177
+ *
178
+ * A session working directory is often a CONTAINER for repositories (the
179
+ * reference host has `/home/xiray/dsh_workbase` holding `emv-toolbox-web`), and
180
+ * `git status` at the container answers 128. This is a one-level `ls`-style
181
+ * probe — bounded work, no `find` — so it can never become the expensive part
182
+ * of a scan.
183
+ *
184
+ * It prints ALL matches on purpose: a workspace with several repositories has no
185
+ * single right answer, and the caller must refuse to guess rather than pick the
186
+ * first one. The SCM panel's explicit repository directory is the real answer.
187
+ *
188
+ * @param root - remote absolute POSIX directory (the session cwd).
189
+ */
190
+ export declare function reposBelowCommand(root: string): string;
191
+ /**
192
+ * Parse a {@link readWorktreeCommand} / {@link readHeadCommand} reply.
193
+ *
194
+ * Keys are the decoded paths, exactly as requested. A marker without its
195
+ * content line can only mean the reply was cut short (a timeout killing the
196
+ * pipe); such a path is reported truncated rather than as an empty file, so an
197
+ * aborted read never masquerades as "the file is empty".
198
+ *
199
+ * @param stdout - raw reply of either read command.
200
+ */
201
+ export declare function parseReads(stdout: string): Map<string, ReadResult>;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Line-level diff for the session change ledger.
3
+ *
4
+ * The recorder gets *content* (see `status.ts`) and needs the same thing git
5
+ * would have printed with `-U3`: hunks a reviewer can read, counts for the file
6
+ * list, and a patch that can undo the change. Doing it here, on the host side,
7
+ * means a file's before/after text is compared without another round trip to
8
+ * the remote — and without teaching git about the session baseline.
9
+ *
10
+ * No dependency, no IO, on purpose. The algorithm is Myers' greedy edit script
11
+ * (the one git's own xdiff implements): common prefix and suffix are stripped
12
+ * first, then the middle is diffed, so the cost follows the size of the edit
13
+ * rather than the size of the file.
14
+ *
15
+ * Line conventions, chosen to match `git diff` byte for byte:
16
+ *
17
+ * - a final `\n` does not produce an extra empty line;
18
+ * - `\r` stays inside the line, so a CRLF file keeps its `\r` and a CRLF/LF
19
+ * mismatch shows up as changed lines instead of a phantom whole-file edit;
20
+ * - a file that does not end in `\n` keeps its last line, and the hunk is
21
+ * flagged `oldNoEol` / `newNoEol` so {@link hunkPatch} can emit git's
22
+ * `` marker — without it `git apply -R` refuses to
23
+ * touch such a file.
24
+ */
25
+ /** One unified-diff hunk. Line numbers are 1-based, like git's. */
26
+ export interface TextHunk {
27
+ /** First old-side line in the hunk; 0 when the hunk only adds lines. */
28
+ oldStart: number;
29
+ /** Number of old-side lines the hunk covers (context + removed). */
30
+ oldLines: number;
31
+ /** First new-side line in the hunk; 0 when the hunk only removes lines. */
32
+ newStart: number;
33
+ /** Number of new-side lines the hunk covers (context + added). */
34
+ newLines: number;
35
+ /**
36
+ * Lines with their leading `' '`, `'-'` or `'+'`, and without the trailing
37
+ * `\n` — the shape the client's diff view already consumes.
38
+ */
39
+ lines: string[];
40
+ /** The hunk reaches the end of `before`, which does not end in a newline. */
41
+ oldNoEol?: true;
42
+ /** The hunk reaches the end of `after`, which does not end in a newline. */
43
+ newNoEol?: true;
44
+ }
45
+ /** A whole text comparison: hunks plus the counts the file list shows. */
46
+ export interface TextDiff {
47
+ hunks: TextHunk[];
48
+ /** `'+'` lines, counted from {@link hunks} so the two can never disagree. */
49
+ added: number;
50
+ /** `'-'` lines, counted from {@link hunks}. */
51
+ deleted: number;
52
+ }
53
+ /** Compare two texts into git-shaped hunks (default 3 lines of context). */
54
+ export declare function diffText(before: string, after: string, context?: number): TextDiff;
55
+ /**
56
+ * Serialize one hunk as a patch `git apply` accepts (and `git apply -R` undoes).
57
+ *
58
+ * The `@@ -a,b +c,d @@` counts are always written out — git accepts the elided
59
+ * `,1` and `,-0` spellings, but a reviewer reading a log is better served by the
60
+ * explicit form, and one spelling means one code path to test.
61
+ *
62
+ * @param displayPath - repo-relative path, as the panel shows it.
63
+ * @param hunk - one hunk of {@link diffText}.
64
+ */
65
+ export declare function hunkPatch(displayPath: string, hunk: TextHunk): string;
@@ -0,0 +1,125 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ /**
3
+ * The bits of an editor's view state that must survive an unmount.
4
+ *
5
+ * Switching file tabs remounts `CodeEditor` (its `key` is the tab id, so an
6
+ * identity change gets a fresh buffer rather than inheriting the previous
7
+ * file's undo history). Without carrying this across, every tab switch threw
8
+ * the reader back to line 1 with the caret at the start.
9
+ */
10
+ export interface CodeEditorViewState {
11
+ /** Scroll offset of the code scroller, in pixels. */
12
+ scrollTop: number;
13
+ /** Scroll offset of the code scroller, in pixels. */
14
+ scrollLeft: number;
15
+ /** Caret position as a document offset. */
16
+ cursor: number;
17
+ /** All selection ranges (multi-cursor survives a tab switch too). */
18
+ ranges: {
19
+ anchor: number;
20
+ head: number;
21
+ }[];
22
+ }
23
+ export interface CodeEditorHandle {
24
+ getContent: () => string;
25
+ isDirty: () => boolean;
26
+ markSaved: () => void;
27
+ /** Snapshot scroll + selection, for restoring after a remount. */
28
+ getViewState: () => CodeEditorViewState | undefined;
29
+ /** Scroll back and put the caret where it was. */
30
+ applyViewState: (state: CodeEditorViewState) => void;
31
+ /** 选中一个行区间(1-based 行号)并滚到视野中央。 */
32
+ revealRange: (from: number, to: number) => void;
33
+ /**
34
+ * 改动标记是否已被编辑作废(true = 屏幕上不再画任何改动点)。
35
+ *
36
+ * 暴露成独立的一个布尔查询、而不是把 `EditorView` 交出去:外部唯一需要知道的
37
+ * 就是"这份数据还准不准",拿到了整个 view 则能改文档、改配置,是另一回事。
38
+ */
39
+ getChangeStale: () => boolean;
40
+ }
41
+ import { type ChangeMark, type ChangeSet } from './vsc/change-marks.ts';
42
+ /**
43
+ * The editor surface is OPAQUE and scrolls in the editor's own palette
44
+ * (`vsc/editor-theme.ts`): a code editor is not a translucent DSH panel, and
45
+ * the VS Code theme is what gives the tokens their readable colors.
46
+ */
47
+ export declare const CodeEditor: import("react").ForwardRefExoticComponent<{
48
+ initial: string;
49
+ path: string;
50
+ readOnly?: boolean;
51
+ /** Parent sub-tab visibility (views stay mounted; remeasure on show). */
52
+ visible?: boolean;
53
+ /** Chat-insert target: absent/disabled hides the selection popup. */
54
+ ctx?: Context;
55
+ sessionId?: string | undefined;
56
+ remoteRoot?: string | undefined;
57
+ chatEnabled?: boolean;
58
+ addLabel?: string;
59
+ /** 1-based line to reveal after mount (a content-search hit). */
60
+ revealLine?: number;
61
+ /** Bumped per reveal request, so the same line can be jumped to twice. */
62
+ revealNonce?: number;
63
+ /**
64
+ * 本会话在这个文件里的改动(行号 1-based)。`undefined` = 不开启改动标记:
65
+ * 不装扩展、不出现 gutter —— 没传这个 prop 的调用方(diff 面板等)行为与
66
+ * 加入本功能之前完全一致。
67
+ */
68
+ changeSet?: ChangeSet | null;
69
+ /**
70
+ * 用户在某一处改动上按下「撤销这处改动」时回调。
71
+ *
72
+ * 收到的是 `changeSet.marks` 里的那个对象(`hunk` 是 `changeSet.patches` 的下标)。
73
+ * 不传就不装悬浮撤销那一整套(`undoAffordance`),编辑器行为与没有这个功能时
74
+ * 完全一致 —— 也就不会点出一个没人接的动作。
75
+ */
76
+ onUndoChange?: (mark: ChangeMark) => void;
77
+ /** 悬浮按钮的文案(由调用方按 `kind` 给出它的 i18n 说法)。 */
78
+ undoLabel?: (mark: ChangeMark) => string;
79
+ /** 改动点跳转的目标行(1-based),配合 `revealHunkNonce` 重跳同一行。 */
80
+ revealHunkLine?: number;
81
+ /** 改动点跳转的 nonce(同 `revealNonce` 的道理)。 */
82
+ revealHunkNonce?: number;
83
+ /**
84
+ * 用户按下「上一个/下一个改动点」快捷键时回调。
85
+ *
86
+ * 列表算在 store 那边(`gotoChange`),编辑器只转发方向:组件内自己维护一份
87
+ * hunk 列表会与 tab 上的 `activeHunk` 漂成两份。
88
+ */
89
+ onStepHunk?: (dir: 1 | -1) => void;
90
+ /**
91
+ * 文档被编辑、改动标记已作废时回调一次(false→true 的跳变)。
92
+ *
93
+ * 用回调而不是让上层轮询 handle:这件事发生在一次按键里,轮询会漏。
94
+ */
95
+ onChangeStale?: () => void;
96
+ /** Reports how many token spans the grammar produced (diagnostics). */
97
+ onTokens?: (count: number) => void;
98
+ /**
99
+ * Reports this editor's scroll + caret as they change, and once more when it
100
+ * unmounts. The FIRST argument is this editor's own tab id, so the consumer
101
+ * never has to guess which tab a reading belongs to.
102
+ *
103
+ * The editor reports it, rather than the parent asking through the ref,
104
+ * because the parent's ref is already `null` by the time its layout-effect
105
+ * cleanup runs — React clears a child's ref before running the parent's
106
+ * cleanup. Only this component still holds the `EditorView` at that moment.
107
+ */
108
+ onViewState?: (tabId: string, state: CodeEditorViewState) => void;
109
+ /**
110
+ * This editor's tab id, echoed back with every report (see `onViewState`).
111
+ * Optional because the diff panes mount `CodeEditor` too and have no tab of
112
+ * their own; they pass no `onViewState`, so nothing is reported for them.
113
+ */
114
+ tabId?: string;
115
+ /**
116
+ * Scroll + caret to restore after this instance mounts.
117
+ *
118
+ * Read ONCE on mount (through a ref, so a later change to the prop cannot
119
+ * re-scroll a live editor out from under the reader). Used to come back to
120
+ * where a file was when its tab is re-selected.
121
+ */
122
+ viewState?: CodeEditorViewState;
123
+ onSave: () => void;
124
+ onDirty: (dirty: boolean) => void;
125
+ } & import("react").RefAttributes<CodeEditorHandle>>;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Small inline input dialog (new file / folder / rename). Rendered as a
3
+ * fixed overlay card; Enter confirms, Escape cancels.
4
+ */
5
+ import { type ReactNode } from 'react';
6
+ export declare function InputDialog(props: {
7
+ title: string;
8
+ label: string;
9
+ initial?: string;
10
+ placeholder?: string;
11
+ okText?: string;
12
+ onOk: (value: string) => void;
13
+ onCancel: () => void;
14
+ }): ReactNode;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Phase 4 native directory-flow occupant: a Local/Remote tabbed dialog that
3
+ * shadows the stock picker (registered at priority -1 into both
4
+ * directory-flow holes).
5
+ *
6
+ * - Local leg: dual-mode. When the Host serves the browse capability, a
7
+ * compact in-app browser drives uiWorkspace.listDirectory/createDirectory.
8
+ * When it serves native (this Windows machine), one button opens the OS
9
+ * chooser via uiWorkspace.pickDirectory. The mode is probed once per open:
10
+ * a working list() means browse, `directory-picker/unavailable` means
11
+ * native, anything else is an inline error with retry.
12
+ * - Remote leg: the shared WorkspaceWizard; on save the materialized alias
13
+ * path goes to onPicked and the owner adopts it as a real workspace
14
+ * (its title was already set to `<server> > <base>` by syncWorkspaces).
15
+ *
16
+ * The occupant never drives the owner's onError arm: every failure stays
17
+ * inside the dialog (stock BrowseDirectoryFlow does the same), and Cancel is
18
+ * always available. If this entry ever crashes, slot shadowing retires it and
19
+ * the stock picker renders (graceful fallback, no dead surface).
20
+ */
21
+ import { type ReactNode } from 'react';
22
+ import type { DirectoryFlowOwnerProps } from '@deepseek-ai/dsh-client-ui-workspace/client';
23
+ import type { DirectoryListing } from '@deepseek-ai/dsh-host-directory-picker/types';
24
+ /** Injected face: uiWorkspace directory primitives (bound in apply's closure). */
25
+ export interface DirectoryFlowInjected {
26
+ listDirectory: (path?: string, signal?: AbortSignal) => Promise<DirectoryListing>;
27
+ createDirectory: (path: string, name: string) => Promise<string>;
28
+ pickDirectory: () => Promise<string | null>;
29
+ }
30
+ export declare function RemoteDirectoryFlow(props: DirectoryFlowOwnerProps & DirectoryFlowInjected): ReactNode;