Skip to content

Commit e1c3d47

Browse files
jonathanlabclaude
andcommitted
fix: arr top now creates empty @ above stack for new work
Previously arr top just navigated to the topmost described change. Now it creates an empty @ above the stack (if not already there), matching the STORIES.md expectation: "Empty @ above stack top". Uses jj directly to check if @ is empty/undescribed with no children. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent f881b97 commit e1c3d47

File tree

1 file changed

+64
-11
lines changed

1 file changed

+64
-11
lines changed

packages/core/src/jj.ts

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -804,30 +804,83 @@ export class JJ {
804804
}
805805

806806
async navigateTop(): Promise<Result<NavigationResult>> {
807-
// Use heads(descendants(@)) to find the tip of the current stack
807+
// Check if @ is empty, undescribed, and has no children (already at top ready for work)
808+
const wcResult = await this.run([
809+
"log",
810+
"-r",
811+
"@",
812+
"--no-graph",
813+
"-T",
814+
'empty ++ "\\t" ++ description.first_line()',
815+
]);
816+
const childrenResult = await this.run([
817+
"log",
818+
"-r",
819+
"@+",
820+
"--no-graph",
821+
"-T",
822+
"change_id.short()",
823+
]);
824+
825+
if (wcResult.ok && childrenResult.ok) {
826+
const [empty, desc = ""] = wcResult.value.stdout.trim().split("\t");
827+
const hasChildren = childrenResult.value.stdout.trim() !== "";
828+
829+
if (empty === "true" && desc === "" && !hasChildren) {
830+
// Already at top with empty @, return parent info
831+
const parentResult = await this.run([
832+
"log",
833+
"-r",
834+
"@-",
835+
"--no-graph",
836+
"-T",
837+
'change_id.short() ++ "\\t" ++ description.first_line()',
838+
]);
839+
if (parentResult.ok) {
840+
const [changeId, description] = parentResult.value.stdout
841+
.trim()
842+
.split("\t");
843+
return ok({ changeId, description: description || "" });
844+
}
845+
}
846+
}
847+
848+
// Navigate to the head of the stack
808849
const editResult = await this.run(["edit", "heads(descendants(@))"]);
809850
if (!editResult.ok) {
810851
if (editResult.error.message.includes("No descendant")) {
811-
return err(createError("NAVIGATION_FAILED", "Already at top of stack"));
812-
}
813-
if (editResult.error.message.includes("more than one revision")) {
852+
// Already at head - fall through to create empty @
853+
} else if (editResult.error.message.includes("more than one revision")) {
814854
return err(
815855
createError(
816856
"NAVIGATION_FAILED",
817857
"Stack has multiple heads - cannot determine top",
818858
),
819859
);
860+
} else {
861+
return editResult;
820862
}
821-
return editResult;
822863
}
823864

824-
const status = await this.status();
825-
if (!status.ok) return status;
865+
// Create a new empty change above the head for new work
866+
const newResult = await this.run(["new"]);
867+
if (!newResult.ok) return newResult;
826868

827-
return ok({
828-
changeId: status.value.workingCopy.changeId,
829-
description: status.value.workingCopy.description,
830-
});
869+
// Get parent info (the actual stack top)
870+
const parentResult = await this.run([
871+
"log",
872+
"-r",
873+
"@-",
874+
"--no-graph",
875+
"-T",
876+
'change_id.short() ++ "\\t" ++ description.first_line()',
877+
]);
878+
if (!parentResult.ok) return parentResult;
879+
880+
const [changeId, description] = parentResult.value.stdout
881+
.trim()
882+
.split("\t");
883+
return ok({ changeId, description: description || "" });
831884
}
832885

833886
async navigateBottom(): Promise<Result<NavigationResult>> {

0 commit comments

Comments
 (0)