feat: per-change-block hunk actions (#50), codex lanes without a pipeline, macOS LaunchAgent
- git (#50): stage-hunk/discard-hunk take an optional `block` — one run of consecutive +/- lines inside an @@ hunk. `onlyBlock` (server/lib/git-diff.js) cuts git's fresh hunk down to it, folding the other blocks into the side being checked; LaneDiffView hangs one review bar per block (unified + split) and its arrows walk blocks. - git exclude: entries AgentHub writes for a lane go to the COMMON git dir with the lane's path prefix (server/lib/git-exclude.js, used by router-apply and lane-mcp) — a linked worktree's own info/exclude is never read by git, and a subdir lane has no .git. MCP sync no longer fails with ENOTDIR on a worktree lane or creates a stray .git in a non-repo lane. - codex lanes carry no pipeline: codex-ingest mirrors only idle/running, codex-nudge is removed, agents install writes nothing for codex, the Workspace hides stage/pipeline/feature pickers/PipelineMap for them, and every server start clears leftover codex stage data and the old AGENTS.md stage block. - Changes panel: a whole file opens for reading and enters the editor on a click on its text (caret at the clicked line), not automatically. - macOS: LaunchAgent start-at-login (scripts/launchd-service.js, picked by scripts/service-manager.js). - Adds the AgentHub introduction slides and their image-embedding script.
This commit is contained in:
+59
-2
@@ -851,8 +851,13 @@ async function listTree(dir, { subdir = "" } = {}) {
|
||||
* applied is always git's own text of the current tree — never a patch a
|
||||
* request carried. Null when no hunk matches (the tree moved on, or the file
|
||||
* is a secret, binary or too large to read whole).
|
||||
*
|
||||
* `block` narrows it to one CHANGE BLOCK of that hunk — a run of consecutive
|
||||
* +/- lines. git merges changes up to 2×context lines apart into one hunk, so
|
||||
* without this a single Undo/Keep acted on every nearby edit at once (#50).
|
||||
* `reverse` says which side the patch will be applied to (see `onlyBlock`).
|
||||
*/
|
||||
async function hunkPatch(dir, file, header, context) {
|
||||
async function hunkPatch(dir, file, header, context, { block, reverse = false } = {}) {
|
||||
if (!header || (await isSecretOnDisk(dir, file))) return null;
|
||||
const u = `-U${normalizeContext(context)}`;
|
||||
const raw = await readCapped(
|
||||
@@ -871,7 +876,59 @@ async function hunkPatch(dir, file, header, context) {
|
||||
.slice(at + 1)
|
||||
.split(/^(?=@@ )/m)
|
||||
.find((h) => h.slice(0, h.indexOf("\n")) === header);
|
||||
return hunk ? head + (hunk.endsWith("\n") ? hunk : `${hunk}\n`) : null;
|
||||
if (!hunk) return null;
|
||||
const body = block === undefined ? hunk : onlyBlock(hunk, block, reverse);
|
||||
return body ? head + (body.endsWith("\n") ? body : `${body}\n`) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* One hunk's text cut down to its `block`-th change block, the way an edited
|
||||
* `git add -p` hunk is: every OTHER block is folded into the side the patch
|
||||
* will be checked against, so that side stays byte-for-byte the hunk's own and
|
||||
* `git apply` finds it exactly where git put it.
|
||||
*
|
||||
* - forward (stage, applied to the index = the OLD side): another block's
|
||||
* "-" lines become context and its "+" lines are dropped;
|
||||
* - reverse (undo, `apply -R` on the working tree = the NEW side): its "+"
|
||||
* lines become context and its "-" lines are dropped.
|
||||
*
|
||||
* A "\ No newline" marker follows the line it annotates, kept or dropped with
|
||||
* it. The header's counts are recomputed; its starts do not move, because the
|
||||
* checked side is unchanged. Null when the hunk has no such block.
|
||||
*/
|
||||
function onlyBlock(hunk, block, reverse) {
|
||||
if (!Number.isInteger(block) || block < 0) return null;
|
||||
const [header, ...lines] = hunk.replace(/\n$/, "").split("\n");
|
||||
const m = header.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@(.*)$/);
|
||||
if (!m) return null;
|
||||
const out = [];
|
||||
let index = -1;
|
||||
let inBlock = false;
|
||||
let keptPrev = false;
|
||||
let oldCount = 0;
|
||||
let newCount = 0;
|
||||
for (const line of lines) {
|
||||
const mark = line[0];
|
||||
if (mark === "\\") {
|
||||
if (keptPrev) out.push(line);
|
||||
continue;
|
||||
}
|
||||
const change = mark === "+" || mark === "-";
|
||||
if (change && !inBlock) index += 1;
|
||||
inBlock = change;
|
||||
let kept = line;
|
||||
if (change && index !== block) {
|
||||
const fold = reverse ? "+" : "-";
|
||||
kept = mark === fold ? ` ${line.slice(1)}` : null;
|
||||
}
|
||||
keptPrev = kept !== null;
|
||||
if (kept === null) continue;
|
||||
out.push(kept);
|
||||
if (kept[0] !== "+") oldCount += 1;
|
||||
if (kept[0] !== "-") newCount += 1;
|
||||
}
|
||||
if (block > index) return null;
|
||||
return [`@@ -${m[1]},${oldCount} +${m[2]},${newCount} @@${m[3]}`, ...out].join("\n") + "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user