Skip to content

Commit c991e1c

Browse files
jason-rlclaude
andcommitted
feat: add public/custom tab toggle for blueprints list
Add Tab toggle between public and custom blueprints in TUI, matching the benchmark list screen implementation from PR #230. - Add publicOnly and includeTotalCount options to listBlueprints - Reuse listBlueprints with publicOnly flag (no code duplication) - Add showPublic state and Tab key handler - Add tab bar UI between breadcrumb and search - Update navigation tips with Tab key hint - Fix building_complete -> build_complete typos Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2eef88c commit c991e1c

2 files changed

Lines changed: 73 additions & 61 deletions

File tree

src/commands/blueprint/list.tsx

Lines changed: 60 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
} from "../../hooks/useInputHandler.js";
3232
import { useNavigation } from "../../store/navigationStore.js";
3333
import { ConfirmationPrompt } from "../../components/ConfirmationPrompt.js";
34+
import { listBlueprints as listBlueprintsService } from "../../services/blueprintService.js";
3435

3536
const DEFAULT_PAGE_SIZE = 10;
3637

@@ -42,10 +43,17 @@ type OperationType =
4243
| null;
4344

4445
// Local interface for blueprint data used in this component
46+
type BlueprintStatus =
47+
| "queued"
48+
| "provisioning"
49+
| "building"
50+
| "failed"
51+
| "build_complete";
52+
4553
interface BlueprintListItem {
4654
id: string;
4755
name?: string;
48-
status?: string;
56+
status?: BlueprintStatus;
4957
create_time_ms?: number;
5058
[key: string]: unknown;
5159
}
@@ -78,6 +86,7 @@ const ListBlueprintsUI = ({
7886
const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
7987
const [selectedIndex, setSelectedIndex] = React.useState(0);
8088
const [showPopup, setShowPopup] = React.useState(false);
89+
const [showPublic, setShowPublic] = React.useState(false);
8190
const { navigate } = useNavigation();
8291

8392
// Search state
@@ -86,8 +95,8 @@ const ListBlueprintsUI = ({
8695
onSearchClear: () => setSelectedIndex(0),
8796
});
8897

89-
// Calculate overhead for viewport height
90-
const overhead = 13 + search.getSearchOverhead();
98+
// Calculate overhead for viewport height (14 = breadcrumb + tab bar + search + table header + stats + nav tips + borders)
99+
const overhead = 14 + search.getSearchOverhead();
91100
const { viewportHeight, terminalWidth } = useViewportHeight({
92101
overhead,
93102
minHeight: 5,
@@ -118,50 +127,26 @@ const ListBlueprintsUI = ({
118127
startingAt?: string;
119128
includeTotalCount?: boolean;
120129
}) => {
121-
const client = getClient();
122-
const pageBlueprints: BlueprintListItem[] = [];
123-
124-
// Build query params
125-
const queryParams: Record<string, unknown> = {
130+
const result = await listBlueprintsService({
126131
limit: params.limit,
127-
// Only request total_count on first page (expensive for backend)
128-
include_total_count: params.includeTotalCount === true,
129-
};
130-
if (params.startingAt) {
131-
queryParams.starting_after = params.startingAt;
132-
}
133-
if (search.submittedSearchQuery) {
134-
queryParams.search = search.submittedSearchQuery;
135-
}
136-
137-
// Fetch ONE page only
138-
const page = (await client.blueprints.list(
139-
queryParams,
140-
)) as unknown as BlueprintsCursorIDPage<BlueprintListItem> & {
141-
total_count?: number;
142-
};
143-
144-
// Extract data and create defensive copies
145-
if (page.blueprints && Array.isArray(page.blueprints)) {
146-
page.blueprints.forEach((b: BlueprintListItem) => {
147-
pageBlueprints.push({
148-
id: b.id,
149-
name: b.name,
150-
status: b.status,
151-
create_time_ms: b.create_time_ms,
152-
});
153-
});
154-
}
132+
startingAfter: params.startingAt,
133+
search: search.submittedSearchQuery || undefined,
134+
publicOnly: showPublic,
135+
includeTotalCount: params.includeTotalCount,
136+
});
155137

156-
const result = {
157-
items: pageBlueprints,
158-
hasMore: page.has_more || false,
159-
totalCount: page.total_count,
138+
return {
139+
items: result.blueprints.map((b) => ({
140+
id: b.id,
141+
name: b.name,
142+
status: b.status,
143+
create_time_ms: b.create_time_ms,
144+
})),
145+
hasMore: result.hasMore,
146+
totalCount: result.totalCount,
160147
};
161-
162-
return result;
163148
},
164-
[search.submittedSearchQuery],
149+
[showPublic, search.submittedSearchQuery],
165150
);
166151

167152
// Use the shared pagination hook
@@ -187,7 +172,7 @@ const ListBlueprintsUI = ({
187172
!executingOperation &&
188173
!showDeleteConfirm &&
189174
!search.searchMode,
190-
deps: [search.submittedSearchQuery],
175+
deps: [search.submittedSearchQuery, showPublic],
191176
});
192177

193178
// Memoize columns array
@@ -325,11 +310,7 @@ const ListBlueprintsUI = ({
325310
icon: figures.info,
326311
});
327312

328-
if (
329-
blueprint &&
330-
(blueprint.status === "build_complete" ||
331-
blueprint.status === "building_complete")
332-
) {
313+
if (blueprint && blueprint.status === "build_complete") {
333314
operations.push({
334315
key: "create_devbox",
335316
label: "Create Devbox from Blueprint",
@@ -602,8 +583,7 @@ const ListBlueprintsUI = ({
602583
c: () => {
603584
if (
604585
selectedBlueprintItem &&
605-
(selectedBlueprintItem.status === "build_complete" ||
606-
selectedBlueprintItem.status === "building_complete")
586+
selectedBlueprintItem.status === "build_complete"
607587
) {
608588
setShowPopup(false);
609589
setSelectedBlueprint(selectedBlueprintItem);
@@ -657,6 +637,10 @@ const ListBlueprintsUI = ({
657637
right: goToNextPage,
658638
p: goToPrevPage,
659639
left: goToPrevPage,
640+
tab: () => {
641+
setShowPublic((prev) => !prev);
642+
setSelectedIndex(0);
643+
},
660644
enter: () => {
661645
if (selectedBlueprintItem) {
662646
navigate("blueprint-detail", {
@@ -882,6 +866,27 @@ const ListBlueprintsUI = ({
882866
<>
883867
<Breadcrumb items={[{ label: "Blueprints", active: true }]} />
884868

869+
{/* Tab bar */}
870+
<Box paddingX={2} marginBottom={0}>
871+
<Text
872+
color={!showPublic ? colors.primary : colors.textDim}
873+
bold={!showPublic}
874+
>
875+
{!showPublic ? figures.pointer : " "} Custom
876+
</Text>
877+
<Text color={colors.textDim}></Text>
878+
<Text
879+
color={showPublic ? colors.primary : colors.textDim}
880+
bold={showPublic}
881+
>
882+
{showPublic ? figures.pointer : " "} Public
883+
</Text>
884+
<Text color={colors.textDim} dimColor>
885+
{" "}
886+
(Tab to switch)
887+
</Text>
888+
</Box>
889+
885890
{/* Search bar */}
886891
<SearchBar
887892
searchMode={search.searchMode}
@@ -899,11 +904,12 @@ const ListBlueprintsUI = ({
899904
data={blueprints}
900905
keyExtractor={(blueprint: BlueprintListItem) => blueprint.id}
901906
selectedIndex={selectedIndex}
902-
title={`blueprints[${totalCount}]`}
907+
title={`blueprints[${totalCount}] ${showPublic ? "(public)" : "(custom)"}`}
903908
columns={blueprintColumns}
904909
emptyState={
905910
<Text color={colors.textDim}>
906-
{figures.info} No blueprints found. Try: rli blueprint create
911+
{figures.info} No {showPublic ? "public " : ""}blueprints found
912+
{!showPublic ? ". Try: rli blueprint create" : ""}
907913
</Text>
908914
}
909915
/>
@@ -1003,6 +1009,7 @@ const ListBlueprintsUI = ({
10031009
},
10041010
{ key: "Enter", label: "Details" },
10051011
{ key: "a", label: "Actions" },
1012+
{ key: "Tab", label: "Switch tab" },
10061013
{ key: "o", label: "Browser" },
10071014
{ key: "/", label: "Search" },
10081015
{ key: "Esc", label: "Back" },

src/services/blueprintService.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export interface ListBlueprintsOptions {
1313
limit: number;
1414
startingAfter?: string;
1515
search?: string;
16+
publicOnly?: boolean;
17+
includeTotalCount?: boolean;
1618
}
1719

1820
export interface ListBlueprintsResult {
@@ -29,8 +31,9 @@ export async function listBlueprints(
2931
): Promise<ListBlueprintsResult> {
3032
const client = getClient();
3133

32-
const queryParams: BlueprintListParams = {
34+
const queryParams: BlueprintListParams & { include_total_count?: boolean } = {
3335
limit: options.limit,
36+
include_total_count: options.includeTotalCount === true,
3437
};
3538

3639
if (options.startingAfter) {
@@ -40,9 +43,14 @@ export async function listBlueprints(
4043
queryParams.name = options.search;
4144
}
4245

43-
const pagePromise = client.blueprints.list(queryParams);
46+
// Use listPublic or list based on publicOnly option
47+
const pagePromise = options.publicOnly
48+
? client.blueprints.listPublic(queryParams)
49+
: client.blueprints.list(queryParams);
4450
const page =
45-
(await pagePromise) as unknown as BlueprintsCursorIDPage<BlueprintView>;
51+
(await pagePromise) as unknown as BlueprintsCursorIDPage<BlueprintView> & {
52+
total_count?: number;
53+
};
4654

4755
const blueprints: Blueprint[] = [];
4856

@@ -51,7 +59,6 @@ export async function listBlueprints(
5159
// CRITICAL: Truncate all strings to prevent Yoga crashes
5260
const MAX_ID_LENGTH = 100;
5361
const MAX_NAME_LENGTH = 200;
54-
const MAX_STATUS_LENGTH = 50;
5562
const MAX_ARCH_LENGTH = 50;
5663
const MAX_RESOURCES_LENGTH = 100;
5764

@@ -77,13 +84,11 @@ export async function listBlueprints(
7784
});
7885
}
7986

80-
const result = {
87+
return {
8188
blueprints,
82-
totalCount: blueprints.length,
89+
totalCount: page.total_count ?? blueprints.length,
8390
hasMore: page.has_more || false,
8491
};
85-
86-
return result;
8792
}
8893

8994
/**

0 commit comments

Comments
 (0)