dsh-gitbar-lite 0.0.0-stage → 0.1.0
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 +274 -0
- package/README.md +246 -2
- package/cordis.patch.yml +19 -0
- package/docs/CONTRACT.md +394 -0
- package/icon.svg +15 -0
- package/lib/client.js +2154 -0
- package/lib/host/diff.js +216 -0
- package/lib/host/git.js +211 -0
- package/lib/host/parse.js +267 -0
- package/lib/host/repo.js +729 -0
- package/lib/host/routes.js +448 -0
- package/lib/host/store.js +360 -0
- package/lib/host/workspace.js +84 -0
- package/lib/index.js +160 -0
- package/lib/shared.js +125 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +88 -4
package/docs/CONTRACT.md
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
# dsh-gitbar-lite — implementation contract
|
|
2
|
+
|
|
3
|
+
This document is the part of the design that is *not* visible from the outside:
|
|
4
|
+
the wire between the two halves, where state lives, the invariants the code is
|
|
5
|
+
built around, and — just as importantly — what has **not** been verified.
|
|
6
|
+
|
|
7
|
+
It is written for whoever changes this plugin next, including a future me who has
|
|
8
|
+
forgotten why the discard order is what it is.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. What this is, and the shape of the split
|
|
13
|
+
|
|
14
|
+
A GitHub Desktop-shaped Git panel that lives in DSH's built-in right sidebar.
|
|
15
|
+
Three tab types, all session-scoped because that is what the right column is:
|
|
16
|
+
|
|
17
|
+
| Tab | Kind | Address | Opened by |
|
|
18
|
+
|---|---|---|---|
|
|
19
|
+
| The panel | `gitbar` | page (`sidebar://gitbar`) | the guide card in the right column |
|
|
20
|
+
| The trash | `gitbar-trash` | page (`sidebar://gitbar-trash`) | a button in the panel |
|
|
21
|
+
| A diff | `gitbar-diff` | **resource** (`dsh-resource://dsh-gitbar-lite/diff/current`) | double-clicking a file, or a trash entry |
|
|
22
|
+
|
|
23
|
+
The split between the halves is total: the browser half never touches the disk,
|
|
24
|
+
never shells out, and never sees a path it did not get from the host.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
right sidebar tab (browser) host process
|
|
28
|
+
────────────────────────── ─────────────
|
|
29
|
+
GitPanel / TrashBody / DiffBody
|
|
30
|
+
│
|
|
31
|
+
│ POST /api/dsh-gitbar-lite/rpc
|
|
32
|
+
│ { action, ...payload } connection.fetch.register
|
|
33
|
+
▼ → admit() (fence + cookie)
|
|
34
|
+
createRoute.fetch ─────────────────────► → createHandlers[action]
|
|
35
|
+
│
|
|
36
|
+
├─ git.js ctx.subprocess
|
|
37
|
+
├─ repo.js the operations
|
|
38
|
+
└─ store.js <DSH_HOME>/dsh-gitbar-lite
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Why one route and not one action per route.** `connection.fetch` matches on the
|
|
42
|
+
exact pathname and method, and every call already carries a `sessionId` plus the
|
|
43
|
+
few fields the action needs. One `POST` with an `action` field keeps the wire
|
|
44
|
+
contract in one place — mirrored verbatim in the browser half — instead of
|
|
45
|
+
spreading fifteen path literals across two languages.
|
|
46
|
+
|
|
47
|
+
**Why it must be `connection.fetch` and never `webServer.register`.** The web
|
|
48
|
+
server resolves `kind: 'exact'` routes *before* its `/api` prefix route, and
|
|
49
|
+
everything that makes an `/api` request safe hangs off that prefix route:
|
|
50
|
+
`connection.admit(req)` runs the Host/Origin fence and the browser-cookie check
|
|
51
|
+
before `bridge()` dispatches. An exact route on the same path shadows all of it,
|
|
52
|
+
turning this into an unauthenticated git surface reachable by anything that can
|
|
53
|
+
guess the path. `test/index.test.mjs` and `test/check.mjs` both assert the
|
|
54
|
+
carrier, and the check script proves its own assertion is sensitive by running
|
|
55
|
+
the predicate against a deliberately broken copy of the source.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 2. The wire contract
|
|
60
|
+
|
|
61
|
+
`POST /api/dsh-gitbar-lite/rpc`, body `{ action, ...payload }`, `requestBody:
|
|
62
|
+
'buffered'`.
|
|
63
|
+
|
|
64
|
+
Every response is JSON with `cache-control: no-store`. Success is
|
|
65
|
+
`{ ok: true, ...data }`. Failure is `{ ok: false, error: { code, message } }`
|
|
66
|
+
**and** a meaningful HTTP status — both, on purpose, because the browser half
|
|
67
|
+
reads the body and a non-2xx status must not be the only signal. The `message`
|
|
68
|
+
the host sends is already user-facing (Chinese), and the browser half localises
|
|
69
|
+
`code` when it recognises it, falling back to that message.
|
|
70
|
+
|
|
71
|
+
### Actions
|
|
72
|
+
|
|
73
|
+
| action | payload | returns |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| `status` | `sessionId` | the whole panel state (see below), or the picker's candidate list when nothing is bound |
|
|
76
|
+
| `bind` | `sessionId`, `repoRoot: string \| null` | `{ repoRoot, bound }` or `{ cleared: true }` |
|
|
77
|
+
| `repos` | `sessionId` | `{ candidates, scanned, scanTruncated, repoRoot, source }` |
|
|
78
|
+
| `stage` | `sessionId`, `paths: string[]`, `staged: boolean` | fresh status |
|
|
79
|
+
| `commit` | `sessionId`, `summary`, `description?` | `{ commit, ...status }` |
|
|
80
|
+
| `commitAndPush` | same | `{ commit, push: { ok, ... } , ...status }` |
|
|
81
|
+
| `push` | `sessionId` | `{ action: 'push', outcome, ...status }` |
|
|
82
|
+
| `fetch` | `sessionId` | `{ action: 'fetch', outcome, ...status }` |
|
|
83
|
+
| `discard` | `sessionId`, `paths: string[] \| null`, `hardDelete` | `{ discard, ...status }` |
|
|
84
|
+
| `fileDiff` | `sessionId`, `path` | `{ diff }` |
|
|
85
|
+
| `trashList` | `sessionId`, `all?` | `{ entries, total, showAll, repoRoot }` |
|
|
86
|
+
| `trashDiff` | `id` | `{ entry, text }` |
|
|
87
|
+
| `trashDelete` | `id` | `{ removed }` |
|
|
88
|
+
| `trashClear` | `sessionId`, `all?` | `{ removed }` |
|
|
89
|
+
| `prefs` | `sessionId`, `trashHardDelete?`, `trashShowAllRepos?` | `{ prefs }` |
|
|
90
|
+
|
|
91
|
+
`paths: null` on `discard` means *every changed file*; an explicit array means
|
|
92
|
+
exactly those paths. An array that matches nothing is an `E_BAD_REQUEST`, and a
|
|
93
|
+
clean working tree is refused too, so "discard nothing" never writes an empty
|
|
94
|
+
archive.
|
|
95
|
+
|
|
96
|
+
### The status payload
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
{
|
|
100
|
+
workspacePath, workspaceTitle, source: 'binding' | 'detected' | 'none',
|
|
101
|
+
repoRoot, repoName, head: { oid, short, subject },
|
|
102
|
+
branch: { oid, head, upstream, ahead, behind, detached, unborn },
|
|
103
|
+
remotes: string[],
|
|
104
|
+
files: [{ path, origPath?, index, worktree, kind, untracked, conflicted,
|
|
105
|
+
staged, partial, added, deleted, binary, synthesized? }],
|
|
106
|
+
counts: { files, staged, partial, conflicted, additions, deletions },
|
|
107
|
+
primary: 'push' | 'publish' | 'fetch',
|
|
108
|
+
prefs: { trashHardDelete, trashShowAllRepos }
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`head` is the commit a discarded diff would be taken *against*, which is why the
|
|
113
|
+
trash can say "against `a1b2c3d` *init*" and mean it.
|
|
114
|
+
|
|
115
|
+
### Failure codes
|
|
116
|
+
|
|
117
|
+
`E_BAD_REQUEST` · `E_UNKNOWN_ACTION` · `E_NO_WORKSPACE` · `E_NO_REPO` ·
|
|
118
|
+
`E_NO_GIT` · `E_EMPTY_SUMMARY` · `E_NOTHING_STAGED` · `E_CONFLICT` ·
|
|
119
|
+
`E_GIT_FAILED` · `E_NO_REMOTE` · `E_NO_UPSTREAM` · `E_AUTH` · `E_PUSH_REJECTED` ·
|
|
120
|
+
`E_TRASH_NOT_FOUND` · `E_INTERNAL`.
|
|
121
|
+
|
|
122
|
+
`E_AUTH` and `E_PUSH_REJECTED` exist so the panel can say the one useful
|
|
123
|
+
sentence for each instead of pasting git's stderr. Both are derived by pattern
|
|
124
|
+
matching git's output in `classifyGitFailure`, which reads **stdout as well as
|
|
125
|
+
stderr** — `git commit` with nothing staged exits 1 with an empty stderr and
|
|
126
|
+
"nothing to commit" on stdout, and a stderr-only matcher reports that as an
|
|
127
|
+
unexplained internal failure.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## 3. Where state lives
|
|
132
|
+
|
|
133
|
+
`<DSH_HOME>/dsh-gitbar-lite/`, falling back to `~/.dsh`. **Do not read
|
|
134
|
+
`$DSH_HOME` alone**: the Host process usually does not have it set (the variable
|
|
135
|
+
belongs to the shell DSH spawns), so a state file written under a bare
|
|
136
|
+
`process.env.DSH_HOME` lands somewhere else or fails to resolve at all. This is
|
|
137
|
+
the same trap `dsh-tailscale-remote` documents.
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
bindings.json { version, bindings: { <canonical workspace path>: { repoRoot, workspacePath, updatedAt } } }
|
|
141
|
+
prefs.json { version, trashHardDelete, trashShowAllRepos }
|
|
142
|
+
trash/index.json { version, entries: [ ... ] }
|
|
143
|
+
trash/<id>.diff the archived diff, one file per discard operation
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Every write goes through a temp file plus a rename, so a crash mid-write cannot
|
|
147
|
+
truncate a file, and every read-modify-write is serialized per path — two tabs
|
|
148
|
+
issuing overlapping mutations is the normal case here, not an edge case.
|
|
149
|
+
|
|
150
|
+
**Why not the settings service or a storage domain.** A binding is keyed by an
|
|
151
|
+
arbitrary absolute workspace path, and a `storageDomain` record key is restricted
|
|
152
|
+
to `^[a-zA-Z0-9_-]+$` because it becomes a path segment; `C:\Users\…\<a folder
|
|
153
|
+
with spaces>` cannot be one without hashing it, at which point the "native"
|
|
154
|
+
option is doing less than the twelve lines here. A settings `Config` would put a
|
|
155
|
+
growing map of machine paths into the user-visible settings form, which is worse.
|
|
156
|
+
|
|
157
|
+
**The binding key** is canonical, separator-normalized, and case-folded on
|
|
158
|
+
Windows (`workspaceKey`), so `C:\Foo\`, `C:\Foo` and `c:\foo` are one record. The
|
|
159
|
+
original spelling is stored alongside for display.
|
|
160
|
+
|
|
161
|
+
**A stale binding is dropped, not reported.** If the bound directory is gone, or
|
|
162
|
+
is no longer a repository, `resolveBinding` clears the record and re-detects. A
|
|
163
|
+
broken binding the user cannot clear from the UI is worse than re-deciding.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## 4. The invariant: a tick *is* the index
|
|
168
|
+
|
|
169
|
+
This is the claim that makes the plugin safe next to a terminal, and the claim a
|
|
170
|
+
pathspec-limited commit would quietly break.
|
|
171
|
+
|
|
172
|
+
- Ticking a file runs `git add -A -- :(literal)<path>`.
|
|
173
|
+
- Unticking runs `git restore --staged -- :(literal)<path>`.
|
|
174
|
+
- A file reads as ticked when **all** of its change is in the index
|
|
175
|
+
(`index !== '.'`), and as *indeterminate* when the index and the worktree are
|
|
176
|
+
both dirty (`MM`) — one tick cannot represent half a file, so the box shows a
|
|
177
|
+
dash and either click restates the file as a whole.
|
|
178
|
+
- Commit runs a plain `git commit -F -` with **no pathspec**. What the panel
|
|
179
|
+
shows, what `git status` shows, and what gets committed cannot drift apart.
|
|
180
|
+
- Untracked and `.gitignore`d files are never ticked: they are not in the index
|
|
181
|
+
and cannot be.
|
|
182
|
+
|
|
183
|
+
`:(literal)` matters: without it, discarding a file named `docs` would also reach
|
|
184
|
+
`docs/anything`, and a file named `a[1].txt` would be treated as a pattern.
|
|
185
|
+
|
|
186
|
+
The two user-visible consequences, stated plainly: the plugin *does* mutate your
|
|
187
|
+
index (that is the point — GitHub Desktop does the same), and a file you staged
|
|
188
|
+
by hand shows up ticked, because it is.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## 5. Discarding, in order, and why
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
1. read the full status ── decide the target set
|
|
196
|
+
2. refuse if any target is conflicted ── E_CONFLICT, one sentence
|
|
197
|
+
3. build the archive ── BEFORE anything is destroyed
|
|
198
|
+
tracked: git diff --no-color --find-renames HEAD -- <literal paths>
|
|
199
|
+
(+ git diff --numstat … in the same round, for the per-file table)
|
|
200
|
+
untracked: synthesizeNewFileDiff per path, from the file's own bytes
|
|
201
|
+
4. cap it on a line boundary ── limitDiff, records droppedLines
|
|
202
|
+
5. write the trash entry + blob ── unless hardDelete
|
|
203
|
+
6. restore
|
|
204
|
+
tracked: git restore --source=HEAD --staged --worktree -- <literal paths>
|
|
205
|
+
untracked: delete the file, then prune directories the discard emptied
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Steps 3 and 6 are not interchangeable. `git restore` destroys the original, so
|
|
209
|
+
the archive has to exist first; that is the whole reason the plugin has a trash.
|
|
210
|
+
|
|
211
|
+
**Why tracked and untracked are restored by different means.** A mixed
|
|
212
|
+
pathspec — one untracked path alongside tracked ones — makes the *entire*
|
|
213
|
+
`git restore` fail with `pathspec … did not match any file(s) known to git` and
|
|
214
|
+
restores nothing (verified on git 2.54). Splitting them is not tidiness; it is
|
|
215
|
+
correctness.
|
|
216
|
+
|
|
217
|
+
**Why untracked paths are deleted rather than restored.** They are not in HEAD,
|
|
218
|
+
so `git restore --source=HEAD` cannot reach them. Their content is captured in
|
|
219
|
+
step 3 or lost, which is exactly why the synthesized diff exists.
|
|
220
|
+
|
|
221
|
+
**One entry per discard operation, not one per file.** Discarding a single file
|
|
222
|
+
archives one blob with one file in it; "Discard all" archives **one blob holding
|
|
223
|
+
every file's diff**, with `scope: 'all'`. That is what was asked for, and it also
|
|
224
|
+
means a whole-tree discard is one write rather than N.
|
|
225
|
+
|
|
226
|
+
**Conflicts are refused, not resolved.** Discarding a conflicted path would mean
|
|
227
|
+
`git restore` against HEAD in the middle of a merge, which can disturb the merge
|
|
228
|
+
state. The panel says one sentence instead, and `test/e2e.mjs` S12 proves both
|
|
229
|
+
the single-file and the discard-all paths refuse.
|
|
230
|
+
|
|
231
|
+
**`hardDelete` is a per-operation choice, remembered.** The confirmation box's
|
|
232
|
+
"delete for good" starts unticked for a first-time user (the safe answer keeps a
|
|
233
|
+
copy) and is persisted after each discard, so it mirrors the user's last
|
|
234
|
+
decision rather than latching.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## 6. The trash
|
|
239
|
+
|
|
240
|
+
An entry records, so that a diff can still be read months later:
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
{ id, at, repoRoot, repoName, workspacePath, sessionId,
|
|
244
|
+
scope: 'file' | 'all',
|
|
245
|
+
branch: { head, detached },
|
|
246
|
+
base: { oid, short, subject } | null, ← the commit the diff is against
|
|
247
|
+
files: [{ path, origPath?, kind, added, deleted, binary, synthesized?, oversized? }],
|
|
248
|
+
bytes, truncated, droppedLines, blob }
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`base` is HEAD at the moment of the discard. That is the answer to "which version
|
|
252
|
+
is this a diff against" — and it is recorded rather than inferred, because HEAD
|
|
253
|
+
moves.
|
|
254
|
+
|
|
255
|
+
The list is filtered to the currently bound repository by default; `total` still
|
|
256
|
+
reports the whole index, and a toggle shows everything. Clearing with the filter
|
|
257
|
+
on clears only the visible repository's entries.
|
|
258
|
+
|
|
259
|
+
Reading an entry whose `.diff` was deleted by hand still lists it: the metadata
|
|
260
|
+
is worth keeping even when the blob is gone, and the read reports the blob as
|
|
261
|
+
missing rather than pretending the entry vanished.
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## 7. The right-sidebar tab identities
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
id: dsh-gitbar-lite/git kind: gitbar page keepMounted, one guide card (order 30)
|
|
269
|
+
id: dsh-gitbar-lite/trash kind: gitbar-trash page
|
|
270
|
+
id: dsh-gitbar-lite/diff kind: gitbar-diff resource, patterns: dsh-resource://dsh-gitbar-lite/diff/**
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
`definition.id` is **also the slot key** the body and title register under. Two
|
|
274
|
+
consequences the code leans on: a rename in one place without the other silently
|
|
275
|
+
renders nothing, and `test/client.test.mjs` asserts every slot key is one of the
|
|
276
|
+
three ids.
|
|
277
|
+
|
|
278
|
+
**Why the diff tab is a resource with a fixed address.** Re-opening a resource
|
|
279
|
+
address reveals the tab that is already open — the host looks the content id up
|
|
280
|
+
across the whole layout before planning a new tab — and the `params` payload
|
|
281
|
+
re-navigates that tab (each open bumps `navigation.revision`). A page kind would
|
|
282
|
+
reuse only within the active dock pane, so a tab the user had floated would get a
|
|
283
|
+
second copy. The fixed address *is* the "one diff tab, ever" requirement, and the
|
|
284
|
+
diff's own chip is a title seat because it has to read the live
|
|
285
|
+
`navigation.params` to name the file currently on screen — a title captured at
|
|
286
|
+
open time never changes.
|
|
287
|
+
|
|
288
|
+
**Why `keepMounted` on the panel and not the trash.** The panel holds the tick
|
|
289
|
+
state that mirrors the index and a half-written Summary; peeking at a diff must
|
|
290
|
+
not lose either. The trash holds only what it re-reads on every visit, so
|
|
291
|
+
retaining it would buy nothing.
|
|
292
|
+
|
|
293
|
+
**The panel re-reads on `tab.visible`.** Coming back to the panel refreshes
|
|
294
|
+
quietly, because the working tree may have changed under it — an agent in the
|
|
295
|
+
same session is a normal author of those changes.
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## 8. Configuration
|
|
300
|
+
|
|
301
|
+
Every field is volatile, which is what lets the settings page edit them without a
|
|
302
|
+
restart — and is why `cordis.patch.yml` deliberately carries **no** `config`
|
|
303
|
+
block: values written into the profile's own patch layer must not be shadowed by
|
|
304
|
+
this package on the next upgrade.
|
|
305
|
+
|
|
306
|
+
| field | default | why |
|
|
307
|
+
|---|---|---|
|
|
308
|
+
| `gitPath` | `''` | Empty means "whatever is on PATH". Set it when git is somewhere unusual. |
|
|
309
|
+
| `stateDir` | `''` | Empty means `<DSH_HOME>/dsh-gitbar-lite`. |
|
|
310
|
+
| `timeoutMs` | `30000` | Per-command. A push to a slow remote may need more. |
|
|
311
|
+
| `maxDiffBytes` | `524288` | Ceiling on one archived diff. A discarding user is having a bad day already; a 200 MB generated file must not become a 200 MB write that then fails. |
|
|
312
|
+
| `maxFileBytes` | `2097152` | Larger untracked files are listed but their content is not read, archived, or diffed. |
|
|
313
|
+
| `trashLimit` | `200` | Entries kept; the oldest are dropped and their blobs deleted. |
|
|
314
|
+
| `scanDepth` | `3` | Directory depth the picker scans below the workspace root. |
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## 9. Verified, and not
|
|
319
|
+
|
|
320
|
+
### Verified
|
|
321
|
+
|
|
322
|
+
| what | how |
|
|
323
|
+
|---|---|
|
|
324
|
+
| porcelain v2 parsing, including a rename, a staged delete, a conflict, a non-UTF-8-hostile path, and a path containing a space | `test/parse.test.mjs`, 47 cases; the conflict fixture pins the nine-field `u` record |
|
|
325
|
+
| diff assembly, truncation on line boundaries, per-file splitting | `test/diff.test.mjs` |
|
|
326
|
+
| state files, the binding key, corruption tolerance, write serialization | `test/store.test.mjs` |
|
|
327
|
+
| the route carrier, single registration under a hostile service order, error classification | `test/index.test.mjs` |
|
|
328
|
+
| every registration identity, the wire literals, the dictionaries, and the hook-free components' rendered trees | `test/client.test.mjs` |
|
|
329
|
+
| the whole host stack against **real git**, mutation by mutation | `test/e2e.mjs`, 224 assertions over 18 independent repositories |
|
|
330
|
+
| the manifest, the patch layer, the tarball's contents, the two copies of the wire contract, the dictionaries, the workflow | `test/check.mjs` |
|
|
331
|
+
| that the client bundle would not abort the Host boot | `scripts/check-client-bundle.mjs` |
|
|
332
|
+
|
|
333
|
+
The e2e suite is deliberately hostile about §4: after ticking one file it reads
|
|
334
|
+
`git status --porcelain` back and asserts git agrees, which is the only way to
|
|
335
|
+
show the index claim rather than assume it.
|
|
336
|
+
|
|
337
|
+
### Not verified
|
|
338
|
+
|
|
339
|
+
- **`ctx.subprocess` is doubled in the tests.** The shipped service lives inside
|
|
340
|
+
DSH's own bundle and is not importable from a plugin repository, so
|
|
341
|
+
`test/helpers/subprocess.mjs` reproduces its documented shapes (the spawn spec,
|
|
342
|
+
`collected.stdout.readFrom`, `done`) on top of `node:child_process`. It does
|
|
343
|
+
**not** reproduce the Windows Job containment or the environment scrubbing —
|
|
344
|
+
those belong to the real service. The first live run in a real DSH is what
|
|
345
|
+
closes that gap; see `test/live.mjs`.
|
|
346
|
+
- **No headless-DOM render test.** The panel, the trash and the diff are driven
|
|
347
|
+
by hooks, and the three components with no hooks in them (the row, the
|
|
348
|
+
confirmation, the picker) *are* asserted on their element trees. What is not
|
|
349
|
+
covered is real layout: whether the panel actually fills the column, whether a
|
|
350
|
+
diff row's background is the token it claims. A browser harness with a
|
|
351
|
+
hand-written React shim would be needed, and the two bugs that motivate one in
|
|
352
|
+
other plugins in this family were both DOM-measurement bugs — this plugin
|
|
353
|
+
measures nothing and augments no host DOM, so the harness was judged not worth
|
|
354
|
+
its fixture-fidelity cost. That judgement is worth revisiting if the UI grows
|
|
355
|
+
anything hover-dependent or portalled.
|
|
356
|
+
- **What a 401 from `/api` does and does not prove.** `connection.admit()` — the
|
|
357
|
+
Host/Origin fence and the browser-cookie check — runs on the `/api` prefix
|
|
358
|
+
route *before* dispatch, so an unauthenticated request to a path nobody serves
|
|
359
|
+
answers 401 as well. `test/live.mjs` therefore reports 401 as *inconclusive*
|
|
360
|
+
rather than as "mounted"; only a 200 (or, when authenticated, a 404) is
|
|
361
|
+
evidence about whether this plugin is actually registered.
|
|
362
|
+
- **`openTabIn` / `openResourceIn` are outside the published type contract.**
|
|
363
|
+
They are prototype methods on the sidebar-right controller and are called by
|
|
364
|
+
shipped plugins (including `dsh-deepseek-webchat`), but three source comments
|
|
365
|
+
label them "not part of `ISidebarRight`". This is the first thing to re-check
|
|
366
|
+
on a DSH upgrade.
|
|
367
|
+
- **`@deepseek-ai/dsh-client-ui-dockkit` was not available to read**, so the tab
|
|
368
|
+
strip's height and the pane's padding could not be confirmed. The styles
|
|
369
|
+
therefore add no top padding and claim only `height: 100%; min-height: 0`, so
|
|
370
|
+
they fill whatever the kit provides instead of guessing.
|
|
371
|
+
- **Git's path encoding on this machine.** `core.quotepath=false` plus `-z`
|
|
372
|
+
produced UTF-8 paths in every fixture that was checked (including a non-ASCII
|
|
373
|
+
name), and the parsers split on NUL rather than on quoting. A repository whose
|
|
374
|
+
paths git emits in another encoding is untested.
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## 10. Known limitations
|
|
379
|
+
|
|
380
|
+
- One repository per workspace, one workspace per binding. No submodules, no
|
|
381
|
+
worktrees, no multiple remotes beyond "prefer `origin`, else the first".
|
|
382
|
+
- Push and Fetch always act on a single remote. There is no branch switching, no
|
|
383
|
+
history list, and no stash — that was the agreed scope.
|
|
384
|
+
- `git fetch --prune` is what Fetch does, matching GitHub Desktop. It deletes
|
|
385
|
+
remote-tracking refs whose branch the remote no longer has.
|
|
386
|
+
- Commit hooks run. A hook that blocks or hangs is the user's own hook; the child
|
|
387
|
+
is killed at `graceMs` only if the command times out, and `timeoutMs` is the
|
|
388
|
+
knob for that.
|
|
389
|
+
- The trash keeps the diffs, not the files. Recovery means applying the archived
|
|
390
|
+
diff by hand; the plugin does not offer a restore button, by agreement.
|
|
391
|
+
- Binary content is not archived — the diff says `Binary files … differ`, which
|
|
392
|
+
is honest but is not a backup.
|
|
393
|
+
- An untracked file over `maxFileBytes` is deleted with only a stub in the
|
|
394
|
+
archive. The confirmation box does not yet warn about that specifically.
|
package/icon.svg
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Git bar">
|
|
2
|
+
<title>Git bar</title>
|
|
3
|
+
<!--
|
|
4
|
+
The branch mark the panel itself draws, at the plugin-list size.
|
|
5
|
+
Same 16-grid geometry as the host's IconBranchOutline artwork, so the icon in
|
|
6
|
+
the plugin list and the icon on the tab chip are recognisably one mark;
|
|
7
|
+
stroke-width is raised because this one is rendered at 24px rather than 16.
|
|
8
|
+
-->
|
|
9
|
+
<g stroke="#4F6BED" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round">
|
|
10
|
+
<path d="M1.01503 8.0001L5.6964 8.0001C6.41913 8.0001 6.78049 8.0001 7.12115 7.91951C7.4232 7.84804 7.71233 7.73014 7.97821 7.57C8.27809 7.38939 8.5364 7.13669 9.05303 6.63129L11.3281 4.40564" />
|
|
11
|
+
<path d="M1.01221 7.9999L5.6964 7.9999C6.41913 7.9999 6.78049 7.9999 7.12115 8.08049C7.4232 8.15196 7.71233 8.26986 7.97821 8.43C8.27809 8.61061 8.5364 8.86331 9.05303 9.36871L11.3281 11.5944" />
|
|
12
|
+
<circle cx="12.4502" cy="3.3079" r="1.56962" />
|
|
13
|
+
<circle cx="12.4502" cy="12.6921" r="1.56962" />
|
|
14
|
+
</g>
|
|
15
|
+
</svg>
|