aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-12 18:53:13 -0500
committerChristian Cleberg <[email protected]>2026-04-12 18:53:13 -0500
commitb526cddd8ec421eba00feecbdffae3978cb61b34 (patch)
treef8426032d07a92d034975e90decd1ec0f85bd452
parent92fb89cd35588ee36190c95b1af1e9595b397fae (diff)
downloadhutch-b526cddd8ec421eba00feecbdffae3978cb61b34.tar.gz
hutch-b526cddd8ec421eba00feecbdffae3978cb61b34.tar.bz2
hutch-b526cddd8ec421eba00feecbdffae3978cb61b34.zip
feat: improve Home structure and navigationv2.16.0
implements: https://todo.sr.ht/~ccleberg/hutch/14 implements: https://todo.sr.ht/~ccleberg/hutch/15 implements: https://todo.sr.ht/~ccleberg/hutch/16
-rw-r--r--Hutch.xcodeproj/project.pbxproj18
-rw-r--r--Hutch/App/AppState.swift6
-rw-r--r--Hutch/App/AppStorageKeys.swift3
-rw-r--r--Hutch/App/RootView.swift3
-rw-r--r--Hutch/Views/Builds/BuildRowView.swift38
-rw-r--r--Hutch/Views/Home/HomeSectionView.swift63
-rw-r--r--Hutch/Views/Home/HomeView.swift207
-rwxr-xr-xlabels.sh96
8 files changed, 365 insertions, 69 deletions
diff --git a/Hutch.xcodeproj/project.pbxproj b/Hutch.xcodeproj/project.pbxproj
index 0f11fbc..70c6c29 100644
--- a/Hutch.xcodeproj/project.pbxproj
+++ b/Hutch.xcodeproj/project.pbxproj
@@ -307,8 +307,8 @@
projectRoot = "";
targets = (
8B4B28D02F6704280045FA19 /* Hutch */,
- 8B8182B62F6B73F3000AE049 /* HutchTests */,
8BE081F72F80000100000001 /* HutchWidgetExtension */,
+ 8B8182B62F6B73F3000AE049 /* HutchTests */,
);
};
/* End PBXProject section */
@@ -515,7 +515,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 35;
+ CURRENT_PROJECT_VERSION = 36;
DEVELOPMENT_TEAM = ZCNAX3VL9D;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -532,7 +532,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 2.15.3;
+ MARKETING_VERSION = 2.16.0;
PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -552,7 +552,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 35;
+ CURRENT_PROJECT_VERSION = 36;
DEVELOPMENT_TEAM = ZCNAX3VL9D;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -569,7 +569,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 2.15.3;
+ MARKETING_VERSION = 2.16.0;
PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -632,7 +632,7 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 35;
+ CURRENT_PROJECT_VERSION = 36;
DEVELOPMENT_TEAM = ZCNAX3VL9D;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -642,7 +642,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 2.15.3;
+ MARKETING_VERSION = 2.16.0;
PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -661,7 +661,7 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 35;
+ CURRENT_PROJECT_VERSION = 36;
DEVELOPMENT_TEAM = ZCNAX3VL9D;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -671,7 +671,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 2.15.3;
+ MARKETING_VERSION = 2.16.0;
PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
diff --git a/Hutch/App/AppState.swift b/Hutch/App/AppState.swift
index 35ac77f..0f0b036 100644
--- a/Hutch/App/AppState.swift
+++ b/Hutch/App/AppState.swift
@@ -20,6 +20,7 @@ final class AppState {
case tracker(TrackerSummary)
case mailingList(InboxMailingListReference)
case systemStatus
+ case builds
}
enum AuthPhase {
@@ -309,6 +310,11 @@ final class AppState {
selectedTab = .more
}
+ func navigateToBuildsList() {
+ pendingTabNavigation = .builds
+ selectedTab = .builds
+ }
+
func presentRepositoryDeepLinkError() {
deepLinkError = "The repository could not be found or is inaccessible."
}
diff --git a/Hutch/App/AppStorageKeys.swift b/Hutch/App/AppStorageKeys.swift
index c1b39c4..68a9588 100644
--- a/Hutch/App/AppStorageKeys.swift
+++ b/Hutch/App/AppStorageKeys.swift
@@ -8,4 +8,7 @@ enum AppStorageKeys {
static let hutchStatsBaseURL = "hutchStatsBaseURL"
static let systemStatusSnapshotCache = "systemStatusSnapshotCache"
static let systemStatusIncidentCache = "systemStatusIncidentCache"
+ static let homeProjectsExpanded = "homeProjectsExpanded"
+ static let homeAssignedTicketsExpanded = "homeAssignedTicketsExpanded"
+ static let homeBuildsExpanded = "homeBuildsExpanded"
}
diff --git a/Hutch/App/RootView.swift b/Hutch/App/RootView.swift
index 80532a0..baca7a7 100644
--- a/Hutch/App/RootView.swift
+++ b/Hutch/App/RootView.swift
@@ -242,6 +242,9 @@ struct RootView: View {
await settleNavigationTransition()
morePath.append(MoreRoute.systemStatus)
}
+ case .builds:
+ buildsPath = NavigationPath()
+ appState.selectedTab = .builds
}
}
diff --git a/Hutch/Views/Builds/BuildRowView.swift b/Hutch/Views/Builds/BuildRowView.swift
index e2afd27..9602e5c 100644
--- a/Hutch/Views/Builds/BuildRowView.swift
+++ b/Hutch/Views/Builds/BuildRowView.swift
@@ -36,6 +36,44 @@ struct BuildRowView: View {
}
}
+struct JobStatusBadge: View {
+ let status: JobStatus
+
+ var body: some View {
+ HStack(spacing: 4) {
+ JobStatusIcon(status: status)
+ .frame(width: 12, height: 12)
+ Text(status.displayTitle)
+ .font(.caption2.weight(.medium))
+ }
+ .foregroundStyle(.secondary)
+ .padding(.horizontal, 8)
+ .padding(.vertical, 4)
+ .background(Color(.secondarySystemFill), in: Capsule())
+ }
+}
+
+extension JobStatus {
+ var displayTitle: String {
+ switch self {
+ case .pending:
+ "Pending"
+ case .queued:
+ "Queued"
+ case .running:
+ "Running"
+ case .success:
+ "Succeeded"
+ case .failed:
+ "Failed"
+ case .cancelled:
+ "Cancelled"
+ case .timeout:
+ "Timed Out"
+ }
+ }
+}
+
// MARK: - Job Status Icon
struct JobStatusIcon: View {
diff --git a/Hutch/Views/Home/HomeSectionView.swift b/Hutch/Views/Home/HomeSectionView.swift
new file mode 100644
index 0000000..5f04d58
--- /dev/null
+++ b/Hutch/Views/Home/HomeSectionView.swift
@@ -0,0 +1,63 @@
+import SwiftUI
+
+struct HomeSectionView<Accessory: View, Content: View>: View {
+ let title: String
+ @Binding var isExpanded: Bool
+ let accessory: Accessory
+ let content: Content
+
+ init(
+ _ title: String,
+ isExpanded: Binding<Bool>,
+ @ViewBuilder accessory: () -> Accessory,
+ @ViewBuilder content: () -> Content
+ ) {
+ self.title = title
+ self._isExpanded = isExpanded
+ self.accessory = accessory()
+ self.content = content()
+ }
+
+ var body: some View {
+ Section {
+ if isExpanded {
+ content
+ }
+ } header: {
+ HomeSectionHeader(
+ title: title,
+ isExpanded: $isExpanded,
+ accessory: accessory
+ )
+ }
+ }
+}
+
+private struct HomeSectionHeader<Accessory: View>: View {
+ let title: String
+ @Binding var isExpanded: Bool
+ let accessory: Accessory
+
+ var body: some View {
+ HStack(spacing: 12) {
+ Button {
+ isExpanded.toggle()
+ } label: {
+ HStack(spacing: 8) {
+ Image(systemName: isExpanded ? "chevron.down" : "chevron.right")
+ .font(.caption.weight(.semibold))
+ .foregroundStyle(.secondary)
+ Text(title)
+ }
+ .contentShape(Rectangle())
+ }
+ .buttonStyle(.plain)
+ .accessibilityLabel(isExpanded ? "Collapse \(title)" : "Expand \(title)")
+
+ Spacer(minLength: 8)
+
+ accessory
+ }
+ .textCase(nil)
+ }
+}
diff --git a/Hutch/Views/Home/HomeView.swift b/Hutch/Views/Home/HomeView.swift
index 3efef7a..180f0f3 100644
--- a/Hutch/Views/Home/HomeView.swift
+++ b/Hutch/Views/Home/HomeView.swift
@@ -2,6 +2,9 @@ import SwiftUI
struct HomeView: View {
@AppStorage(AppStorageKeys.swipeActionsEnabled) private var swipeActionsEnabled = true
+ @AppStorage(AppStorageKeys.homeProjectsExpanded) private var projectsExpanded = true
+ @AppStorage(AppStorageKeys.homeAssignedTicketsExpanded) private var assignedTicketsExpanded = true
+ @AppStorage(AppStorageKeys.homeBuildsExpanded) private var buildsExpanded = true
@Environment(AppState.self) private var appState
@Environment(\.scenePhase) private var scenePhase
@State private var viewModel: HomeViewModel?
@@ -116,7 +119,7 @@ struct HomeView: View {
summary: viewModel.buildsSummaryText,
countText: String(viewModel.failedBuildCount + viewModel.activeBuildCount),
action: {
- appState.selectedTab = .builds
+ appState.navigateToBuildsList()
}
)
}
@@ -176,7 +179,15 @@ struct HomeView: View {
@ViewBuilder
private func projectsSection(_ viewModel: HomeViewModel) -> some View {
if !viewModel.projects.isEmpty {
- Section {
+ HomeSectionView("Projects", isExpanded: $projectsExpanded) {
+ NavigationLink {
+ HomeProjectsListView(viewModel: viewModel)
+ } label: {
+ Text("See All")
+ .font(.caption.weight(.medium))
+ }
+ .buttonStyle(.plain)
+ } content: {
ForEach(viewModel.projects.prefix(projectPreviewLimit)) { project in
NavigationLink {
ProjectDetailView(project: project)
@@ -184,17 +195,21 @@ struct HomeView: View {
HomeProjectRow(project: project)
}
}
- } header: {
- HomeSectionHeader("Projects") {
- HomeProjectsListView(viewModel: viewModel)
- }
}
}
}
@ViewBuilder
private func assignedTicketsSection(_ viewModel: HomeViewModel) -> some View {
- Section {
+ HomeSectionView("Tickets", isExpanded: $assignedTicketsExpanded) {
+ NavigationLink {
+ HomeAssignedTicketsListView(viewModel: viewModel)
+ } label: {
+ Text("See All")
+ .font(.caption.weight(.medium))
+ }
+ .buttonStyle(.plain)
+ } content: {
if viewModel.isLoadingAssignedTickets && viewModel.assignedTickets.isEmpty {
HomeSectionLoadingRow(label: "Loading assigned tickets")
} else if let error = viewModel.assignedTicketsError, viewModel.assignedTickets.isEmpty {
@@ -241,16 +256,18 @@ struct HomeView: View {
}
}
}
- } header: {
- HomeSectionHeader("Assigned Tickets") {
- HomeAssignedTicketsListView(viewModel: viewModel)
- }
}
}
@ViewBuilder
private func recentBuildsSection(_ viewModel: HomeViewModel) -> some View {
- Section {
+ HomeSectionView("Builds", isExpanded: $buildsExpanded) {
+ Button("See All") {
+ appState.navigateToBuildsList()
+ }
+ .font(.caption.weight(.medium))
+ .buttonStyle(.plain)
+ } content: {
if viewModel.isLoadingRecentBuilds && viewModel.recentBuilds.isEmpty {
HomeSectionLoadingRow(label: "Loading recent builds")
} else if let error = viewModel.recentBuildsError, viewModel.recentBuilds.isEmpty {
@@ -266,32 +283,67 @@ struct HomeView: View {
systemImage: "clock"
)
} else {
- ForEach(viewModel.recentBuilds.prefix(previewLimit)) { build in
- NavigationLink {
- BuildDetailView(jobId: build.job.id)
- } label: {
- HomeBuildRow(build: build)
+ ForEach(buildGroups(for: viewModel.recentBuilds)) { group in
+ if let repositoryDisplayName = group.repositoryDisplayName {
+ HomeBuildGroupHeader(
+ repositoryDisplayName: repositoryDisplayName,
+ buildCount: group.builds.count,
+ latestStatus: group.latestStatus
+ )
}
- .swipeActions(edge: .leading, allowsFullSwipe: true) {
- if swipeActionsEnabled, build.job.status.isCancellable {
- Button {
- Task {
- await viewModel.cancelBuild(build)
+
+ ForEach(group.builds) { build in
+ NavigationLink {
+ BuildDetailView(jobId: build.job.id)
+ } label: {
+ HomeBuildRow(
+ build: build,
+ showsRepositoryLink: group.repositoryDisplayName == nil
+ )
+ }
+ .swipeActions(edge: .leading, allowsFullSwipe: true) {
+ if swipeActionsEnabled, build.job.status.isCancellable {
+ Button {
+ Task {
+ await viewModel.cancelBuild(build)
+ }
}
+ label: {
+ Label("Cancel", systemImage: "xmark.circle")
+ }
+ .tint(.red)
}
- label: {
- Label("Cancel", systemImage: "xmark.circle")
- }
- .tint(.red)
}
}
}
}
- } header: {
- HomeSectionActionHeader("Recent Builds") {
- appState.selectedTab = .builds
+ }
+ }
+
+ private func buildGroups(for builds: [HomeBuildItem]) -> [HomeBuildGroup] {
+ let previewBuilds = Array(builds.prefix(previewLimit))
+ guard let firstBuild = previewBuilds.first else { return [] }
+
+ var groups: [HomeBuildGroup] = []
+ var currentIdentity = HomeBuildGroup.Identity(build: firstBuild)
+ var currentBuilds: [HomeBuildItem] = []
+
+ for build in previewBuilds {
+ let identity = HomeBuildGroup.Identity(build: build)
+ if identity == currentIdentity {
+ currentBuilds.append(build)
+ } else {
+ groups.append(HomeBuildGroup(identity: currentIdentity, builds: currentBuilds))
+ currentIdentity = identity
+ currentBuilds = [build]
}
}
+
+ if !currentBuilds.isEmpty {
+ groups.append(HomeBuildGroup(identity: currentIdentity, builds: currentBuilds))
+ }
+
+ return groups
}
@ViewBuilder
@@ -387,6 +439,7 @@ private struct HomeProjectsListView: View {
private struct HomeBuildRow: View {
@Environment(AppState.self) private var appState
let build: HomeBuildItem
+ var showsRepositoryLink = true
var body: some View {
VStack(alignment: .leading, spacing: 6) {
@@ -395,7 +448,7 @@ private struct HomeBuildRow: View {
.frame(width: 20)
VStack(alignment: .leading, spacing: 4) {
- Text(primaryTitle)
+ Text(build.job.displayLabel)
.font(.subheadline.weight(.medium))
.lineLimit(1)
@@ -408,7 +461,7 @@ private struct HomeBuildRow: View {
.font(.caption)
.foregroundStyle(.tertiary)
- Text(build.job.status.rawValue.capitalized)
+ Text(build.job.status.displayTitle)
.font(.caption)
.foregroundStyle(.secondary)
@@ -425,7 +478,7 @@ private struct HomeBuildRow: View {
}
}
- if let repositoryDisplayName = build.repositoryDisplayName {
+ if showsRepositoryLink, let repositoryDisplayName = build.repositoryDisplayName {
Button {
openRepository()
} label: {
@@ -439,13 +492,6 @@ private struct HomeBuildRow: View {
.padding(.vertical, 2)
}
- private var primaryTitle: String {
- if let repositoryDisplayName = build.repositoryDisplayName {
- return repositoryDisplayName
- }
- return build.job.displayLabel
- }
-
private func openRepository() {
guard let repositoryName = build.repositoryName,
let repositoryOwner = build.repositoryOwner else { return }
@@ -463,6 +509,68 @@ private struct HomeBuildRow: View {
}
}
+private struct HomeBuildGroup: Identifiable {
+ enum Identity: Hashable {
+ case repository(owner: String?, name: String)
+ case standalone(Int)
+
+ init(build: HomeBuildItem) {
+ if let repositoryName = build.repositoryName {
+ self = .repository(owner: build.repositoryOwner, name: repositoryName)
+ } else {
+ self = .standalone(build.id)
+ }
+ }
+ }
+
+ let identity: Identity
+ let builds: [HomeBuildItem]
+
+ var id: String {
+ switch identity {
+ case .repository(let owner, let name):
+ return "\(owner ?? "_")/\(name)#\(builds.first?.id ?? 0)"
+ case .standalone(let jobId):
+ return "job-\(jobId)"
+ }
+ }
+
+ var repositoryDisplayName: String? {
+ builds.first?.repositoryDisplayName
+ }
+
+ var latestStatus: JobStatus {
+ builds.first?.job.status ?? .pending
+ }
+}
+
+private struct HomeBuildGroupHeader: View {
+ let repositoryDisplayName: String
+ let buildCount: Int
+ let latestStatus: JobStatus
+
+ var body: some View {
+ HStack(spacing: 12) {
+ Label(repositoryDisplayName, systemImage: "book.closed")
+ .font(.caption.weight(.medium))
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+
+ Spacer(minLength: 8)
+
+ Text("\(buildCount) \(buildCount == 1 ? "build" : "builds")")
+ .font(.caption2.weight(.medium))
+ .foregroundStyle(.tertiary)
+
+ JobStatusBadge(status: latestStatus)
+ }
+ .padding(.top, 4)
+ .listRowInsets(EdgeInsets(top: 8, leading: 20, bottom: 0, trailing: 20))
+ .listRowSeparator(.hidden)
+ .accessibilityElement(children: .combine)
+ }
+}
+
private struct HomeAssignedTicketRow: View {
@Environment(AppState.self) private var appState
let ticket: HomeAssignedTicket
@@ -717,27 +825,6 @@ private struct HomeAttentionLinkRow<Destination: View>: View {
}
}
-private struct HomeSectionActionHeader: View {
- let title: String
- let action: () -> Void
-
- init(_ title: String, action: @escaping () -> Void) {
- self.title = title
- self.action = action
- }
-
- var body: some View {
- HStack {
- Text(title)
- Spacer()
- Button("See All", action: action)
- .font(.caption.weight(.medium))
- .buttonStyle(.plain)
- }
- .textCase(nil)
- }
-}
-
private struct HomeAssignedTicketsListView: View {
let viewModel: HomeViewModel
@AppStorage(AppStorageKeys.swipeActionsEnabled) private var swipeActionsEnabled = true
diff --git a/labels.sh b/labels.sh
new file mode 100755
index 0000000..746e0c8
--- /dev/null
+++ b/labels.sh
@@ -0,0 +1,96 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+TRACKER="hutch"
+ERR_FILE="$(mktemp)"
+trap 'rm -f "$ERR_FILE"' EXIT
+
+label_ticket() {
+ local ticket_id="$1"
+ shift
+
+ for label in "$@"; do
+ echo "Applying label '$label' to ticket #$ticket_id"
+ if ! hut todo ticket label "$ticket_id" -t "$TRACKER" -l "$label" 2>"$ERR_FILE"; then
+ if grep -q "already assigned to this ticket" "$ERR_FILE"; then
+ echo " already present, skipping"
+ else
+ cat "$ERR_FILE" >&2
+ exit 1
+ fi
+ fi
+ done
+}
+
+# v2.16.x
+label_ticket 14 enhancement ui ux # Home: Collapsible Sections
+label_ticket 15 enhancement ui # Home: See All Navigation
+label_ticket 16 enhancement ui ux # Home: Repo-Based Grouping
+
+label_ticket 17 enhancement builds # Builds: Auto-Refresh Toggle
+label_ticket 18 enhancement builds # Builds: Per-Repo Filter
+label_ticket 19 enhancement builds ux # Builds: Retry/Cancel Polish
+
+label_ticket 20 enhancement tickets ui # Tickets: Swipe Actions
+label_ticket 21 enhancement tickets ui # Tickets: Status and Label Visibility
+
+label_ticket 22 bug inbox ui # Inbox: Patch Rendering Fixes
+label_ticket 23 enhancement inbox ux # Inbox: Reply Flow Polish
+
+label_ticket 24 new-feature theming ui # Theming: AMOLED Mode
+label_ticket 25 new-feature theming ui # Theming: High-Density Mode
+
+# v2.17.x
+label_ticket 26 new-feature projects # Projects: Project List View
+label_ticket 27 new-feature projects # Projects: Project Detail View
+label_ticket 28 enhancement projects ui # Projects: Pin to Home
+
+label_ticket 29 enhancement builds # Builds: Log Search
+label_ticket 30 enhancement builds # Builds: Jump to Error
+label_ticket 31 enhancement builds # Builds: Artifact List
+
+label_ticket 32 enhancement tickets # Tickets: Saved Filters
+label_ticket 33 enhancement tickets # Tickets: Label Filtering
+
+label_ticket 34 enhancement inbox # Inbox: Basic Threading
+label_ticket 35 enhancement inbox # Inbox: Collapse Long Diffs
+
+# v2.18.x
+label_ticket 36 new-feature projects # Projects: Create Project
+label_ticket 37 enhancement projects # Projects: Edit Project
+label_ticket 38 enhancement projects # Projects: Manage Linked Resources
+
+label_ticket 39 new-feature acl repo # ACL: View Permissions
+label_ticket 40 enhancement acl repo # ACL: Add and Remove Users
+label_ticket 41 enhancement acl repo # ACL: Edit Permissions
+
+label_ticket 42 enhancement repo # Repo Settings: Edit Metadata
+label_ticket 43 enhancement repo # Repo Settings: Set Default Branch
+label_ticket 44 enhancement repo # Repo Settings: Visibility Changes
+
+label_ticket 45 new-feature tickets # Tickets: Label CRUD
+label_ticket 46 enhancement tickets # Tickets: Bulk Actions
+
+# v2.19.x
+label_ticket 47 new-feature search # Search: Per-Type Search
+label_ticket 48 enhancement search # Search: Recent Searches
+
+label_ticket 49 new-feature accounts # Accounts: Multi-Account Support
+label_ticket 50 enhancement accounts ui # Accounts: Account Switcher UI
+label_ticket 51 enhancement accounts performance # Accounts: Isolated Caching
+
+label_ticket 52 enhancement power # Power: Open in Browser
+label_ticket 53 enhancement power # Power: Copy Actions
+label_ticket 54 enhancement power # Power: Debug View Toggle
+
+# v2.20.x
+label_ticket 55 enhancement navigation ux # Navigation: Merge Inbox and Tickets Evaluation
+
+label_ticket 56 enhancement performance # Performance: Caching Improvements
+label_ticket 57 enhancement performance # Performance: List Virtualization
+
+label_ticket 58 enhancement repo # Consistency: Git and Mercurial Parity
+
+label_ticket 59 bug api # Errors: Normalize GraphQL Handling
+
+echo "Done."