From 21e6a0a2259df2542e31c083ddeb4f53bfd8a995 Mon Sep 17 00:00:00 2001 From: "js@qlax.de" Date: Mon, 9 Feb 2026 08:41:02 +0000 Subject: [PATCH 1/5] fix(isl): use correct 'remove' subcommand for worktree deletion The WorktreeRemoveOperation used 'sl wt rm' but the actual subcommand is 'sl wt remove', causing 'abort: you need to specify a subcommand' when attempting to delete a worktree from the UI. --- addons/isl/src/operations/WorktreeRemoveOperation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/isl/src/operations/WorktreeRemoveOperation.ts b/addons/isl/src/operations/WorktreeRemoveOperation.ts index ab569a74743fb..d8a83f4b67f19 100644 --- a/addons/isl/src/operations/WorktreeRemoveOperation.ts +++ b/addons/isl/src/operations/WorktreeRemoveOperation.ts @@ -25,7 +25,7 @@ export class WorktreeRemoveOperation extends Operation { } getArgs() { - const args = ['wt', 'rm']; + const args = ['wt', 'remove']; if (this.force) { args.push('--force'); } From edefc973c8ab7748b37c86e64dd3b146727ae8b2 Mon Sep 17 00:00:00 2001 From: "js@qlax.de" Date: Mon, 9 Feb 2026 08:42:05 +0000 Subject: [PATCH 2/5] fix(isl): filter stale worktrees with non-existent directories The worktree dropdown showed deleted worktrees because 'sl wt list' returns entries even after directories are removed from disk. Added server-side validation using exists() to filter out stale entries. --- addons/isl-server/src/Repository.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/addons/isl-server/src/Repository.ts b/addons/isl-server/src/Repository.ts index f2f1494ae364e..a3c6bd7cd7508 100644 --- a/addons/isl-server/src/Repository.ts +++ b/addons/isl-server/src/Repository.ts @@ -31,11 +31,11 @@ import type { SettableConfigName, ShelvedChange, StableInfo, - WorktreeInfo, Submodule, SubmodulesByRoot, UncommittedChanges, ValidatedRepoInfo, + WorktreeInfo, } from 'isl/src/types'; import type {Comparison} from 'shared/Comparison'; import type {EjecaChildProcess, EjecaOptions} from 'shared/ejeca'; @@ -1562,13 +1562,14 @@ export class Repository { } public async getWorktrees(ctx: RepositoryContext): Promise> { - const result = await this.runCommand( - ['wt', 'list', '--json'], - 'WorktreeListCommand', - ctx, - ); + const result = await this.runCommand(['wt', 'list', '--json'], 'WorktreeListCommand', ctx); try { - return JSON.parse(result.stdout) as Array; + const worktrees = JSON.parse(result.stdout) as Array; + // Filter out stale worktrees whose directories no longer exist on disk + const validated = await Promise.all( + worktrees.map(async wt => ({wt, pathExists: await exists(wt.path)})), + ); + return validated.filter(({pathExists}) => pathExists).map(({wt}) => wt); } catch (err) { ctx.logger.error('Failed to parse worktree list output:', err); return []; From 537946aefd00a5453d547b3821ba713909326735 Mon Sep 17 00:00:00 2001 From: "js@qlax.de" Date: Mon, 9 Feb 2026 08:43:13 +0000 Subject: [PATCH 3/5] style(isl): improve PR stack layout and avatar positioning - Move stack card avatars closer to title (remove margin-left: auto) - Push action buttons to far right with margin-left: auto on actions div - Reduce left/right padding on PR dashboard content for wider cards - Bump version to v4.2.1 --- addons/isl/src/Commit.tsx | 8 +- addons/isl/src/PRDashboard.css | 82 ++++++++++----- addons/isl/src/PRDashboard.tsx | 175 ++++++++++++++++++--------------- 3 files changed, 158 insertions(+), 107 deletions(-) diff --git a/addons/isl/src/Commit.tsx b/addons/isl/src/Commit.tsx index 81240ad9fc693..4b7a0366f5773 100644 --- a/addons/isl/src/Commit.tsx +++ b/addons/isl/src/Commit.tsx @@ -26,8 +26,8 @@ import {MS_PER_DAY} from 'shared/constants'; import {useAutofocusRef} from 'shared/hooks'; import {notEmpty, nullthrows} from 'shared/utils'; import {spacing} from '../../components/theme/tokens.stylex'; -import {AllBookmarksTruncated, Bookmark, Bookmarks, createBookmarkAtCommit} from './Bookmark'; import {CommitAvatar} from './Avatar'; +import {AllBookmarksTruncated, Bookmark, Bookmarks, createBookmarkAtCommit} from './Bookmark'; import {openBrowseUrlForHash, supportsBrowseUrlForHash} from './BrowseRepo'; import {hasUnsavedEditedCommitMessage} from './CommitInfoView/CommitInfoState'; import {showComparison} from './ComparisonView/atoms'; @@ -41,6 +41,7 @@ import {SubmitSingleCommitButton} from './SubmitSingleCommitButton'; import {getSuggestedRebaseOperation, suggestedRebaseDestinations} from './SuggestedRebase'; import {UncommitButton} from './UncommitButton'; import {UncommittedChanges} from './UncommittedChanges'; +import {WorktreeIndicator} from './WorktreeIndicator'; import {tracker} from './analytics'; import {clipboardLinkHtml} from './clipboard'; import { @@ -50,13 +51,13 @@ import { diffSummary, latestCommitMessageTitle, } from './codeReview/CodeReviewInfo'; +import {DiffBadge, DiffFollower, DiffInfo} from './codeReview/DiffBadge'; import { - showOnlyMyStacksAtom, hideBotStacksAtom, hideMergedStacksAtom, isBotAuthor, + showOnlyMyStacksAtom, } from './codeReview/PRStacksAtom'; -import {DiffBadge, DiffFollower, DiffInfo} from './codeReview/DiffBadge'; import {SyncStatus, syncStatusAtom} from './codeReview/syncStatus'; import {useFeatureFlagSync} from './featureFlags'; import {FoldButton, useRunFoldPreview} from './fold'; @@ -97,7 +98,6 @@ import {latestSuccessorUnlessExplicitlyObsolete} from './successionUtils'; import {copyAndShowToast} from './toast'; import {showModal} from './useModal'; import {short} from './utils'; -import {WorktreeIndicator} from './WorktreeIndicator'; export const rebaseOffWarmWarningEnabled = localStorageBackedAtom( 'isl.rebase-off-warm-warning-enabled', diff --git a/addons/isl/src/PRDashboard.css b/addons/isl/src/PRDashboard.css index fb20e5da60c55..2ecbdb12cbe9c 100644 --- a/addons/isl/src/PRDashboard.css +++ b/addons/isl/src/PRDashboard.css @@ -110,9 +110,9 @@ .pr-dashboard-content { padding: var(--pad); - /* Account for negative margins on parent */ - padding-left: calc(var(--pad) + 16px); - padding-right: calc(var(--pad) + 16px); + /* Account for negative margins on parent, keep padding tight for more card width */ + padding-left: calc(var(--halfpad) + 16px); + padding-right: calc(var(--halfpad) + 16px); } .pr-dashboard-empty { @@ -162,12 +162,12 @@ } .stack-card-title { - flex: 1; font-weight: 500; cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + min-width: 0; } .stack-card-pull-button { @@ -305,8 +305,7 @@ .stack-card-author { display: flex; align-items: center; - margin-left: auto; - margin-right: var(--halfpad); + margin-left: 4px; } .stack-card-avatar { @@ -320,6 +319,7 @@ display: flex; align-items: center; gap: 2px; + margin-left: auto; } /* Label editor */ @@ -605,26 +605,60 @@ } /* Stagger animation delays for visual interest */ -.stack-card-skeleton:nth-child(1) { animation-delay: 0s; } -.stack-card-skeleton:nth-child(2) { animation-delay: 0.1s; } -.stack-card-skeleton:nth-child(3) { animation-delay: 0.2s; } -.stack-card-skeleton:nth-child(4) { animation-delay: 0.3s; } -.stack-card-skeleton:nth-child(5) { animation-delay: 0.4s; } -.stack-card-skeleton:nth-child(6) { animation-delay: 0.5s; } -.stack-card-skeleton:nth-child(7) { animation-delay: 0.6s; } -.stack-card-skeleton:nth-child(8) { animation-delay: 0.7s; } +.stack-card-skeleton:nth-child(1) { + animation-delay: 0s; +} +.stack-card-skeleton:nth-child(2) { + animation-delay: 0.1s; +} +.stack-card-skeleton:nth-child(3) { + animation-delay: 0.2s; +} +.stack-card-skeleton:nth-child(4) { + animation-delay: 0.3s; +} +.stack-card-skeleton:nth-child(5) { + animation-delay: 0.4s; +} +.stack-card-skeleton:nth-child(6) { + animation-delay: 0.5s; +} +.stack-card-skeleton:nth-child(7) { + animation-delay: 0.6s; +} +.stack-card-skeleton:nth-child(8) { + animation-delay: 0.7s; +} /* Stagger skeleton boxes within each card */ -.stack-card-skeleton .skeleton-box:nth-child(1) { animation-delay: 0s; } -.stack-card-skeleton .skeleton-box:nth-child(2) { animation-delay: 0.05s; } -.stack-card-skeleton .skeleton-box:nth-child(3) { animation-delay: 0.1s; } -.stack-card-skeleton .skeleton-box:nth-child(4) { animation-delay: 0.15s; } - -.pr-row-skeleton:nth-child(1) .skeleton-box { animation-delay: 0.1s; } -.pr-row-skeleton:nth-child(2) .skeleton-box { animation-delay: 0.15s; } -.pr-row-skeleton:nth-child(3) .skeleton-box { animation-delay: 0.2s; } -.pr-row-skeleton:nth-child(4) .skeleton-box { animation-delay: 0.25s; } -.pr-row-skeleton:nth-child(5) .skeleton-box { animation-delay: 0.3s; } +.stack-card-skeleton .skeleton-box:nth-child(1) { + animation-delay: 0s; +} +.stack-card-skeleton .skeleton-box:nth-child(2) { + animation-delay: 0.05s; +} +.stack-card-skeleton .skeleton-box:nth-child(3) { + animation-delay: 0.1s; +} +.stack-card-skeleton .skeleton-box:nth-child(4) { + animation-delay: 0.15s; +} + +.pr-row-skeleton:nth-child(1) .skeleton-box { + animation-delay: 0.1s; +} +.pr-row-skeleton:nth-child(2) .skeleton-box { + animation-delay: 0.15s; +} +.pr-row-skeleton:nth-child(3) .skeleton-box { + animation-delay: 0.2s; +} +.pr-row-skeleton:nth-child(4) .skeleton-box { + animation-delay: 0.25s; +} +.pr-row-skeleton:nth-child(5) .skeleton-box { + animation-delay: 0.3s; +} /* Time Range Dropdown */ .time-range-wrapper { diff --git a/addons/isl/src/PRDashboard.tsx b/addons/isl/src/PRDashboard.tsx index aef76c237f6de..9b1678db89559 100644 --- a/addons/isl/src/PRDashboard.tsx +++ b/addons/isl/src/PRDashboard.tsx @@ -9,41 +9,43 @@ import type {PRStack} from './codeReview/PRStacksAtom'; import type {DiffSummary, TimeRangeDays} from './types'; import {Button} from 'isl-components/Button'; -import {Dropdown} from 'isl-components/Dropdown'; import {Icon} from 'isl-components/Icon'; import {TextField} from 'isl-components/TextField'; import {Tooltip} from 'isl-components/Tooltip'; import {useAtom, useAtomValue} from 'jotai'; -import {useState, useCallback, useEffect, useRef} from 'react'; +import {useCallback, useEffect, useRef, useState} from 'react'; import {ComparisonType} from 'shared/Comparison'; import serverAPI from './ClientToServerAPI'; -import {showComparison} from './ComparisonView/atoms'; -import {enterReviewMode} from './reviewMode'; -import {currentGitHubUser, allDiffSummaries, triggerFullDiffSummariesRefresh} from './codeReview/CodeReviewInfo'; import { - prStacksAtom, - stackLabelsAtom, + allDiffSummaries, + currentGitHubUser, + triggerFullDiffSummariesRefresh, +} from './codeReview/CodeReviewInfo'; +import { hiddenStacksAtom, - hideMergedStacksAtom, - showOnlyMyStacksAtom, hideBotStacksAtom, + hideMergedStacksAtom, isBotAuthor, + prStacksAtom, + showOnlyMyStacksAtom, + stackLabelsAtom, } from './codeReview/PRStacksAtom'; -import {T} from './i18n'; import {scrollToCommit} from './CommitTreeList'; +import {showComparison} from './ComparisonView/atoms'; +import {T, t} from './i18n'; import {writeAtom} from './jotaiUtils'; -import {inlineProgressByHash, useRunOperation} from './operationsState'; -import {PullOperation} from './operations/PullOperation'; -import {GotoOperation} from './operations/GotoOperation'; import {ClosePROperation} from './operations/ClosePROperation'; +import {GotoOperation} from './operations/GotoOperation'; +import {PullOperation} from './operations/PullOperation'; import {WorktreeAddOperation} from './operations/WorktreeAddOperation'; -import {worktreesForCommit} from './worktrees'; -import {showToast} from './toast'; -import {t} from './i18n'; +import {inlineProgressByHash, useRunOperation} from './operationsState'; import {dagWithPreviews} from './previews'; +import {enterReviewMode} from './reviewMode'; import {selectedCommits} from './selection'; import {selectedTimeRangeAtom, setTimeRange} from './serverAPIState'; +import {showToast} from './toast'; import {succeedableRevset} from './types'; +import {worktreesForCommit} from './worktrees'; import './PRDashboard.css'; @@ -135,9 +137,14 @@ function MainBranchSection({}: {isScrolled?: boolean}) { // Find main/master bookmark in the dag const mainCommit = dag.resolve('main') ?? dag.resolve('master'); - const remoteName = mainCommit?.remoteBookmarks.find(b => - b === 'origin/main' || b === 'origin/master' || b === 'remote/main' || b === 'remote/master' - ) ?? 'main'; + const remoteName = + mainCommit?.remoteBookmarks.find( + b => + b === 'origin/main' || + b === 'origin/master' || + b === 'remote/main' || + b === 'remote/master', + ) ?? 'main'; // Check if we're currently on main const currentCommit = dag.resolve('.'); @@ -161,11 +168,7 @@ function MainBranchSection({}: {isScrolled?: boolean}) { runOperation(new GotoOperation(succeedableRevset(remoteName))); }, [isOnMain, isBehind, runOperation, remoteName]); - const syncStatusText = isBehind - ? 'Updates available' - : isOnMain - ? 'You are here' - : 'Up to date'; + const syncStatusText = isBehind ? 'Updates available' : isOnMain ? 'You are here' : 'Up to date'; const statusClass = isBehind ? 'main-branch-status main-branch-status-behind' @@ -182,13 +185,8 @@ function MainBranchSection({}: {isScrolled?: boolean}) { @@ -219,7 +217,9 @@ function TimeRangeDropdown() { value={selectedRange === undefined ? 'undefined' : String(selectedRange)} onChange={handleChange}> {TIME_RANGE_OPTIONS.map(opt => ( - ))} @@ -284,9 +284,7 @@ export function PRDashboard() { return true; }); - const hiddenCount = stacks.filter(stack => - hiddenStacks.includes(stack.id), - ).length; + const hiddenCount = stacks.filter(stack => hiddenStacks.includes(stack.id)).length; const mergedCount = stacks.filter(stack => stack.isMerged).length; @@ -302,24 +300,32 @@ export function PRDashboard() {
- PR Stacks (v4.2) + PR Stacks (v4.2.1)
{currentUser && ( + title={ + showOnlyMine + ? `Show all authors (${otherAuthorsCount} hidden)` + : 'Show only my stacks' + }> )} + title={ + hideBots ? `Show ${botCount} bot PRs` : 'Hide bot PRs (renovate, dependabot, etc)' + }> - + {hiddenCount > 0 && ( -