@young1lin/dsh-ui-gitworkbench 0.1.15 → 0.1.17
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/CHANGELOG.md +26 -0
- package/CHANGELOG_EN.md +26 -0
- package/README.md +30 -5
- package/README_EN.md +1 -1
- package/lib/client.js +1600 -519
- package/lib/dir-listing.js +34 -0
- package/lib/fs-remove.js +5 -36
- package/lib/index.js +233 -52
- package/lib/path-lock.js +54 -0
- package/lib/worktree.js +133 -0
- package/lib/write-checked.js +1 -1
- package/package.json +1 -1
- package/src/client/ChromeGlyph.tsx +5 -0
- package/src/client/CodeEditor.tsx +19 -1
- package/src/client/DiffViews.tsx +122 -123
- package/src/client/FileBrowser.tsx +196 -23
- package/src/client/GitWorkbenchPanel.module.css +1 -0
- package/src/client/GitWorkbenchPanel.tsx +114 -12
- package/src/client/SideRails.tsx +106 -0
- package/src/client/diff-cells.tsx +147 -0
- package/src/client/diff-model.ts +20 -0
- package/src/client/diff-nav.ts +4 -1
- package/src/client/dir-tree.ts +31 -1
- package/src/client/file-rows.ts +40 -0
- package/src/client/h-rail.ts +70 -0
- package/src/client/ignored-cache.ts +193 -0
- package/src/client/index.ts +22 -3
- package/src/client/locales.ts +18 -4
- package/src/client/row-heights.ts +225 -0
- package/src/client/styles/changes.css +33 -2
- package/src/client/styles/controls.css +5 -0
- package/src/client/styles/files.css +5 -0
- package/src/client/styles/rails.css +72 -0
- package/src/client/use-row-window.ts +7 -3
- package/src/client/use-variable-row-window.ts +210 -0
- package/src/dir-listing.ts +47 -0
- package/src/fs-remove.ts +5 -36
- package/src/index.ts +257 -55
- package/src/path-lock.ts +56 -0
- package/src/types/dsh-shim.d.ts +12 -2
- package/src/worktree.ts +153 -0
- package/src/write-checked.ts +1 -1
package/src/worktree.ts
CHANGED
|
@@ -183,10 +183,163 @@ export function isRefName(ref: string): boolean {
|
|
|
183
183
|
&& REF_CHARS.test(ref)
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
/**
|
|
187
|
+
* The extra rules an ENTER branch must satisfy beyond {@link isRefName}.
|
|
188
|
+
*
|
|
189
|
+
* isRefName guards untrusted refs that reach git as positional arguments
|
|
190
|
+
* (leading `-`, `..`, alien characters). A branch worktreeEnter CREATES has
|
|
191
|
+
* two more classes of trouble: spellings check-ref-format refuses that
|
|
192
|
+
* REF_CHARS happens to pass (a leading or trailing dot, a `.lock` ending),
|
|
193
|
+
* and `head` — a legal ref on Linux that collides with HEAD on the
|
|
194
|
+
* case-insensitive filesystems the host runs on.
|
|
195
|
+
*/
|
|
196
|
+
function isEnterBranch(branch: string): boolean {
|
|
197
|
+
return isRefName(branch)
|
|
198
|
+
&& !branch.startsWith('.')
|
|
199
|
+
&& !branch.endsWith('.')
|
|
200
|
+
&& !branch.endsWith('.lock')
|
|
201
|
+
&& branch.toLowerCase() !== 'head'
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Decide the branch a worktreeEnter call lands on.
|
|
206
|
+
*
|
|
207
|
+
* The worktree NAME is the identity knob — the directory under
|
|
208
|
+
* `.agents/worktrees/` — and doubles as the branch by default (the old
|
|
209
|
+
* contract, kept for callers that pass no branchName). branchName splits the
|
|
210
|
+
* two for the one thing the name can never express: a SLASH branch
|
|
211
|
+
* (`feature/foo` is a legal ref and an impossible Windows directory).
|
|
212
|
+
*
|
|
213
|
+
* Reuse keeps the registered worktree's own branch, request or no request;
|
|
214
|
+
* an explicit branchName it displaces is reported as branchOverridden so the
|
|
215
|
+
* hint can say so — refusing there would break enter's idempotency (the same
|
|
216
|
+
* call re-issued must rebind, not explode).
|
|
217
|
+
*
|
|
218
|
+
* An illegal branchName is REFUSED, never substituted: the worktree name may
|
|
219
|
+
* be auto-generated because a directory label is arbitrary, but a branch is
|
|
220
|
+
* semantic — silently renaming it lands work on the wrong branch.
|
|
221
|
+
* @param wtName - sanitized worktree name (the default branch).
|
|
222
|
+
* @param branchName - caller-requested branch, or undefined for the default.
|
|
223
|
+
* @param existingBranch - the registered worktree's own branch when the
|
|
224
|
+
* target directory already holds one, else undefined.
|
|
225
|
+
* @returns the branch to create or keep, plus whether an explicit request
|
|
226
|
+
* was set aside — or the refusal error.
|
|
227
|
+
*/
|
|
228
|
+
export function resolveEnterBranch(
|
|
229
|
+
wtName: string,
|
|
230
|
+
branchName: string | undefined,
|
|
231
|
+
existingBranch: string | undefined,
|
|
232
|
+
): { ok: true; branch: string; branchOverridden: boolean } | { ok: false; error: string } {
|
|
233
|
+
if (branchName !== undefined && !isEnterBranch(branchName)) {
|
|
234
|
+
return { ok: false, error: 'branchName is not a valid branch name' }
|
|
235
|
+
}
|
|
236
|
+
if (existingBranch !== undefined) {
|
|
237
|
+
return { ok: true, branch: existingBranch, branchOverridden: branchName !== undefined && branchName !== existingBranch }
|
|
238
|
+
}
|
|
239
|
+
return { ok: true, branch: branchName ?? wtName, branchOverridden: false }
|
|
240
|
+
}
|
|
241
|
+
|
|
186
242
|
export function worktreeDir(repoRoot: string, name: string): string {
|
|
187
243
|
return `${repoRoot.replace(/\/+$/, '')}/.agents/worktrees/${name}`
|
|
188
244
|
}
|
|
189
245
|
|
|
246
|
+
// ---- session lineage: the binding a session effectively works under ----
|
|
247
|
+
|
|
248
|
+
/** A binding resolved for a session, with whether it is the session's own. */
|
|
249
|
+
export interface EffectiveBinding<T> {
|
|
250
|
+
readonly binding: T
|
|
251
|
+
/** False when the binding is the session's own; true when an ancestor's. */
|
|
252
|
+
readonly inherited: boolean
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Deepest ancestor chain walked before the lookup gives up. Real delegation
|
|
257
|
+
* nests two or three levels; the cap exists so a corrupt lineage (a cycle the
|
|
258
|
+
* guard below somehow missed, a pathologically deep chain) costs a bounded
|
|
259
|
+
* number of lookups instead of walking forever.
|
|
260
|
+
*/
|
|
261
|
+
const LINEAGE_HOP_CAP = 8
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Resolve the binding a session effectively works under: its own, else the
|
|
265
|
+
* nearest ancestor's.
|
|
266
|
+
*
|
|
267
|
+
* A subagent session never gets a binding of its own — `worktree_enter` is
|
|
268
|
+
* called by the session that wants the worktree — but it works wherever its
|
|
269
|
+
* parent conversation works: the standing prompt, the chip, and `worktree_exit`'s
|
|
270
|
+
* diagnostics all answer "which worktree is THIS session in" through here. The
|
|
271
|
+
* walk is re-resolved on every read, so a session exiting its worktree changes
|
|
272
|
+
* only its own binding: descendants lend the next bound ancestor up the chain
|
|
273
|
+
* on their next read (possibly none — the common case — possibly a grandparent's,
|
|
274
|
+
* which is still the conversation tree they work in) and nothing dangles.
|
|
275
|
+
*
|
|
276
|
+
* Own wins over inherited on purpose: a session that enters a worktree of its
|
|
277
|
+
* own is deliberately somewhere else than its parent.
|
|
278
|
+
* @param sessionId - the session whose effective binding is wanted.
|
|
279
|
+
* @param parentOf - session id → parent session id, as `agent/session-start`
|
|
280
|
+
* delivered it (subagent headers name their parent).
|
|
281
|
+
* @param bindingOf - binding lookup (the bindings file, or the prompt mirror).
|
|
282
|
+
* @returns the effective binding, or undefined when neither the session nor any
|
|
283
|
+
* ancestor (within the hop cap) is bound.
|
|
284
|
+
*/
|
|
285
|
+
export function resolveEffectiveBinding<T>(
|
|
286
|
+
sessionId: string,
|
|
287
|
+
parentOf: ReadonlyMap<string, string>,
|
|
288
|
+
bindingOf: (id: string) => T | undefined,
|
|
289
|
+
): EffectiveBinding<T> | undefined {
|
|
290
|
+
const own = bindingOf(sessionId)
|
|
291
|
+
if (own !== undefined) return { binding: own, inherited: false }
|
|
292
|
+
const seen = new Set<string>([sessionId])
|
|
293
|
+
let ancestor = parentOf.get(sessionId)
|
|
294
|
+
for (let hops = 0; ancestor !== undefined && hops < LINEAGE_HOP_CAP; hops += 1) {
|
|
295
|
+
if (seen.has(ancestor)) return undefined
|
|
296
|
+
seen.add(ancestor)
|
|
297
|
+
const binding = bindingOf(ancestor)
|
|
298
|
+
if (binding !== undefined) return { binding, inherited: true }
|
|
299
|
+
ancestor = parentOf.get(ancestor)
|
|
300
|
+
}
|
|
301
|
+
return undefined
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* The session's parent edge, read off a dsh session header: the id of the
|
|
306
|
+
* session this one was delegated by, or undefined for a top-level session (or
|
|
307
|
+
* a malformed empty value). Both `parentOf` feeds — the `agent/session-start`
|
|
308
|
+
* listener and the prompt-time self-heal — go through here, so their input
|
|
309
|
+
* guards cannot drift apart.
|
|
310
|
+
*/
|
|
311
|
+
export function lineageEdgeOf(header: { readonly parentSession?: string } | undefined): string | undefined {
|
|
312
|
+
const parent = header?.parentSession
|
|
313
|
+
return typeof parent === 'string' && parent.length > 0 ? parent : undefined
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The standing notice for a session's effective binding — the text the
|
|
318
|
+
* `worktree:binding` prompt context returns. Both variants carry the same two
|
|
319
|
+
* operational rules; what differs is who holds the binding, and the inherited
|
|
320
|
+
* variant must NOT offer `worktree_exit` (the caller cannot unbind a parent's
|
|
321
|
+
* binding — the exit would fail, and the model should not be told to try).
|
|
322
|
+
* @param name - worktree name (also the directory under `.agents/worktrees/`).
|
|
323
|
+
* @param branch - branch checked out there, when known.
|
|
324
|
+
* @param inherited - whether an ancestor, not this session, holds the binding.
|
|
325
|
+
*/
|
|
326
|
+
export function bindingNotice(name: string, branch: string | undefined, inherited: boolean): string {
|
|
327
|
+
const rel = `.agents/worktrees/${name}`
|
|
328
|
+
const branchNote = branch === undefined ? '' : ` (branch ${branch})`
|
|
329
|
+
const opening = inherited
|
|
330
|
+
? `This session works in git worktree "${name}"${branchNote}, entered by its parent session.`
|
|
331
|
+
: `This session is bound to git worktree "${name}"${branchNote}.`
|
|
332
|
+
const closing = inherited
|
|
333
|
+
? 'A path without that prefix acts on the MAIN worktree, not the worktree this conversation works in. '
|
|
334
|
+
+ '(The binding belongs to the parent session; worktree_exit here would not unbind it.)'
|
|
335
|
+
: 'A path without that prefix acts on the MAIN worktree, not the bound one. Call worktree_exit to unbind.'
|
|
336
|
+
return `${opening}\n`
|
|
337
|
+
+ 'The session working directory is still the repository root, so the binding is a convention you must apply yourself:\n'
|
|
338
|
+
+ `- shell commands: pass workdir "${rel}"\n`
|
|
339
|
+
+ `- file tools: prefix every path with ${rel}/\n`
|
|
340
|
+
+ closing
|
|
341
|
+
}
|
|
342
|
+
|
|
190
343
|
export function parseWorktreeList(porcelain: string): WorktreeEntry[] {
|
|
191
344
|
const out: WorktreeEntry[] = []
|
|
192
345
|
let path = ''
|
package/src/write-checked.ts
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
import { randomBytes } from 'node:crypto'
|
|
34
34
|
|
|
35
35
|
import { renameWithRetry } from './atomic-json.js'
|
|
36
|
-
import { resolveInside } from './
|
|
36
|
+
import { resolveInside } from './path-lock.js'
|
|
37
37
|
import { decodesAsUtf8, isSafePathArg, type OpFailure } from './git-ops.js'
|
|
38
38
|
import type { GitRun } from './apply-blocks.js'
|
|
39
39
|
|