From 05933df5c07b31e5092bc5ec99055023cb15673c Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Tue, 17 Feb 2026 15:54:57 -0800 Subject: [PATCH 1/6] Remove total_count and deprecated_count in pagination results --- src/commands/blueprint/list.tsx | 2 +- src/commands/devbox/list.tsx | 2 +- src/commands/gateway-config/list.tsx | 2 +- src/commands/network-policy/list.tsx | 2 +- src/commands/object/list.tsx | 3 +-- src/commands/snapshot/list.tsx | 2 +- src/hooks/useCursorPagination.ts | 7 ++++--- src/services/benchmarkJobService.ts | 2 +- src/services/benchmarkService.ts | 8 ++++---- src/services/blueprintService.ts | 2 +- src/services/devboxService.ts | 2 +- src/services/gatewayConfigService.ts | 2 +- src/services/networkPolicyService.ts | 2 +- src/services/objectService.ts | 3 +-- src/services/scenarioService.ts | 2 +- src/services/snapshotService.ts | 2 +- 16 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/commands/blueprint/list.tsx b/src/commands/blueprint/list.tsx index 7a50e627..e846247c 100644 --- a/src/commands/blueprint/list.tsx +++ b/src/commands/blueprint/list.tsx @@ -148,7 +148,7 @@ const ListBlueprintsUI = ({ const result = { items: pageBlueprints, hasMore: page.has_more || false, - totalCount: page.total_count || pageBlueprints.length, + totalCount: pageBlueprints.length, }; return result; diff --git a/src/commands/devbox/list.tsx b/src/commands/devbox/list.tsx index 7abe0b17..5d269cb9 100644 --- a/src/commands/devbox/list.tsx +++ b/src/commands/devbox/list.tsx @@ -117,7 +117,7 @@ const ListDevboxesUI = ({ const result = { items: pageDevboxes, hasMore: page.has_more || false, - totalCount: page.total_count || pageDevboxes.length, + totalCount: pageDevboxes.length, }; return result; diff --git a/src/commands/gateway-config/list.tsx b/src/commands/gateway-config/list.tsx index 07263668..a4b19465 100644 --- a/src/commands/gateway-config/list.tsx +++ b/src/commands/gateway-config/list.tsx @@ -162,7 +162,7 @@ const ListGatewayConfigsUI = ({ const result = { items: pageConfigs, hasMore: page.has_more || false, - totalCount: page.total_count || pageConfigs.length, + totalCount: pageConfigs.length, }; return result; diff --git a/src/commands/network-policy/list.tsx b/src/commands/network-policy/list.tsx index fa416c09..bd5835ac 100644 --- a/src/commands/network-policy/list.tsx +++ b/src/commands/network-policy/list.tsx @@ -173,7 +173,7 @@ const ListNetworkPoliciesUI = ({ const result = { items: pagePolicies, hasMore: page.has_more || false, - totalCount: page.total_count || pagePolicies.length, + totalCount: pagePolicies.length, }; return result; diff --git a/src/commands/object/list.tsx b/src/commands/object/list.tsx index c4e76cd9..be745a3c 100644 --- a/src/commands/object/list.tsx +++ b/src/commands/object/list.tsx @@ -172,14 +172,13 @@ const ListObjectsUI = ({ // Access pagination properties from the result const pageResult = result as unknown as { objects: unknown[]; - total_count?: number; has_more?: boolean; }; return { items: pageObjects, hasMore: pageResult.has_more || false, - totalCount: pageResult.total_count || pageObjects.length, + totalCount: pageObjects.length, }; }, [search.submittedSearchQuery], diff --git a/src/commands/snapshot/list.tsx b/src/commands/snapshot/list.tsx index ff523a1f..735a0059 100644 --- a/src/commands/snapshot/list.tsx +++ b/src/commands/snapshot/list.tsx @@ -142,7 +142,7 @@ const ListSnapshotsUI = ({ const result = { items: pageSnapshots, hasMore: page.has_more || false, - totalCount: page.total_count || pageSnapshots.length, + totalCount: pageSnapshots.length, }; return result; diff --git a/src/hooks/useCursorPagination.ts b/src/hooks/useCursorPagination.ts index 6c8a1323..49e7b21b 100644 --- a/src/hooks/useCursorPagination.ts +++ b/src/hooks/useCursorPagination.ts @@ -190,9 +190,10 @@ export function useCursorPagination( // Update pagination state setHasMore(result.hasMore); - if (result.totalCount !== undefined) { - setTotalCount(result.totalCount); - } + // Compute cumulative total: items seen on all previous pages + current page. + // This monotonically increases as the user pages forward and is exact when + // hasMore is false (i.e. we've reached the last page). + setTotalCount(page * pageSizeRef.current + result.items.length); } catch (err) { if (!isMountedRef.current) return; setError(err as Error); diff --git a/src/services/benchmarkJobService.ts b/src/services/benchmarkJobService.ts index 7cfacb5f..43b2bd78 100644 --- a/src/services/benchmarkJobService.ts +++ b/src/services/benchmarkJobService.ts @@ -79,7 +79,7 @@ export async function listBenchmarkJobs( return { jobs, - totalCount: page.total_count || jobs.length, + totalCount: jobs.length, hasMore: page.has_more || false, }; } diff --git a/src/services/benchmarkService.ts b/src/services/benchmarkService.ts index c4d265da..17a164cd 100644 --- a/src/services/benchmarkService.ts +++ b/src/services/benchmarkService.ts @@ -66,7 +66,7 @@ export async function listBenchmarkRuns( return { benchmarkRuns, - totalCount: page.total_count || benchmarkRuns.length, + totalCount: benchmarkRuns.length, hasMore: page.has_more || false, }; } @@ -105,7 +105,7 @@ export async function listScenarioRuns( return { scenarioRuns, - totalCount: page.total_count || scenarioRuns.length, + totalCount: scenarioRuns.length, hasMore: page.has_more || false, }; } @@ -124,7 +124,7 @@ export async function listScenarioRuns( return { scenarioRuns, - totalCount: page.total_count || scenarioRuns.length, + totalCount: scenarioRuns.length, hasMore: page.has_more || false, }; } @@ -166,7 +166,7 @@ export async function listBenchmarks( return { benchmarks, - totalCount: page.total_count || benchmarks.length, + totalCount: benchmarks.length, hasMore: page.has_more || false, }; } diff --git a/src/services/blueprintService.ts b/src/services/blueprintService.ts index 7e1b8f19..d0be6f6e 100644 --- a/src/services/blueprintService.ts +++ b/src/services/blueprintService.ts @@ -79,7 +79,7 @@ export async function listBlueprints( const result = { blueprints, - totalCount: page.total_count || blueprints.length, + totalCount: blueprints.length, hasMore: page.has_more || false, }; diff --git a/src/services/devboxService.ts b/src/services/devboxService.ts index 4f0ce8cc..8c93b5da 100644 --- a/src/services/devboxService.ts +++ b/src/services/devboxService.ts @@ -130,7 +130,7 @@ export async function listDevboxes( const result = { devboxes, - totalCount: page.total_count || devboxes.length, + totalCount: devboxes.length, hasMore: page.has_more || false, }; diff --git a/src/services/gatewayConfigService.ts b/src/services/gatewayConfigService.ts index b8849ca0..e72a9325 100644 --- a/src/services/gatewayConfigService.ts +++ b/src/services/gatewayConfigService.ts @@ -73,7 +73,7 @@ export async function listGatewayConfigs( const result = { gatewayConfigs, - totalCount: page.total_count || gatewayConfigs.length, + totalCount: gatewayConfigs.length, hasMore: page.has_more || false, }; diff --git a/src/services/networkPolicyService.ts b/src/services/networkPolicyService.ts index 6727581a..1a12ee70 100644 --- a/src/services/networkPolicyService.ts +++ b/src/services/networkPolicyService.ts @@ -72,7 +72,7 @@ export async function listNetworkPolicies( const result = { networkPolicies, - totalCount: page.total_count || networkPolicies.length, + totalCount: networkPolicies.length, hasMore: page.has_more || false, }; diff --git a/src/services/objectService.ts b/src/services/objectService.ts index 2195cb22..b4c2647b 100644 --- a/src/services/objectService.ts +++ b/src/services/objectService.ts @@ -77,13 +77,12 @@ export async function listObjects( // Access pagination properties from the result const pageResult = result as unknown as { objects: unknown[]; - total_count?: number; has_more?: boolean; }; return { objects, - totalCount: pageResult.total_count || objects.length, + totalCount: objects.length, hasMore: pageResult.has_more || false, }; } diff --git a/src/services/scenarioService.ts b/src/services/scenarioService.ts index 6a413263..0b322095 100644 --- a/src/services/scenarioService.ts +++ b/src/services/scenarioService.ts @@ -47,7 +47,7 @@ export async function listScenarios( return { scenarios, - totalCount: page.total_count || scenarios.length, + totalCount: scenarios.length, hasMore: page.has_more || false, }; } diff --git a/src/services/snapshotService.ts b/src/services/snapshotService.ts index bc8e8f02..5bb93b0e 100644 --- a/src/services/snapshotService.ts +++ b/src/services/snapshotService.ts @@ -90,7 +90,7 @@ export async function listSnapshots( const result = { snapshots, - totalCount: page.total_count || snapshots.length, + totalCount: snapshots.length, hasMore: page.has_more || false, }; From a570a9341098524db5e2a3b28cd285d61fa24f4c Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Tue, 17 Feb 2026 16:14:08 -0800 Subject: [PATCH 2/6] Clarify that totalPage isn't total size, use plus symbol for hasMore --- src/commands/blueprint/list.tsx | 8 ++++---- src/commands/devbox/list.tsx | 6 +++--- src/commands/gateway-config/list.tsx | 8 ++++---- src/commands/network-policy/list.tsx | 8 ++++---- src/commands/object/list.tsx | 8 ++++---- src/commands/secret/list.tsx | 6 +++--- src/commands/snapshot/list.tsx | 8 ++++---- 7 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/commands/blueprint/list.tsx b/src/commands/blueprint/list.tsx index e846247c..0f7c9b24 100644 --- a/src/commands/blueprint/list.tsx +++ b/src/commands/blueprint/list.tsx @@ -875,7 +875,7 @@ const ListBlueprintsUI = ({ data={blueprints} keyExtractor={(blueprint: BlueprintListItem) => blueprint.id} selectedIndex={selectedIndex} - title={`blueprints[${totalCount}]`} + title={`blueprints[${hasMore ? `${totalCount}+` : totalCount}]`} columns={blueprintColumns} emptyState={ @@ -889,7 +889,7 @@ const ListBlueprintsUI = ({ {!showPopup && ( - {figures.hamburger} {totalCount} + {figures.hamburger} {hasMore ? `${totalCount}+` : totalCount} {" "} @@ -907,7 +907,7 @@ const ListBlueprintsUI = ({ ) : ( - Page {currentPage + 1} of {totalPages} + Page {currentPage + 1} of {hasMore ? `${totalPages}+` : totalPages} )} @@ -917,7 +917,7 @@ const ListBlueprintsUI = ({ •{" "} - Showing {startIndex + 1}-{endIndex} of {totalCount} + Showing {startIndex + 1}-{endIndex} of {hasMore ? `${totalCount}+` : totalCount} {search.submittedSearchQuery && ( <> diff --git a/src/commands/devbox/list.tsx b/src/commands/devbox/list.tsx index 5d269cb9..f9339a44 100644 --- a/src/commands/devbox/list.tsx +++ b/src/commands/devbox/list.tsx @@ -712,7 +712,7 @@ const ListDevboxesUI = ({ {!showPopup && ( - {figures.hamburger} {totalCount} + {figures.hamburger} {hasMore ? `${totalCount}+` : totalCount} {" "} @@ -730,7 +730,7 @@ const ListDevboxesUI = ({ ) : ( - Page {currentPage + 1} of {totalPages} + Page {currentPage + 1} of {hasMore ? `${totalPages}+` : totalPages} )} @@ -740,7 +740,7 @@ const ListDevboxesUI = ({ •{" "} - Showing {startIndex + 1}-{endIndex} of {totalCount} + Showing {startIndex + 1}-{endIndex} of {hasMore ? `${totalCount}+` : totalCount} {search.submittedSearchQuery && ( <> diff --git a/src/commands/gateway-config/list.tsx b/src/commands/gateway-config/list.tsx index a4b19465..6bdbf529 100644 --- a/src/commands/gateway-config/list.tsx +++ b/src/commands/gateway-config/list.tsx @@ -630,7 +630,7 @@ const ListGatewayConfigsUI = ({ data={configs} keyExtractor={(config: GatewayConfigListItem) => config.id} selectedIndex={selectedIndex} - title={`gateway_configs[${totalCount}]`} + title={`gateway_configs[${hasMore ? `${totalCount}+` : totalCount}]`} columns={columns} emptyState={ @@ -645,7 +645,7 @@ const ListGatewayConfigsUI = ({ {!showPopup && ( - {figures.hamburger} {totalCount} + {figures.hamburger} {hasMore ? `${totalCount}+` : totalCount} {" "} @@ -663,7 +663,7 @@ const ListGatewayConfigsUI = ({ ) : ( - Page {currentPage + 1} of {totalPages} + Page {currentPage + 1} of {hasMore ? `${totalPages}+` : totalPages} )} @@ -673,7 +673,7 @@ const ListGatewayConfigsUI = ({ •{" "} - Showing {startIndex + 1}-{endIndex} of {totalCount} + Showing {startIndex + 1}-{endIndex} of {hasMore ? `${totalCount}+` : totalCount} {search.submittedSearchQuery && ( <> diff --git a/src/commands/network-policy/list.tsx b/src/commands/network-policy/list.tsx index bd5835ac..fbefc602 100644 --- a/src/commands/network-policy/list.tsx +++ b/src/commands/network-policy/list.tsx @@ -660,7 +660,7 @@ const ListNetworkPoliciesUI = ({ data={policies} keyExtractor={(policy: NetworkPolicyListItem) => policy.id} selectedIndex={selectedIndex} - title={`network_policies[${totalCount}]`} + title={`network_policies[${hasMore ? `${totalCount}+` : totalCount}]`} columns={columns} emptyState={ @@ -674,7 +674,7 @@ const ListNetworkPoliciesUI = ({ {!showPopup && ( - {figures.hamburger} {totalCount} + {figures.hamburger} {hasMore ? `${totalCount}+` : totalCount} {" "} @@ -692,7 +692,7 @@ const ListNetworkPoliciesUI = ({ ) : ( - Page {currentPage + 1} of {totalPages} + Page {currentPage + 1} of {hasMore ? `${totalPages}+` : totalPages} )} @@ -702,7 +702,7 @@ const ListNetworkPoliciesUI = ({ •{" "} - Showing {startIndex + 1}-{endIndex} of {totalCount} + Showing {startIndex + 1}-{endIndex} of {hasMore ? `${totalCount}+` : totalCount} {search.submittedSearchQuery && ( <> diff --git a/src/commands/object/list.tsx b/src/commands/object/list.tsx index be745a3c..38200463 100644 --- a/src/commands/object/list.tsx +++ b/src/commands/object/list.tsx @@ -705,7 +705,7 @@ const ListObjectsUI = ({ data={objects} keyExtractor={(obj: ObjectListItem) => obj.id} selectedIndex={selectedIndex} - title={`storage_objects[${totalCount}]`} + title={`storage_objects[${hasMore ? `${totalCount}+` : totalCount}]`} columns={columns} emptyState={ @@ -720,7 +720,7 @@ const ListObjectsUI = ({ {!showPopup && ( - {figures.hamburger} {totalCount} + {figures.hamburger} {hasMore ? `${totalCount}+` : totalCount} {" "} @@ -738,7 +738,7 @@ const ListObjectsUI = ({ ) : ( - Page {currentPage + 1} of {totalPages} + Page {currentPage + 1} of {hasMore ? `${totalPages}+` : totalPages} )} @@ -748,7 +748,7 @@ const ListObjectsUI = ({ •{" "} - Showing {startIndex + 1}-{endIndex} of {totalCount} + Showing {startIndex + 1}-{endIndex} of {hasMore ? `${totalCount}+` : totalCount} {search.submittedSearchQuery && ( <> diff --git a/src/commands/secret/list.tsx b/src/commands/secret/list.tsx index 48fe7eca..70bf7598 100644 --- a/src/commands/secret/list.tsx +++ b/src/commands/secret/list.tsx @@ -525,7 +525,7 @@ const ListSecretsUI = ({ {!showPopup && ( - {figures.hamburger} {totalCount} + {figures.hamburger} {hasMore ? `${totalCount}+` : totalCount} {" "} @@ -543,7 +543,7 @@ const ListSecretsUI = ({ ) : ( - Page {currentPage + 1} of {totalPages} + Page {currentPage + 1} of {hasMore ? `${totalPages}+` : totalPages} )} @@ -553,7 +553,7 @@ const ListSecretsUI = ({ •{" "} - Showing {startIndex + 1}-{endIndex} of {totalCount} + Showing {startIndex + 1}-{endIndex} of {hasMore ? `${totalCount}+` : totalCount} {search.submittedSearchQuery && ( <> diff --git a/src/commands/snapshot/list.tsx b/src/commands/snapshot/list.tsx index 735a0059..1c3d7c3b 100644 --- a/src/commands/snapshot/list.tsx +++ b/src/commands/snapshot/list.tsx @@ -576,7 +576,7 @@ const ListSnapshotsUI = ({ data={snapshots} keyExtractor={(snapshot: SnapshotListItem) => snapshot.id} selectedIndex={selectedIndex} - title={`snapshots[${totalCount}]`} + title={`snapshots[${hasMore ? `${totalCount}+` : totalCount}]`} columns={columns} emptyState={ @@ -591,7 +591,7 @@ const ListSnapshotsUI = ({ {!showPopup && ( - {figures.hamburger} {totalCount} + {figures.hamburger} {hasMore ? `${totalCount}+` : totalCount} {" "} @@ -609,7 +609,7 @@ const ListSnapshotsUI = ({ ) : ( - Page {currentPage + 1} of {totalPages} + Page {currentPage + 1} of {hasMore ? `${totalPages}+` : totalPages} )} @@ -619,7 +619,7 @@ const ListSnapshotsUI = ({ •{" "} - Showing {startIndex + 1}-{endIndex} of {totalCount} + Showing {startIndex + 1}-{endIndex} of {hasMore ? `${totalCount}+` : totalCount} {search.submittedSearchQuery && ( <> From 8332fba2668f5767369f2d9e1b937977d7ac4945 Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Tue, 17 Feb 2026 16:24:25 -0800 Subject: [PATCH 3/6] format --- src/commands/blueprint/list.tsx | 6 ++++-- src/commands/devbox/list.tsx | 6 ++++-- src/commands/gateway-config/list.tsx | 6 ++++-- src/commands/network-policy/list.tsx | 6 ++++-- src/commands/object/list.tsx | 6 ++++-- src/commands/secret/list.tsx | 6 ++++-- src/commands/snapshot/list.tsx | 6 ++++-- 7 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/commands/blueprint/list.tsx b/src/commands/blueprint/list.tsx index 0f7c9b24..0e71fcab 100644 --- a/src/commands/blueprint/list.tsx +++ b/src/commands/blueprint/list.tsx @@ -907,7 +907,8 @@ const ListBlueprintsUI = ({ ) : ( - Page {currentPage + 1} of {hasMore ? `${totalPages}+` : totalPages} + Page {currentPage + 1} of{" "} + {hasMore ? `${totalPages}+` : totalPages} )} @@ -917,7 +918,8 @@ const ListBlueprintsUI = ({ •{" "} - Showing {startIndex + 1}-{endIndex} of {hasMore ? `${totalCount}+` : totalCount} + Showing {startIndex + 1}-{endIndex} of{" "} + {hasMore ? `${totalCount}+` : totalCount} {search.submittedSearchQuery && ( <> diff --git a/src/commands/devbox/list.tsx b/src/commands/devbox/list.tsx index f9339a44..bcdb220f 100644 --- a/src/commands/devbox/list.tsx +++ b/src/commands/devbox/list.tsx @@ -730,7 +730,8 @@ const ListDevboxesUI = ({ ) : ( - Page {currentPage + 1} of {hasMore ? `${totalPages}+` : totalPages} + Page {currentPage + 1} of{" "} + {hasMore ? `${totalPages}+` : totalPages} )} @@ -740,7 +741,8 @@ const ListDevboxesUI = ({ •{" "} - Showing {startIndex + 1}-{endIndex} of {hasMore ? `${totalCount}+` : totalCount} + Showing {startIndex + 1}-{endIndex} of{" "} + {hasMore ? `${totalCount}+` : totalCount} {search.submittedSearchQuery && ( <> diff --git a/src/commands/gateway-config/list.tsx b/src/commands/gateway-config/list.tsx index 6bdbf529..be9bba93 100644 --- a/src/commands/gateway-config/list.tsx +++ b/src/commands/gateway-config/list.tsx @@ -663,7 +663,8 @@ const ListGatewayConfigsUI = ({ ) : ( - Page {currentPage + 1} of {hasMore ? `${totalPages}+` : totalPages} + Page {currentPage + 1} of{" "} + {hasMore ? `${totalPages}+` : totalPages} )} @@ -673,7 +674,8 @@ const ListGatewayConfigsUI = ({ •{" "} - Showing {startIndex + 1}-{endIndex} of {hasMore ? `${totalCount}+` : totalCount} + Showing {startIndex + 1}-{endIndex} of{" "} + {hasMore ? `${totalCount}+` : totalCount} {search.submittedSearchQuery && ( <> diff --git a/src/commands/network-policy/list.tsx b/src/commands/network-policy/list.tsx index fbefc602..0d2afe78 100644 --- a/src/commands/network-policy/list.tsx +++ b/src/commands/network-policy/list.tsx @@ -692,7 +692,8 @@ const ListNetworkPoliciesUI = ({ ) : ( - Page {currentPage + 1} of {hasMore ? `${totalPages}+` : totalPages} + Page {currentPage + 1} of{" "} + {hasMore ? `${totalPages}+` : totalPages} )} @@ -702,7 +703,8 @@ const ListNetworkPoliciesUI = ({ •{" "} - Showing {startIndex + 1}-{endIndex} of {hasMore ? `${totalCount}+` : totalCount} + Showing {startIndex + 1}-{endIndex} of{" "} + {hasMore ? `${totalCount}+` : totalCount} {search.submittedSearchQuery && ( <> diff --git a/src/commands/object/list.tsx b/src/commands/object/list.tsx index 38200463..d3fa9b03 100644 --- a/src/commands/object/list.tsx +++ b/src/commands/object/list.tsx @@ -738,7 +738,8 @@ const ListObjectsUI = ({ ) : ( - Page {currentPage + 1} of {hasMore ? `${totalPages}+` : totalPages} + Page {currentPage + 1} of{" "} + {hasMore ? `${totalPages}+` : totalPages} )} @@ -748,7 +749,8 @@ const ListObjectsUI = ({ •{" "} - Showing {startIndex + 1}-{endIndex} of {hasMore ? `${totalCount}+` : totalCount} + Showing {startIndex + 1}-{endIndex} of{" "} + {hasMore ? `${totalCount}+` : totalCount} {search.submittedSearchQuery && ( <> diff --git a/src/commands/secret/list.tsx b/src/commands/secret/list.tsx index 70bf7598..0710f9a1 100644 --- a/src/commands/secret/list.tsx +++ b/src/commands/secret/list.tsx @@ -543,7 +543,8 @@ const ListSecretsUI = ({ ) : ( - Page {currentPage + 1} of {hasMore ? `${totalPages}+` : totalPages} + Page {currentPage + 1} of{" "} + {hasMore ? `${totalPages}+` : totalPages} )} @@ -553,7 +554,8 @@ const ListSecretsUI = ({ •{" "} - Showing {startIndex + 1}-{endIndex} of {hasMore ? `${totalCount}+` : totalCount} + Showing {startIndex + 1}-{endIndex} of{" "} + {hasMore ? `${totalCount}+` : totalCount} {search.submittedSearchQuery && ( <> diff --git a/src/commands/snapshot/list.tsx b/src/commands/snapshot/list.tsx index 1c3d7c3b..2852473c 100644 --- a/src/commands/snapshot/list.tsx +++ b/src/commands/snapshot/list.tsx @@ -609,7 +609,8 @@ const ListSnapshotsUI = ({ ) : ( - Page {currentPage + 1} of {hasMore ? `${totalPages}+` : totalPages} + Page {currentPage + 1} of{" "} + {hasMore ? `${totalPages}+` : totalPages} )} @@ -619,7 +620,8 @@ const ListSnapshotsUI = ({ •{" "} - Showing {startIndex + 1}-{endIndex} of {hasMore ? `${totalCount}+` : totalCount} + Showing {startIndex + 1}-{endIndex} of{" "} + {hasMore ? `${totalCount}+` : totalCount} {search.submittedSearchQuery && ( <> From 9adc529e9d1f2c957d7ba3b569235a442b926795 Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Wed, 18 Feb 2026 16:30:56 -0800 Subject: [PATCH 4/6] fix pagination, add --limit option for listing snapshots and blueprints --- src/commands/blueprint/list.tsx | 51 +++++++++++++------ src/commands/devbox/list.tsx | 50 +++++++++++++------ src/commands/gateway-config/list.tsx | 50 +++++++++++++------ src/commands/network-policy/list.tsx | 51 +++++++++++++------ src/commands/object/list.tsx | 66 +++++++++++++++++-------- src/commands/snapshot/list.tsx | 74 ++++++++++++++++++---------- src/utils/commands.ts | 2 + 7 files changed, 242 insertions(+), 102 deletions(-) diff --git a/src/commands/blueprint/list.tsx b/src/commands/blueprint/list.tsx index 0e71fcab..2168883e 100644 --- a/src/commands/blueprint/list.tsx +++ b/src/commands/blueprint/list.tsx @@ -984,6 +984,7 @@ const ListBlueprintsUI = ({ interface ListBlueprintsOptions { name?: string; + limit?: string; output?: string; } @@ -994,23 +995,45 @@ export async function listBlueprints(options: ListBlueprintsOptions = {}) { try { const client = getClient(); - // Build query params - const queryParams: Record = { - limit: DEFAULT_PAGE_SIZE, - }; - if (options.name) { - queryParams.name = options.name; - } + const maxResults = options.limit ? parseInt(options.limit, 10) : Infinity; + const allBlueprints: unknown[] = []; + let startingAfter: string | undefined; - // Fetch blueprints - const page = (await client.blueprints.list( - queryParams, - )) as BlueprintsCursorIDPage<{ id: string }>; + do { + const remaining = maxResults - allBlueprints.length; + // Build query params + const queryParams: Record = { + limit: Math.min(DEFAULT_PAGE_SIZE, remaining), + }; + if (options.name) { + queryParams.name = options.name; + } + if (startingAfter) { + queryParams.starting_after = startingAfter; + } - // Extract blueprints array - const blueprints = page.blueprints || []; + // Fetch one page + const page = (await client.blueprints.list( + queryParams, + )) as BlueprintsCursorIDPage<{ id: string }>; + + const pageBlueprints = page.blueprints || []; + allBlueprints.push(...pageBlueprints); + + if ( + page.has_more && + pageBlueprints.length > 0 && + allBlueprints.length < maxResults + ) { + startingAfter = ( + pageBlueprints[pageBlueprints.length - 1] as { id: string } + ).id; + } else { + startingAfter = undefined; + } + } while (startingAfter !== undefined); - output(blueprints, { format: options.output, defaultFormat: "json" }); + output(allBlueprints, { format: options.output, defaultFormat: "json" }); } catch (error) { outputError("Failed to list blueprints", error); } diff --git a/src/commands/devbox/list.tsx b/src/commands/devbox/list.tsx index bcdb220f..520b219a 100644 --- a/src/commands/devbox/list.tsx +++ b/src/commands/devbox/list.tsx @@ -798,23 +798,45 @@ export async function listDevboxes(options: ListOptions) { try { const client = getClient(); - // Build query params - const queryParams: Record = { - limit: options.limit ? parseInt(options.limit, 10) : DEFAULT_PAGE_SIZE, - }; - if (options.status) { - queryParams.status = options.status; - } + const maxResults = options.limit ? parseInt(options.limit, 10) : Infinity; + const allDevboxes: unknown[] = []; + let startingAfter: string | undefined; - // Fetch devboxes - const page = (await client.devboxes.list( - queryParams, - )) as DevboxesCursorIDPage<{ id: string }>; + do { + const remaining = maxResults - allDevboxes.length; + // Build query params + const queryParams: Record = { + limit: Math.min(DEFAULT_PAGE_SIZE, remaining), + }; + if (options.status) { + queryParams.status = options.status; + } + if (startingAfter) { + queryParams.starting_after = startingAfter; + } - // Extract devboxes array - const devboxes = page.devboxes || []; + // Fetch one page + const page = (await client.devboxes.list( + queryParams, + )) as DevboxesCursorIDPage<{ id: string }>; + + const pageDevboxes = page.devboxes || []; + allDevboxes.push(...pageDevboxes); + + if ( + page.has_more && + pageDevboxes.length > 0 && + allDevboxes.length < maxResults + ) { + startingAfter = ( + pageDevboxes[pageDevboxes.length - 1] as { id: string } + ).id; + } else { + startingAfter = undefined; + } + } while (startingAfter !== undefined); - output(devboxes, { format: options.output, defaultFormat: "json" }); + output(allDevboxes, { format: options.output, defaultFormat: "json" }); } catch (error) { outputError("Failed to list devboxes", error); } diff --git a/src/commands/gateway-config/list.tsx b/src/commands/gateway-config/list.tsx index be9bba93..c5b95084 100644 --- a/src/commands/gateway-config/list.tsx +++ b/src/commands/gateway-config/list.tsx @@ -26,6 +26,7 @@ import { ConfirmationPrompt } from "../../components/ConfirmationPrompt.js"; interface ListOptions { name?: string; + limit?: string; output?: string; } @@ -746,23 +747,44 @@ export async function listGatewayConfigs(options: ListOptions = {}) { try { const client = getClient(); - // Build query params - const queryParams: Record = { - limit: DEFAULT_PAGE_SIZE, - }; - if (options.name) { - queryParams.name = options.name; - } + const maxResults = options.limit ? parseInt(options.limit, 10) : Infinity; + const allConfigs: unknown[] = []; + let startingAfter: string | undefined; - // Fetch gateway configs - const page = (await client.gatewayConfigs.list( - queryParams, - )) as GatewayConfigsCursorIDPage<{ id: string }>; + do { + const remaining = maxResults - allConfigs.length; + // Build query params + const queryParams: Record = { + limit: Math.min(DEFAULT_PAGE_SIZE, remaining), + }; + if (options.name) { + queryParams.name = options.name; + } + if (startingAfter) { + queryParams.starting_after = startingAfter; + } - // Extract gateway configs array - const gatewayConfigs = page.gateway_configs || []; + // Fetch one page + const page = (await client.gatewayConfigs.list( + queryParams, + )) as GatewayConfigsCursorIDPage<{ id: string }>; + + const pageConfigs = page.gateway_configs || []; + allConfigs.push(...pageConfigs); + + if ( + page.has_more && + pageConfigs.length > 0 && + allConfigs.length < maxResults + ) { + startingAfter = (pageConfigs[pageConfigs.length - 1] as { id: string }) + .id; + } else { + startingAfter = undefined; + } + } while (startingAfter !== undefined); - output(gatewayConfigs, { format: options.output, defaultFormat: "json" }); + output(allConfigs, { format: options.output, defaultFormat: "json" }); } catch (error) { outputError("Failed to list gateway configs", error); } diff --git a/src/commands/network-policy/list.tsx b/src/commands/network-policy/list.tsx index 0d2afe78..3955b45c 100644 --- a/src/commands/network-policy/list.tsx +++ b/src/commands/network-policy/list.tsx @@ -26,6 +26,7 @@ import { ConfirmationPrompt } from "../../components/ConfirmationPrompt.js"; interface ListOptions { name?: string; + limit?: string; output?: string; } @@ -775,23 +776,45 @@ export async function listNetworkPolicies(options: ListOptions = {}) { try { const client = getClient(); - // Build query params - const queryParams: Record = { - limit: DEFAULT_PAGE_SIZE, - }; - if (options.name) { - queryParams.name = options.name; - } + const maxResults = options.limit ? parseInt(options.limit, 10) : Infinity; + const allPolicies: unknown[] = []; + let startingAfter: string | undefined; - // Fetch network policies - const page = (await client.networkPolicies.list( - queryParams, - )) as NetworkPoliciesCursorIDPage<{ id: string }>; + do { + const remaining = maxResults - allPolicies.length; + // Build query params + const queryParams: Record = { + limit: Math.min(DEFAULT_PAGE_SIZE, remaining), + }; + if (options.name) { + queryParams.name = options.name; + } + if (startingAfter) { + queryParams.starting_after = startingAfter; + } - // Extract network policies array - const networkPolicies = page.network_policies || []; + // Fetch one page + const page = (await client.networkPolicies.list( + queryParams, + )) as NetworkPoliciesCursorIDPage<{ id: string }>; + + const pagePolicies = page.network_policies || []; + allPolicies.push(...pagePolicies); + + if ( + page.has_more && + pagePolicies.length > 0 && + allPolicies.length < maxResults + ) { + startingAfter = ( + pagePolicies[pagePolicies.length - 1] as { id: string } + ).id; + } else { + startingAfter = undefined; + } + } while (startingAfter !== undefined); - output(networkPolicies, { format: options.output, defaultFormat: "json" }); + output(allPolicies, { format: options.output, defaultFormat: "json" }); } catch (error) { outputError("Failed to list network policies", error); } diff --git a/src/commands/object/list.tsx b/src/commands/object/list.tsx index d3fa9b03..b48f5acf 100644 --- a/src/commands/object/list.tsx +++ b/src/commands/object/list.tsx @@ -30,6 +30,7 @@ interface ListOptions { contentType?: string; state?: string; public?: boolean; + limit?: string; output?: string; } @@ -817,30 +818,53 @@ export async function listObjects(options: ListOptions) { try { const client = getClient(); - // Build query params - const queryParams: Record = { - limit: DEFAULT_PAGE_SIZE, - }; - if (options.name) { - queryParams.name = options.name; - } - if (options.contentType) { - queryParams.content_type = options.contentType; - } - if (options.state) { - queryParams.state = options.state; - } - if (options.public !== undefined) { - queryParams.is_public = options.public; - } + const maxResults = options.limit ? parseInt(options.limit, 10) : Infinity; + const allObjects: unknown[] = []; + let startingAfter: string | undefined; - // Fetch objects - const result = await client.objects.list(queryParams); + do { + const remaining = maxResults - allObjects.length; + // Build query params + const queryParams: Record = { + limit: Math.min(DEFAULT_PAGE_SIZE, remaining), + }; + if (options.name) { + queryParams.name = options.name; + } + if (options.contentType) { + queryParams.content_type = options.contentType; + } + if (options.state) { + queryParams.state = options.state; + } + if (options.public !== undefined) { + queryParams.is_public = options.public; + } + if (startingAfter) { + queryParams.starting_after = startingAfter; + } - // Extract objects array - const objects = result.objects || []; + // Fetch one page + const result = await client.objects.list(queryParams); + const pageResult = result as unknown as { + objects?: { id: string }[]; + has_more?: boolean; + }; + const pageObjects = pageResult.objects || []; + allObjects.push(...pageObjects); + + if ( + pageResult.has_more && + pageObjects.length > 0 && + allObjects.length < maxResults + ) { + startingAfter = pageObjects[pageObjects.length - 1].id; + } else { + startingAfter = undefined; + } + } while (startingAfter !== undefined); - output(objects, { format: options.output, defaultFormat: "json" }); + output(allObjects, { format: options.output, defaultFormat: "json" }); } catch (error) { outputError("Failed to list storage objects", error); } diff --git a/src/commands/snapshot/list.tsx b/src/commands/snapshot/list.tsx index 2852473c..1a938128 100644 --- a/src/commands/snapshot/list.tsx +++ b/src/commands/snapshot/list.tsx @@ -27,6 +27,7 @@ import { ConfirmationPrompt } from "../../components/ConfirmationPrompt.js"; interface ListOptions { devbox?: string; + limit?: string; output?: string; } @@ -688,33 +689,56 @@ export async function listSnapshots(options: ListOptions) { try { const client = getClient(); - // Build query params - const queryParams: Record = { - limit: DEFAULT_PAGE_SIZE, - }; - if (options.devbox) { - queryParams.devbox_id = options.devbox; - } + const maxResults = options.limit ? parseInt(options.limit, 10) : Infinity; + const allSnapshots: ReturnType[] = []; + let startingAfter: string | undefined; + + do { + const remaining = maxResults - allSnapshots.length; + // Build query params + const queryParams: Record = { + limit: Math.min(DEFAULT_PAGE_SIZE, remaining), + }; + if (options.devbox) { + queryParams.devbox_id = options.devbox; + } + if (startingAfter) { + queryParams.starting_after = startingAfter; + } + + // Fetch one page + const page = (await client.devboxes.listDiskSnapshots( + queryParams, + )) as DiskSnapshotsCursorIDPage; + + const pageSnapshots = page.snapshots || []; + allSnapshots.push(...pageSnapshots.map(mapSnapshot)); - // Fetch snapshots - const page = (await client.devboxes.listDiskSnapshots( - queryParams, - )) as DiskSnapshotsCursorIDPage; - - // Extract snapshots array and strip to plain objects to avoid - // camelCase aliases added by the API client library - const snapshots = (page.snapshots || []).map((s) => ({ - id: s.id, - name: s.name ?? undefined, - create_time_ms: s.create_time_ms, - metadata: s.metadata, - source_devbox_id: s.source_devbox_id, - source_blueprint_id: s.source_blueprint_id ?? undefined, - commit_message: s.commit_message ?? undefined, - })); - - output(snapshots, { format: options.output, defaultFormat: "json" }); + if ( + page.has_more && + pageSnapshots.length > 0 && + allSnapshots.length < maxResults + ) { + startingAfter = pageSnapshots[pageSnapshots.length - 1].id; + } else { + startingAfter = undefined; + } + } while (startingAfter !== undefined); + + output(allSnapshots, { format: options.output, defaultFormat: "json" }); } catch (error) { outputError("Failed to list snapshots", error); } } + +function mapSnapshot(s: DevboxSnapshotView) { + return { + id: s.id, + name: s.name ?? undefined, + create_time_ms: s.create_time_ms, + metadata: s.metadata, + source_devbox_id: s.source_devbox_id, + source_blueprint_id: s.source_blueprint_id ?? undefined, + commit_message: s.commit_message ?? undefined, + }; +} diff --git a/src/utils/commands.ts b/src/utils/commands.ts index 1f196c9f..dbf5e3d3 100644 --- a/src/utils/commands.ts +++ b/src/utils/commands.ts @@ -359,6 +359,7 @@ export function createProgram(): Command { .command("list") .description("List all snapshots") .option("-d, --devbox ", "Filter by devbox ID") + .option("-l, --limit ", "Max results", "20") .option( "-o, --output [format]", "Output format: text|json|yaml (default: json)", @@ -446,6 +447,7 @@ export function createProgram(): Command { .command("list") .description("List all blueprints") .option("-n, --name ", "Filter by blueprint name") + .option("-l, --limit ", "Max results", "20") .option( "-o, --output [format]", "Output format: text|json|yaml (default: json)", From c68e86898a35af3755ae8a8c9c25002d2bb6fd16 Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Wed, 18 Feb 2026 16:46:59 -0800 Subject: [PATCH 5/6] more pagination bug fixes --- src/commands/blueprint/list.tsx | 10 +++++++++- src/commands/devbox/list.tsx | 10 +++++++++- src/commands/gateway-config/list.tsx | 20 +++++++++++++++++++- src/commands/network-policy/list.tsx | 20 +++++++++++++++++++- src/commands/object/list.tsx | 20 +++++++++++++++++++- src/commands/secret/list.tsx | 20 +++++++++++++++++++- src/commands/snapshot/list.tsx | 20 +++++++++++++++++++- src/hooks/useCursorPagination.ts | 10 +++++++--- 8 files changed, 120 insertions(+), 10 deletions(-) diff --git a/src/commands/blueprint/list.tsx b/src/commands/blueprint/list.tsx index 2168883e..ea65fe16 100644 --- a/src/commands/blueprint/list.tsx +++ b/src/commands/blueprint/list.tsx @@ -179,7 +179,7 @@ const ListBlueprintsUI = ({ !executingOperation && !showDeleteConfirm && !search.searchMode, - deps: [PAGE_SIZE, search.submittedSearchQuery], + deps: [search.submittedSearchQuery], }); // Memoize columns array @@ -624,10 +624,18 @@ const ListBlueprintsUI = ({ bindings: { up: () => { if (selectedIndex > 0) setSelectedIndex(selectedIndex - 1); + else if (!loading && !navigating && hasPrev) { + prevPage(); + setSelectedIndex(blueprints.length - 1); + } }, down: () => { if (selectedIndex < blueprints.length - 1) setSelectedIndex(selectedIndex + 1); + else if (!loading && !navigating && hasMore) { + nextPage(); + setSelectedIndex(0); + } }, n: goToNextPage, right: goToNextPage, diff --git a/src/commands/devbox/list.tsx b/src/commands/devbox/list.tsx index 520b219a..5e0b6c21 100644 --- a/src/commands/devbox/list.tsx +++ b/src/commands/devbox/list.tsx @@ -148,7 +148,7 @@ const ListDevboxesUI = ({ !showActions && !showPopup && !search.searchMode, - deps: [status, search.submittedSearchQuery, PAGE_SIZE], + deps: [status, search.submittedSearchQuery], }); // Sync devboxes to store for detail screen @@ -559,10 +559,18 @@ const ListDevboxesUI = ({ bindings: { up: () => { if (selectedIndex > 0) setSelectedIndex(selectedIndex - 1); + else if (!loading && !navigating && hasPrev) { + prevPage(); + setSelectedIndex(devboxes.length - 1); + } }, down: () => { if (selectedIndex < devboxes.length - 1) setSelectedIndex(selectedIndex + 1); + else if (!loading && !navigating && hasMore) { + nextPage(); + setSelectedIndex(0); + } }, n: goToNextPage, right: goToNextPage, diff --git a/src/commands/gateway-config/list.tsx b/src/commands/gateway-config/list.tsx index c5b95084..193edfcc 100644 --- a/src/commands/gateway-config/list.tsx +++ b/src/commands/gateway-config/list.tsx @@ -196,7 +196,7 @@ const ListGatewayConfigsUI = ({ !showEditConfig && !showDeleteConfirm && !search.searchMode, - deps: [PAGE_SIZE, search.submittedSearchQuery], + deps: [search.submittedSearchQuery], }); // Operations for a specific gateway config (shown in popup) @@ -429,8 +429,26 @@ const ListGatewayConfigsUI = ({ // Handle list view navigation if (key.upArrow && selectedIndex > 0) { setSelectedIndex(selectedIndex - 1); + } else if ( + key.upArrow && + selectedIndex === 0 && + !loading && + !navigating && + hasPrev + ) { + prevPage(); + setSelectedIndex(pageConfigs - 1); } else if (key.downArrow && selectedIndex < pageConfigs - 1) { setSelectedIndex(selectedIndex + 1); + } else if ( + key.downArrow && + selectedIndex === pageConfigs - 1 && + !loading && + !navigating && + hasMore + ) { + nextPage(); + setSelectedIndex(0); } else if ( (input === "n" || key.rightArrow) && !loading && diff --git a/src/commands/network-policy/list.tsx b/src/commands/network-policy/list.tsx index 3955b45c..f69d447f 100644 --- a/src/commands/network-policy/list.tsx +++ b/src/commands/network-policy/list.tsx @@ -207,7 +207,7 @@ const ListNetworkPoliciesUI = ({ !showEditPolicy && !showDeleteConfirm && !search.searchMode, - deps: [PAGE_SIZE, search.submittedSearchQuery], + deps: [search.submittedSearchQuery], }); // Operations for a specific network policy (shown in popup) @@ -462,8 +462,26 @@ const ListNetworkPoliciesUI = ({ // Handle list view navigation if (key.upArrow && selectedIndex > 0) { setSelectedIndex(selectedIndex - 1); + } else if ( + key.upArrow && + selectedIndex === 0 && + !loading && + !navigating && + hasPrev + ) { + prevPage(); + setSelectedIndex(pagePolicies - 1); } else if (key.downArrow && selectedIndex < pagePolicies - 1) { setSelectedIndex(selectedIndex + 1); + } else if ( + key.downArrow && + selectedIndex === pagePolicies - 1 && + !loading && + !navigating && + hasMore + ) { + nextPage(); + setSelectedIndex(0); } else if ( (input === "n" || key.rightArrow) && !loading && diff --git a/src/commands/object/list.tsx b/src/commands/object/list.tsx index b48f5acf..7449dab0 100644 --- a/src/commands/object/list.tsx +++ b/src/commands/object/list.tsx @@ -209,7 +209,7 @@ const ListObjectsUI = ({ !showDownloadPrompt && !showDeleteConfirm && !search.searchMode, - deps: [PAGE_SIZE, search.submittedSearchQuery], + deps: [search.submittedSearchQuery], }); // Operations for objects @@ -499,8 +499,26 @@ const ListObjectsUI = ({ // Handle list view navigation if (key.upArrow && selectedIndex > 0) { setSelectedIndex(selectedIndex - 1); + } else if ( + key.upArrow && + selectedIndex === 0 && + !loading && + !navigating && + hasPrev + ) { + prevPage(); + setSelectedIndex(pageObjects - 1); } else if (key.downArrow && selectedIndex < pageObjects - 1) { setSelectedIndex(selectedIndex + 1); + } else if ( + key.downArrow && + selectedIndex === pageObjects - 1 && + !loading && + !navigating && + hasMore + ) { + nextPage(); + setSelectedIndex(0); } else if ( (input === "n" || key.rightArrow) && !loading && diff --git a/src/commands/secret/list.tsx b/src/commands/secret/list.tsx index 0710f9a1..43cae06e 100644 --- a/src/commands/secret/list.tsx +++ b/src/commands/secret/list.tsx @@ -167,7 +167,7 @@ const ListSecretsUI = ({ !executingOperation && !showDeleteConfirm && !search.searchMode, - deps: [PAGE_SIZE, search.submittedSearchQuery], + deps: [search.submittedSearchQuery], }); // Operations for a specific secret (shown in popup) @@ -339,8 +339,26 @@ const ListSecretsUI = ({ // Handle list view navigation if (key.upArrow && selectedIndex > 0) { setSelectedIndex(selectedIndex - 1); + } else if ( + key.upArrow && + selectedIndex === 0 && + !loading && + !navigating && + hasPrev + ) { + prevPage(); + setSelectedIndex(pageSecrets - 1); } else if (key.downArrow && selectedIndex < pageSecrets - 1) { setSelectedIndex(selectedIndex + 1); + } else if ( + key.downArrow && + selectedIndex === pageSecrets - 1 && + !loading && + !navigating && + hasMore + ) { + nextPage(); + setSelectedIndex(0); } else if ( (input === "n" || key.rightArrow) && !loading && diff --git a/src/commands/snapshot/list.tsx b/src/commands/snapshot/list.tsx index 1a938128..3e2a7e98 100644 --- a/src/commands/snapshot/list.tsx +++ b/src/commands/snapshot/list.tsx @@ -175,7 +175,7 @@ const ListSnapshotsUI = ({ !showCreateDevbox && !showDeleteConfirm && !search.searchMode, - deps: [devboxId, PAGE_SIZE, search.submittedSearchQuery], + deps: [devboxId, search.submittedSearchQuery], }); // Operations for snapshots @@ -379,8 +379,26 @@ const ListSnapshotsUI = ({ // Handle list view navigation if (key.upArrow && selectedIndex > 0) { setSelectedIndex(selectedIndex - 1); + } else if ( + key.upArrow && + selectedIndex === 0 && + !loading && + !navigating && + hasPrev + ) { + prevPage(); + setSelectedIndex(pageSnapshots - 1); } else if (key.downArrow && selectedIndex < pageSnapshots - 1) { setSelectedIndex(selectedIndex + 1); + } else if ( + key.downArrow && + selectedIndex === pageSnapshots - 1 && + !loading && + !navigating && + hasMore + ) { + nextPage(); + setSelectedIndex(0); } else if ( (input === "n" || key.rightArrow) && !loading && diff --git a/src/hooks/useCursorPagination.ts b/src/hooks/useCursorPagination.ts index 49e7b21b..5aae8c39 100644 --- a/src/hooks/useCursorPagination.ts +++ b/src/hooks/useCursorPagination.ts @@ -101,6 +101,7 @@ export function useCursorPagination( const [currentPage, setCurrentPage] = React.useState(0); const [hasMore, setHasMore] = React.useState(false); const [totalCount, setTotalCount] = React.useState(0); + const maxTotalCountRef = React.useRef(0); // Cursor history: cursorHistory[N] = last item ID of page N // Used to determine startingAt for page N+1 @@ -191,9 +192,11 @@ export function useCursorPagination( // Update pagination state setHasMore(result.hasMore); // Compute cumulative total: items seen on all previous pages + current page. - // This monotonically increases as the user pages forward and is exact when - // hasMore is false (i.e. we've reached the last page). - setTotalCount(page * pageSizeRef.current + result.items.length); + // Use a high-water mark so the count never decreases when navigating back. + const computed = page * pageSizeRef.current + result.items.length; + const newTotal = Math.max(computed, maxTotalCountRef.current); + maxTotalCountRef.current = newTotal; + setTotalCount(newTotal); } catch (err) { if (!isMountedRef.current) return; setError(err as Error); @@ -213,6 +216,7 @@ export function useCursorPagination( React.useEffect(() => { // Clear cursor history when deps change cursorHistoryRef.current = []; + maxTotalCountRef.current = 0; setCurrentPage(0); setItems([]); setHasMore(false); From 26931fa60c01cc02c96f5027bcccc06257eb1e4c Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Wed, 18 Feb 2026 17:00:10 -0800 Subject: [PATCH 6/6] parse limit option better --- src/commands/blueprint/list.tsx | 4 +-- src/commands/devbox/list.tsx | 4 +-- src/commands/gateway-config/list.tsx | 4 +-- src/commands/network-policy/list.tsx | 4 +-- src/commands/object/list.tsx | 4 +-- src/commands/secret/list.tsx | 41 +++++++++++++++++++++------- src/commands/snapshot/list.tsx | 4 +-- src/utils/output.ts | 18 ++++++++++++ 8 files changed, 61 insertions(+), 22 deletions(-) diff --git a/src/commands/blueprint/list.tsx b/src/commands/blueprint/list.tsx index ea65fe16..00222c5a 100644 --- a/src/commands/blueprint/list.tsx +++ b/src/commands/blueprint/list.tsx @@ -15,7 +15,7 @@ import { Operation } from "../../components/OperationsMenu.js"; import { ActionsPopup } from "../../components/ActionsPopup.js"; import { formatTimeAgo } from "../../components/ResourceListView.js"; import { SearchBar } from "../../components/SearchBar.js"; -import { output, outputError } from "../../utils/output.js"; +import { output, outputError, parseLimit } from "../../utils/output.js"; import { getBlueprintUrl } from "../../utils/url.js"; import { colors } from "../../utils/theme.js"; import { getStatusDisplay } from "../../components/StatusBadge.js"; @@ -1003,7 +1003,7 @@ export async function listBlueprints(options: ListBlueprintsOptions = {}) { try { const client = getClient(); - const maxResults = options.limit ? parseInt(options.limit, 10) : Infinity; + const maxResults = parseLimit(options.limit); const allBlueprints: unknown[] = []; let startingAfter: string | undefined; diff --git a/src/commands/devbox/list.tsx b/src/commands/devbox/list.tsx index 5e0b6c21..e09c4091 100644 --- a/src/commands/devbox/list.tsx +++ b/src/commands/devbox/list.tsx @@ -12,7 +12,7 @@ import type { Column } from "../../components/Table.js"; import { Table, createTextColumn } from "../../components/Table.js"; import { formatTimeAgo } from "../../components/ResourceListView.js"; import { SearchBar } from "../../components/SearchBar.js"; -import { output, outputError } from "../../utils/output.js"; +import { output, outputError, parseLimit } from "../../utils/output.js"; import { DevboxDetailPage } from "../../components/DevboxDetailPage.js"; import { DevboxCreatePage } from "../../components/DevboxCreatePage.js"; import { ResourceActionsMenu } from "../../components/ResourceActionsMenu.js"; @@ -806,7 +806,7 @@ export async function listDevboxes(options: ListOptions) { try { const client = getClient(); - const maxResults = options.limit ? parseInt(options.limit, 10) : Infinity; + const maxResults = parseLimit(options.limit); const allDevboxes: unknown[] = []; let startingAfter: string | undefined; diff --git a/src/commands/gateway-config/list.tsx b/src/commands/gateway-config/list.tsx index 193edfcc..91b0a4bc 100644 --- a/src/commands/gateway-config/list.tsx +++ b/src/commands/gateway-config/list.tsx @@ -14,7 +14,7 @@ import { ActionsPopup } from "../../components/ActionsPopup.js"; import { Operation } from "../../components/OperationsMenu.js"; import { formatTimeAgo } from "../../components/ResourceListView.js"; import { SearchBar } from "../../components/SearchBar.js"; -import { output, outputError } from "../../utils/output.js"; +import { output, outputError, parseLimit } from "../../utils/output.js"; import { colors } from "../../utils/theme.js"; import { useViewportHeight } from "../../hooks/useViewportHeight.js"; import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js"; @@ -765,7 +765,7 @@ export async function listGatewayConfigs(options: ListOptions = {}) { try { const client = getClient(); - const maxResults = options.limit ? parseInt(options.limit, 10) : Infinity; + const maxResults = parseLimit(options.limit); const allConfigs: unknown[] = []; let startingAfter: string | undefined; diff --git a/src/commands/network-policy/list.tsx b/src/commands/network-policy/list.tsx index f69d447f..e6863e40 100644 --- a/src/commands/network-policy/list.tsx +++ b/src/commands/network-policy/list.tsx @@ -14,7 +14,7 @@ import { ActionsPopup } from "../../components/ActionsPopup.js"; import { Operation } from "../../components/OperationsMenu.js"; import { formatTimeAgo } from "../../components/ResourceListView.js"; import { SearchBar } from "../../components/SearchBar.js"; -import { output, outputError } from "../../utils/output.js"; +import { output, outputError, parseLimit } from "../../utils/output.js"; import { colors } from "../../utils/theme.js"; import { useViewportHeight } from "../../hooks/useViewportHeight.js"; import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js"; @@ -794,7 +794,7 @@ export async function listNetworkPolicies(options: ListOptions = {}) { try { const client = getClient(); - const maxResults = options.limit ? parseInt(options.limit, 10) : Infinity; + const maxResults = parseLimit(options.limit); const allPolicies: unknown[] = []; let startingAfter: string | undefined; diff --git a/src/commands/object/list.tsx b/src/commands/object/list.tsx index 7449dab0..d7579290 100644 --- a/src/commands/object/list.tsx +++ b/src/commands/object/list.tsx @@ -15,7 +15,7 @@ import { ActionsPopup } from "../../components/ActionsPopup.js"; import { Operation } from "../../components/OperationsMenu.js"; import { formatTimeAgo } from "../../components/ResourceListView.js"; import { SearchBar } from "../../components/SearchBar.js"; -import { output, outputError } from "../../utils/output.js"; +import { output, outputError, parseLimit } from "../../utils/output.js"; import { colors } from "../../utils/theme.js"; import { useViewportHeight } from "../../hooks/useViewportHeight.js"; import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js"; @@ -836,7 +836,7 @@ export async function listObjects(options: ListOptions) { try { const client = getClient(); - const maxResults = options.limit ? parseInt(options.limit, 10) : Infinity; + const maxResults = parseLimit(options.limit); const allObjects: unknown[] = []; let startingAfter: string | undefined; diff --git a/src/commands/secret/list.tsx b/src/commands/secret/list.tsx index 43cae06e..2f721c66 100644 --- a/src/commands/secret/list.tsx +++ b/src/commands/secret/list.tsx @@ -13,7 +13,7 @@ import { ActionsPopup } from "../../components/ActionsPopup.js"; import { Operation } from "../../components/OperationsMenu.js"; import { formatTimeAgo } from "../../components/ResourceListView.js"; import { SearchBar } from "../../components/SearchBar.js"; -import { output, outputError } from "../../utils/output.js"; +import { output, outputError, parseLimit } from "../../utils/output.js"; import { colors } from "../../utils/theme.js"; import { useViewportHeight } from "../../hooks/useViewportHeight.js"; import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js"; @@ -639,18 +639,39 @@ export async function listSecrets(options: ListOptions = {}) { try { const client = getClient(); - const limit = options.limit - ? parseInt(options.limit, 10) - : DEFAULT_PAGE_SIZE; + const maxResults = parseLimit(options.limit); + const allSecrets: unknown[] = []; + let startingAfter: string | undefined; - // Fetch secrets - const result = await client.secrets.list({ limit }); + do { + const remaining = maxResults - allSecrets.length; + const queryParams: Record = { + limit: Math.min(DEFAULT_PAGE_SIZE, remaining), + }; + if (startingAfter) { + queryParams.starting_after = startingAfter; + } - // Extract secrets array - const secrets = result.secrets || []; + const result = await client.secrets.list(queryParams); + const pageResult = result as unknown as { + secrets?: { id: string }[]; + has_more?: boolean; + }; + const pageSecrets = pageResult.secrets || []; + allSecrets.push(...pageSecrets); + + if ( + pageResult.has_more && + pageSecrets.length > 0 && + allSecrets.length < maxResults + ) { + startingAfter = pageSecrets[pageSecrets.length - 1].id; + } else { + startingAfter = undefined; + } + } while (startingAfter !== undefined); - // Default: output JSON for lists - output(secrets, { format: options.output, defaultFormat: "json" }); + output(allSecrets, { format: options.output, defaultFormat: "json" }); } catch (error) { outputError("Failed to list secrets", error); } diff --git a/src/commands/snapshot/list.tsx b/src/commands/snapshot/list.tsx index 3e2a7e98..bf357a11 100644 --- a/src/commands/snapshot/list.tsx +++ b/src/commands/snapshot/list.tsx @@ -15,7 +15,7 @@ import { ActionsPopup } from "../../components/ActionsPopup.js"; import { Operation } from "../../components/OperationsMenu.js"; import { formatTimeAgo } from "../../components/ResourceListView.js"; import { SearchBar } from "../../components/SearchBar.js"; -import { output, outputError } from "../../utils/output.js"; +import { output, outputError, parseLimit } from "../../utils/output.js"; import { colors } from "../../utils/theme.js"; import { useViewportHeight } from "../../hooks/useViewportHeight.js"; import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js"; @@ -707,7 +707,7 @@ export async function listSnapshots(options: ListOptions) { try { const client = getClient(); - const maxResults = options.limit ? parseInt(options.limit, 10) : Infinity; + const maxResults = parseLimit(options.limit); const allSnapshots: ReturnType[] = []; let startingAfter: string | undefined; diff --git a/src/utils/output.ts b/src/utils/output.ts index 8b973aa9..95026014 100644 --- a/src/utils/output.ts +++ b/src/utils/output.ts @@ -11,6 +11,24 @@ import { processUtils } from "./processUtils.js"; export type OutputFormat = "text" | "json" | "yaml"; +/** + * Parse a --limit option string into a max results number. + * - undefined or "0" → Infinity (unlimited) + * - positive integer string → that number + * - negative integer → Infinity (treat as unlimited) + * - non-numeric non-empty string → throws an error + */ +export function parseLimit(limit: string | undefined): number { + if (!limit || limit.trim() === "") return Infinity; + const trimmed = limit.trim(); + if (!/^-?\d+$/.test(trimmed)) { + throw new Error(`Invalid --limit value: "${limit}". Must be an integer.`); + } + const n = parseInt(trimmed, 10); + if (n <= 0) return Infinity; + return n; +} + export interface OutputOptions { output?: string; }