Skip to content

Commit 11e57ee

Browse files
authored
Merge branch 'main' into fix/user-folder-selection
2 parents 629a4c4 + d8fb08f commit 11e57ee

13 files changed

Lines changed: 271 additions & 163 deletions

File tree

apps/array/src/renderer/features/panels/components/LeafNodeRenderer.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ interface LeafNodeRendererProps {
1717
draggingTabPanelId: string | null;
1818
onActiveTabChange: (panelId: string, tabId: string) => void;
1919
onPanelFocus: (panelId: string) => void;
20-
focusedPanelId: string | null;
2120
onAddTerminal: (panelId: string) => void;
2221
onSplitPanel: (panelId: string, direction: SplitDirection) => void;
2322
}
@@ -34,7 +33,6 @@ export const LeafNodeRenderer: React.FC<LeafNodeRendererProps> = ({
3433
draggingTabPanelId,
3534
onActiveTabChange,
3635
onPanelFocus,
37-
focusedPanelId,
3836
onAddTerminal,
3937
onSplitPanel,
4038
}) => {
@@ -62,7 +60,6 @@ export const LeafNodeRenderer: React.FC<LeafNodeRendererProps> = ({
6260
onPanelFocus={onPanelFocus}
6361
draggingTabId={draggingTabId}
6462
draggingTabPanelId={draggingTabPanelId}
65-
isFocused={focusedPanelId === node.id}
6663
onAddTerminal={() => onAddTerminal(node.id)}
6764
onSplitPanel={(direction) => onSplitPanel(node.id, direction)}
6865
/>

apps/array/src/renderer/features/panels/components/PanelLayout.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,6 @@ const PanelLayoutRenderer: React.FC<{
128128
draggingTabPanelId={layoutState.draggingTabPanelId}
129129
onActiveTabChange={handleSetActiveTab}
130130
onPanelFocus={handlePanelFocus}
131-
focusedPanelId={layoutState.focusedPanelId}
132131
onAddTerminal={handleAddTerminal}
133132
onSplitPanel={handleSplitPanel}
134133
/>

apps/array/src/renderer/features/panels/components/TabbedPanel.tsx

Lines changed: 26 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ interface TabbedPanelProps {
5353
onPanelFocus?: (panelId: string) => void;
5454
draggingTabId?: string | null;
5555
draggingTabPanelId?: string | null;
56-
isFocused?: boolean;
5756
onAddTerminal?: () => void;
5857
onSplitPanel?: (direction: SplitDirection) => void;
5958
}
@@ -68,7 +67,6 @@ export const TabbedPanel: React.FC<TabbedPanelProps> = ({
6867
onPanelFocus,
6968
draggingTabId = null,
7069
draggingTabPanelId = null,
71-
isFocused = false,
7270
onAddTerminal,
7371
onSplitPanel,
7472
}) => {
@@ -193,37 +191,32 @@ export const TabbedPanel: React.FC<TabbedPanelProps> = ({
193191
/>
194192
)}
195193
</Flex>
196-
{isFocused &&
197-
content.droppable &&
198-
(onSplitPanel || onAddTerminal) && (
199-
<Flex
200-
style={{
201-
position: "absolute",
202-
right: 0,
203-
top: 0,
204-
height: "32px",
205-
borderLeft: "1px solid var(--gray-6)",
206-
background: "var(--color-background)",
207-
}}
208-
>
209-
{onSplitPanel && (
210-
<TabBarButton
211-
ariaLabel="Split panel"
212-
onClick={handleSplitClick}
213-
>
214-
<SquareSplitHorizontalIcon width={12} height={12} />
215-
</TabBarButton>
216-
)}
217-
{onAddTerminal && (
218-
<TabBarButton
219-
ariaLabel="Add terminal"
220-
onClick={onAddTerminal}
221-
>
222-
<PlusIcon width={12} height={12} />
223-
</TabBarButton>
224-
)}
225-
</Flex>
226-
)}
194+
{content.droppable && (onSplitPanel || onAddTerminal) && (
195+
<Flex
196+
style={{
197+
position: "absolute",
198+
right: 0,
199+
top: 0,
200+
height: "32px",
201+
borderLeft: "1px solid var(--gray-6)",
202+
background: "var(--color-background)",
203+
}}
204+
>
205+
{onSplitPanel && (
206+
<TabBarButton
207+
ariaLabel="Split panel"
208+
onClick={handleSplitClick}
209+
>
210+
<SquareSplitHorizontalIcon width={12} height={12} />
211+
</TabBarButton>
212+
)}
213+
{onAddTerminal && (
214+
<TabBarButton ariaLabel="Add terminal" onClick={onAddTerminal}>
215+
<PlusIcon width={12} height={12} />
216+
</TabBarButton>
217+
)}
218+
</Flex>
219+
)}
227220
</Box>
228221
)}
229222

apps/array/src/renderer/features/terminal/services/TerminalManager.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,10 @@ function loadAddons(term: XTerm) {
9393
const fit = new FitAddon();
9494
const serialize = new SerializeAddon();
9595

96-
const activateLink = (event: MouseEvent, uri: string) => {
97-
const isMac = /Mac/.test(navigator.platform);
98-
const hasModifier = isMac ? event.metaKey : event.ctrlKey;
99-
100-
if (hasModifier) {
101-
trpcVanilla.os.openExternal.mutate({ url: uri }).catch((error: Error) => {
102-
log.error("Failed to open link:", uri, error);
103-
});
104-
}
96+
const activateLink = (_event: MouseEvent, uri: string) => {
97+
trpcVanilla.os.openExternal.mutate({ url: uri }).catch((error: Error) => {
98+
log.error("Failed to open link:", uri, error);
99+
});
105100
};
106101

107102
const webLinks = new WebLinksAddon(activateLink);

apps/cli/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
"build": "bun build ./src/index.ts --outdir ./dist --target bun",
1111
"dev": "bun run ./bin/arr.ts",
1212
"typecheck": "tsc --noEmit",
13-
"test": "bun test --concurrent tests/unit tests/e2e/cli.test.ts",
1413
"test:pty": "vitest run tests/e2e/pty.test.ts"
1514
},
1615
"devDependencies": {

apps/cli/src/commands/merge.ts

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
formatSuccess,
1313
hint,
1414
message,
15-
status,
1615
warning,
1716
} from "../utils/output";
1817
import { unwrap } from "../utils/run";
@@ -24,8 +23,6 @@ interface MergeFlags {
2423
}
2524

2625
export async function merge(flags: MergeFlags, ctx: ArrContext): Promise<void> {
27-
const trunk = ctx.trunk;
28-
2926
const prsResult = await getMergeablePrs();
3027

3128
if (!prsResult.ok) {
@@ -62,25 +59,22 @@ export async function merge(flags: MergeFlags, ctx: ArrContext): Promise<void> {
6259
if (flags.merge) method = "merge";
6360
if (flags.rebase) method = "rebase";
6461

65-
message(`Merging ${prs.length} PR${prs.length > 1 ? "s" : ""} from stack...`);
62+
message(`Merging ${prs.length} PR${prs.length > 1 ? "s" : ""}...`);
6663
blank();
6764

6865
const result = await mergeCmd(prs, {
6966
method,
7067
engine: ctx.engine,
71-
onMerging: (pr: PRToMerge, nextPr?: PRToMerge) => {
72-
message(`Merging PR #${cyan(String(pr.prNumber))}: ${pr.prTitle}`);
73-
hint(`Branch: ${pr.bookmarkName}${pr.baseRefName}`);
74-
if (nextPr) {
75-
hint(`Rebasing PR #${nextPr.prNumber} onto ${trunk}...`);
76-
}
68+
onWaitingForCI: (pr: PRToMerge) => {
69+
message(`PR #${cyan(String(pr.prNumber))}: ${pr.prTitle}`);
70+
message(dim(" Waiting for CI checks..."));
7771
},
78-
onWaiting: () => {
79-
process.stdout.write(dim(" Waiting for GitHub..."));
72+
onMerging: (_pr: PRToMerge) => {
73+
message(dim(" Merging..."));
8074
},
8175
onMerged: (pr: PRToMerge) => {
82-
process.stdout.write(`\r${" ".repeat(30)}\r`);
83-
message(formatSuccess(`Merged PR #${pr.prNumber}`));
76+
message(formatSuccess(` Merged PR #${pr.prNumber}`));
77+
blank();
8478
},
8579
});
8680

@@ -89,7 +83,10 @@ export async function merge(flags: MergeFlags, ctx: ArrContext): Promise<void> {
8983
process.exit(1);
9084
}
9185

92-
blank();
93-
status("Syncing to update local state...");
94-
message(formatSuccess("Done! All PRs merged and synced."));
86+
message(
87+
formatSuccess(
88+
`Merged ${result.value.merged.length} PR${result.value.merged.length > 1 ? "s" : ""}!`,
89+
),
90+
);
91+
hint("Run 'arr sync' to update local state.");
9592
}

apps/cli/src/commands/track.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,7 @@ export async function track(
1616

1717
message(formatSuccess(`Now tracking ${cyan(result.bookmark)}`));
1818
indent(`${dim("Parent:")} ${result.parent}`);
19+
if (result.linkedPr) {
20+
indent(`${dim("Linked:")} PR #${result.linkedPr}`);
21+
}
1922
}

packages/core/src/commands/merge.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import type { Command } from "./types";
77
interface MergeOptions {
88
method?: "merge" | "squash" | "rebase";
99
engine: Engine;
10-
onMerging?: (pr: PRToMerge, nextPr?: PRToMerge) => void;
11-
onWaiting?: () => void;
10+
onWaitingForCI?: (pr: PRToMerge) => void;
11+
onMerging?: (pr: PRToMerge) => void;
1212
onMerged?: (pr: PRToMerge) => void;
1313
}
1414

@@ -20,7 +20,8 @@ export async function getMergeablePrs(): Promise<Result<PRToMerge[]>> {
2020
}
2121

2222
/**
23-
* Merge the stack of PRs.
23+
* Merge the stack of PRs sequentially.
24+
* Waits for CI to pass on each PR before merging, then updates the next PR's base.
2425
* Untracks merged bookmarks from the engine.
2526
*/
2627
export async function merge(
@@ -31,8 +32,8 @@ export async function merge(
3132
prs,
3233
{ method: options.method ?? "squash", engine: options.engine },
3334
{
35+
onWaitingForCI: options.onWaitingForCI,
3436
onMerging: options.onMerging,
35-
onWaiting: options.onWaiting,
3637
onMerged: options.onMerged,
3738
},
3839
);

packages/core/src/commands/track.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Engine } from "../engine";
2+
import { getPRForBranch } from "../github/pr-status";
23
import { ensureBookmark, getTrunk, list, resolveChange } from "../jj";
34
import { createError, err, ok, type Result } from "../result";
45
import { datePrefixedLabel } from "../slugify";
@@ -7,6 +8,8 @@ import type { Command } from "./types";
78
interface TrackResult {
89
bookmark: string;
910
parent: string;
11+
/** PR number if an existing PR was found and linked */
12+
linkedPr?: number;
1013
}
1114

1215
interface TrackOptions {
@@ -126,7 +129,14 @@ export async function track(
126129
return refreshResult;
127130
}
128131

129-
return ok({ bookmark, parent: parentBranch });
132+
// Check if this bookmark has an existing PR on GitHub
133+
let linkedPr: number | undefined;
134+
const prResult = await getPRForBranch(bookmark);
135+
if (prResult.ok && prResult.value && prResult.value.state === "OPEN") {
136+
linkedPr = prResult.value.number;
137+
}
138+
139+
return ok({ bookmark, parent: parentBranch, linkedPr });
130140
}
131141

132142
export const trackCommand: Command<TrackResult, [TrackOptions]> = {

packages/core/src/github/pr-actions.ts

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -194,19 +194,31 @@ export async function updatePRBranch(
194194
}
195195
}
196196

197+
export interface WaitForMergeableOptions {
198+
timeoutMs?: number;
199+
pollIntervalMs?: number;
200+
/** Callback when status changes, for UI updates */
201+
onStatusChange?: (status: {
202+
mergeable: boolean | null;
203+
state: string;
204+
checksComplete: boolean;
205+
}) => void;
206+
}
207+
197208
export function waitForMergeable(
198209
prNumber: number,
199-
options?: { timeoutMs?: number; pollIntervalMs?: number },
210+
options?: WaitForMergeableOptions,
200211
cwd = process.cwd(),
201212
): Promise<Result<{ mergeable: boolean; reason?: string }>> {
202-
const timeoutMs = options?.timeoutMs ?? 30000;
203-
const pollIntervalMs = options?.pollIntervalMs ?? 2000;
213+
const timeoutMs = options?.timeoutMs ?? 300000; // 5 minutes default
214+
const pollIntervalMs = options?.pollIntervalMs ?? 5000;
204215

205216
return withGitHub(
206217
cwd,
207218
"check mergeable status",
208219
async ({ octokit, owner, repo }) => {
209220
const startTime = Date.now();
221+
let lastState = "";
210222

211223
while (Date.now() - startTime < timeoutMs) {
212224
const { data: pr } = await octokit.pulls.get({
@@ -215,23 +227,58 @@ export function waitForMergeable(
215227
pull_number: prNumber,
216228
});
217229

218-
if (pr.mergeable === true) {
230+
// mergeable_state values:
231+
// - "clean": can merge, all checks passed
232+
// - "blocked": checks pending or required reviews missing
233+
// - "dirty": has conflicts
234+
// - "unstable": has failing checks but can still merge
235+
// - "unknown": GitHub is computing
236+
const state = pr.mergeable_state || "unknown";
237+
const checksComplete = state !== "blocked" && state !== "unknown";
238+
239+
// Notify caller of status change
240+
if (state !== lastState) {
241+
options?.onStatusChange?.({
242+
mergeable: pr.mergeable,
243+
state,
244+
checksComplete,
245+
});
246+
lastState = state;
247+
}
248+
249+
// "clean" means mergeable AND all required checks passed
250+
if (state === "clean" && pr.mergeable === true) {
219251
return { mergeable: true };
220252
}
221253

222-
if (pr.mergeable === false) {
254+
// "unstable" means checks failed but PR is still mergeable (non-required checks)
255+
if (state === "unstable" && pr.mergeable === true) {
256+
return { mergeable: true };
257+
}
258+
259+
// Has conflicts
260+
if (state === "dirty") {
261+
return {
262+
mergeable: false,
263+
reason: "Has merge conflicts",
264+
};
265+
}
266+
267+
// Explicit not mergeable
268+
if (pr.mergeable === false && state !== "unknown") {
223269
return {
224270
mergeable: false,
225-
reason: pr.mergeable_state || "Has conflicts or other issues",
271+
reason: state || "Not mergeable",
226272
};
227273
}
228274

275+
// "blocked" or "unknown" - keep waiting
229276
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
230277
}
231278

232279
return {
233280
mergeable: false,
234-
reason: "Timeout waiting for merge status",
281+
reason: "Timeout waiting for CI checks",
235282
};
236283
},
237284
);

0 commit comments

Comments
 (0)