Skip to content

Commit ea17c8f

Browse files
committed
cp dines
1 parent e20200b commit ea17c8f

4 files changed

Lines changed: 145 additions & 96 deletions

File tree

src/components/DevboxCreatePage.tsx

Lines changed: 125 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,31 +1102,39 @@ export const DevboxCreatePage = ({
11021102
return `Custom (${egress.allowed_hostnames?.length || 0})`;
11031103
};
11041104

1105-
const networkPolicyColumns: Column<NetworkPolicy>[] = [
1106-
createTextColumn<NetworkPolicy>("id", "ID", (policy) => policy.id, {
1107-
width: 25,
1108-
color: colors.idColor,
1109-
}),
1110-
createTextColumn<NetworkPolicy>(
1111-
"name",
1112-
"Name",
1113-
(policy) => policy.name || "",
1114-
{ width: 25 },
1115-
),
1116-
createTextColumn<NetworkPolicy>(
1117-
"egress",
1118-
"Egress",
1119-
(policy) => getEgressLabel(policy.egress),
1120-
{ width: 15 },
1121-
),
1122-
createTextColumn<NetworkPolicy>(
1123-
"created",
1124-
"Created",
1125-
(policy) =>
1126-
policy.create_time_ms ? formatTimeAgo(policy.create_time_ms) : "",
1127-
{ width: 18, color: colors.textDim },
1128-
),
1129-
];
1105+
const buildNetworkPolicyColumns = (tw: number): Column<NetworkPolicy>[] => {
1106+
const fixedWidth = 6;
1107+
const idWidth = 25;
1108+
const egressWidth = 15;
1109+
const timeWidth = 18;
1110+
const baseWidth = fixedWidth + idWidth + egressWidth + timeWidth;
1111+
const nameWidth = Math.min(80, Math.max(15, tw - baseWidth));
1112+
return [
1113+
createTextColumn<NetworkPolicy>("id", "ID", (policy) => policy.id, {
1114+
width: idWidth + 1,
1115+
color: colors.idColor,
1116+
}),
1117+
createTextColumn<NetworkPolicy>(
1118+
"name",
1119+
"Name",
1120+
(policy) => policy.name || "",
1121+
{ width: nameWidth },
1122+
),
1123+
createTextColumn<NetworkPolicy>(
1124+
"egress",
1125+
"Egress",
1126+
(policy) => getEgressLabel(policy.egress),
1127+
{ width: egressWidth },
1128+
),
1129+
createTextColumn<NetworkPolicy>(
1130+
"created",
1131+
"Created",
1132+
(policy) =>
1133+
policy.create_time_ms ? formatTimeAgo(policy.create_time_ms) : "",
1134+
{ width: timeWidth, color: colors.textDim },
1135+
),
1136+
];
1137+
};
11301138

11311139
return (
11321140
<ResourcePicker<NetworkPolicy>
@@ -1146,7 +1154,7 @@ export const DevboxCreatePage = ({
11461154
},
11471155
getItemId: (policy) => policy.id,
11481156
getItemLabel: (policy) => policy.name || policy.id,
1149-
columns: networkPolicyColumns,
1157+
columns: buildNetworkPolicyColumns,
11501158
mode: "single",
11511159
emptyMessage: "No network policies found",
11521160
searchPlaceholder: "Search network policies...",
@@ -1167,31 +1175,45 @@ export const DevboxCreatePage = ({
11671175

11681176
// Gateway config picker screen
11691177
if (showGatewayPicker) {
1170-
const gatewayColumns: Column<GatewayConfig>[] = [
1171-
createTextColumn<GatewayConfig>("id", "ID", (config) => config.id, {
1172-
width: 25,
1173-
color: colors.idColor,
1174-
}),
1175-
createTextColumn<GatewayConfig>(
1176-
"name",
1177-
"Name",
1178-
(config) => config.name || "",
1179-
{ width: 25 },
1180-
),
1181-
createTextColumn<GatewayConfig>(
1182-
"endpoint",
1183-
"Endpoint",
1184-
(config) => config.endpoint || "",
1185-
{ width: 30, color: colors.textDim },
1186-
),
1187-
createTextColumn<GatewayConfig>(
1188-
"created",
1189-
"Created",
1190-
(config) =>
1191-
config.create_time_ms ? formatTimeAgo(config.create_time_ms) : "",
1192-
{ width: 18, color: colors.textDim },
1193-
),
1194-
];
1178+
const buildGatewayColumns = (tw: number): Column<GatewayConfig>[] => {
1179+
const fixedWidth = 6;
1180+
const idWidth = 25;
1181+
const timeWidth = 20;
1182+
const showEndpoint = tw >= 100;
1183+
const endpointWidth = Math.max(20, tw >= 140 ? 40 : 25);
1184+
const baseWidth = fixedWidth + idWidth + timeWidth;
1185+
const optionalWidth = showEndpoint ? endpointWidth : 0;
1186+
const nameWidth = Math.min(80, Math.max(15, tw - baseWidth - optionalWidth));
1187+
return [
1188+
createTextColumn<GatewayConfig>("id", "ID", (config) => config.id, {
1189+
width: idWidth + 1,
1190+
color: colors.idColor,
1191+
}),
1192+
createTextColumn<GatewayConfig>(
1193+
"name",
1194+
"Name",
1195+
(config) => config.name || "",
1196+
{ width: nameWidth },
1197+
),
1198+
...(showEndpoint
1199+
? [
1200+
createTextColumn<GatewayConfig>(
1201+
"endpoint",
1202+
"Endpoint",
1203+
(config) => config.endpoint || "",
1204+
{ width: endpointWidth, color: colors.textDim },
1205+
),
1206+
]
1207+
: []),
1208+
createTextColumn<GatewayConfig>(
1209+
"created",
1210+
"Created",
1211+
(config) =>
1212+
config.create_time_ms ? formatTimeAgo(config.create_time_ms) : "",
1213+
{ width: timeWidth, color: colors.textDim },
1214+
),
1215+
];
1216+
};
11951217

11961218
return (
11971219
<ResourcePicker<GatewayConfig>
@@ -1212,7 +1234,7 @@ export const DevboxCreatePage = ({
12121234
},
12131235
getItemId: (config) => config.id,
12141236
getItemLabel: (config) => config.name || config.id,
1215-
columns: gatewayColumns,
1237+
columns: buildGatewayColumns,
12161238
mode: "single",
12171239
emptyMessage: "No gateway configs found",
12181240
searchPlaceholder: "Search gateway configs...",
@@ -1259,25 +1281,32 @@ export const DevboxCreatePage = ({
12591281

12601282
// Secret picker screen (for gateway)
12611283
if (showSecretPicker) {
1262-
const secretColumns: Column<SecretListItem>[] = [
1263-
createTextColumn<SecretListItem>("id", "ID", (secret) => secret.id, {
1264-
width: 25,
1265-
color: colors.idColor,
1266-
}),
1267-
createTextColumn<SecretListItem>(
1268-
"name",
1269-
"Name",
1270-
(secret) => secret.name || "",
1271-
{ width: 30 },
1272-
),
1273-
createTextColumn<SecretListItem>(
1274-
"created",
1275-
"Created",
1276-
(secret) =>
1277-
secret.create_time_ms ? formatTimeAgo(secret.create_time_ms) : "",
1278-
{ width: 18, color: colors.textDim },
1279-
),
1280-
];
1284+
const buildSecretColumns = (tw: number): Column<SecretListItem>[] => {
1285+
const fixedWidth = 6;
1286+
const idWidth = 30;
1287+
const timeWidth = 20;
1288+
const baseWidth = fixedWidth + idWidth + timeWidth;
1289+
const nameWidth = Math.min(80, Math.max(15, tw - baseWidth));
1290+
return [
1291+
createTextColumn<SecretListItem>("id", "ID", (secret) => secret.id, {
1292+
width: idWidth + 1,
1293+
color: colors.idColor,
1294+
}),
1295+
createTextColumn<SecretListItem>(
1296+
"name",
1297+
"Name",
1298+
(secret) => secret.name || "",
1299+
{ width: nameWidth },
1300+
),
1301+
createTextColumn<SecretListItem>(
1302+
"created",
1303+
"Created",
1304+
(secret) =>
1305+
secret.create_time_ms ? formatTimeAgo(secret.create_time_ms) : "",
1306+
{ width: timeWidth, color: colors.textDim },
1307+
),
1308+
];
1309+
};
12811310

12821311
return (
12831312
<ResourcePicker<SecretListItem>
@@ -1286,25 +1315,38 @@ export const DevboxCreatePage = ({
12861315
title: "Select Secret for Gateway",
12871316
fetchPage: async (params) => {
12881317
const client = getClient();
1289-
// Secrets API doesn't support cursor pagination, just limit
1318+
// Secrets API doesn't support cursor pagination, so we fetch all
1319+
// and do client-side pagination by slicing to the requested page
12901320
const page = await client.secrets.list({
1291-
limit: params.limit,
1321+
limit: 1000,
12921322
});
1323+
const allSecrets = (page.secrets || []).map(
1324+
(s: { id: string; name: string; create_time_ms?: number }) => ({
1325+
id: s.id,
1326+
name: s.name,
1327+
create_time_ms: s.create_time_ms,
1328+
}),
1329+
);
1330+
// Client-side cursor pagination
1331+
let startIdx = 0;
1332+
if (params.startingAt) {
1333+
const cursorIdx = allSecrets.findIndex(
1334+
(s) => s.id === params.startingAt,
1335+
);
1336+
if (cursorIdx >= 0) {
1337+
startIdx = cursorIdx + 1;
1338+
}
1339+
}
1340+
const sliced = allSecrets.slice(startIdx, startIdx + params.limit);
12931341
return {
1294-
items: (page.secrets || []).map(
1295-
(s: { id: string; name: string; create_time_ms?: number }) => ({
1296-
id: s.id,
1297-
name: s.name,
1298-
create_time_ms: s.create_time_ms,
1299-
}),
1300-
),
1301-
hasMore: false, // Secrets API doesn't support pagination
1302-
totalCount: page.total_count || 0,
1342+
items: sliced,
1343+
hasMore: startIdx + params.limit < allSecrets.length,
1344+
totalCount: allSecrets.length,
13031345
};
13041346
},
13051347
getItemId: (secret) => secret.id,
13061348
getItemLabel: (secret) => secret.name || secret.id,
1307-
columns: secretColumns,
1349+
columns: buildSecretColumns,
13081350
mode: "single",
13091351
emptyMessage: "No secrets found",
13101352
searchPlaceholder: "Search secrets...",

src/components/ResourcePicker.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export interface ResourcePickerConfig<T> {
5555
getItemStatus?: (item: T) => string | undefined;
5656

5757
/** Column definitions for table display (if not provided, uses simple label/status) */
58-
columns?: Column<T>[];
58+
columns?: Column<T>[] | ((terminalWidth: number) => Column<T>[]);
5959

6060
/** Selection mode */
6161
mode: "single" | "multi";
@@ -116,7 +116,8 @@ export function ResourcePicker<T>({
116116
onSearchClear: () => setSelectedIndex(0),
117117
});
118118

119-
// Calculate overhead for viewport height (matches list pages)
119+
// Calculate overhead for viewport height
120+
// Matches list pages: breadcrumb(4) + table chrome(4) + stats(2) + nav tips(2) + buffer(1) = 13
120121
const overhead = 13 + search.getSearchOverhead();
121122
const { viewportHeight, terminalWidth } = useViewportHeight({
122123
overhead,
@@ -125,8 +126,14 @@ export function ResourcePicker<T>({
125126

126127
const PAGE_SIZE = viewportHeight;
127128

128-
// terminalWidth available from viewport hook for column calculations
129-
void terminalWidth;
129+
// Resolve columns - support both static array and function that receives terminalWidth
130+
const resolvedColumns = React.useMemo(() => {
131+
if (!config.columns) return undefined;
132+
if (typeof config.columns === "function") {
133+
return config.columns(terminalWidth);
134+
}
135+
return config.columns;
136+
}, [config.columns, terminalWidth]);
130137

131138
// Store fetchPage in a ref to avoid dependency issues
132139
const fetchPageRef = React.useRef(config.fetchPage);
@@ -330,7 +337,7 @@ export function ResourcePicker<T>({
330337
/>
331338

332339
{/* Table view */}
333-
{config.columns ? (
340+
{resolvedColumns ? (
334341
<Table
335342
data={items}
336343
keyExtractor={config.getItemId}
@@ -357,9 +364,9 @@ export function ResourcePicker<T>({
357364
},
358365
{ width: 3 },
359366
),
360-
...config.columns,
367+
...resolvedColumns,
361368
]
362-
: config.columns
369+
: resolvedColumns
363370
}
364371
emptyState={
365372
<Text color={colors.textDim}>

src/mcp/server.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
ListToolsRequestSchema,
77
Tool,
88
} from "@modelcontextprotocol/sdk/types.js";
9-
import RunloopSDK from "@runloop/api-client";
9+
import Runloop from "@runloop/api-client";
1010
import { VERSION } from "@runloop/api-client/version.js";
1111
import Conf from "conf";
1212
import { processUtils } from "../utils/processUtils.js";
@@ -57,7 +57,7 @@ function getBaseUrl(): string {
5757
}
5858
}
5959

60-
function getClient(): RunloopSDK {
60+
function getClient(): Runloop {
6161
const config = getConfig();
6262

6363
if (!config.apiKey) {
@@ -68,7 +68,7 @@ function getClient(): RunloopSDK {
6868

6969
const baseURL = getBaseUrl();
7070

71-
return new RunloopSDK({
71+
return new Runloop({
7272
bearerToken: config.apiKey,
7373
baseURL,
7474
timeout: 10000, // 10 seconds instead of default 30 seconds

src/utils/client.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import RunloopSDK from "@runloop/api-client";
1+
import Runloop from "@runloop/api-client";
22
import { VERSION } from "@runloop/api-client/version.js";
33
import { getConfig } from "./config.js";
44

@@ -19,7 +19,7 @@ function getBaseUrl(): string {
1919
}
2020
}
2121

22-
export function getClient(): RunloopSDK {
22+
export function getClient(): Runloop {
2323
const config = getConfig();
2424

2525
if (!config.apiKey) {
@@ -30,7 +30,7 @@ export function getClient(): RunloopSDK {
3030

3131
const baseURL = getBaseUrl();
3232

33-
return new RunloopSDK({
33+
return new Runloop({
3434
bearerToken: config.apiKey,
3535
baseURL,
3636
defaultHeaders: {

0 commit comments

Comments
 (0)