Skip to content

Commit 7565d45

Browse files
jason-rlclaude
andauthored
feat: add TUI features and fix benchmark pagination total count (#230)
## Summary - **Object create form**: New TUI screen accessible via `c` shortcut from object list. Supports optional file path upload or displays pre-signed S3 URL with clipboard copy. For tar/tgz content types, a multi-item list manager allows adding multiple file paths; for other types, a single text input is shown. Switching content type syncs between the two inputs bidirectionally. - **Benchmark tab toggle**: Changed public/custom toggle from `t` key to `Tab` key with visual tab bar matching the agent list pattern. - **Blueprint tab toggle**: Added public/custom toggle with `Tab` key and visual tab bar matching the benchmark/agent list pattern. - **Benchmark pagination fix**: All benchmark list screens (Benchmark Definitions, Benchmark Runs, Scenario Runs, Benchmark Jobs) now correctly display total count from the API instead of current page item count. - **Full Details viewport fix**: Fixed off-by-one in `DetailedInfoView` overhead calculation that caused the top border to be clipped off screen when content filled the viewport exactly. - **CLI: `blueprint create --base`**: Duplicate a blueprint by passing `--base <id|name>` to `blueprint create`. The source blueprint's parameters are used as defaults; any other flag overrides. `--name` defaults to `{base}-copy` when `--base` is used. - **CLI: `axon events`**: New subcommand to list events for an axon. **Fixed origin field to display enum names (SYSTEM_EVENT, AGENT_EVENT, etc.) instead of numeric values.** - **CLI: `scenario list`**: New subcommand to list scenario runs with pagination and optional benchmark run ID filter. - **Metadata support**: `--metadata key=value` added to `object upload` and `benchmark-job run` CLI commands. TUI create screens for objects and benchmark jobs include a metadata key-value editor matching the devbox create pattern. - **Tar archive: duplicate path rejection**: `createTarBuffer` now rejects duplicate input paths (after `path.resolve`) instead of producing an invalid tar archive. - **Tar archive: symlink support**: Replaced `nanotar` with `tar-stream`. Symlinks are now stored as proper tar symlink entries (type `2`) with their target in `linkname`, instead of being rejected. This applies to both CLI and TUI upload flows. - **TUI tar single-file fix**: When content type is tar/tgz and a single file (not directory) is specified, the TUI now uploads it as-is instead of re-archiving it. Matches the CLI behavior. - **TUI `[d]` keybind consistency**: Changed all `[d] Remove` hints in devbox create (gateway configs, MCP configs, agent mounts, object mounts) to `[d] Delete` to match the rest of the codebase. - **Benchmark job metadata navigation**: Fixed up/down arrow keys moving the main form cursor while editing metadata key-value pairs. Added `isActive: !inMetadataSection` guard to the main form input handler. - **Benchmark list `[c]` keybind fix**: Changed "Create Job" shortcut from `c` to `s` to avoid conflicting with the `[c]` = "Copy ID" convention used in detail screens. The popup already used `s` for this action. - **CLI output consistency**: Standardized all CLI list/get/info commands to default to JSON output and removed chalk-colored text tables from `axon events`, `axon list`, `scenario list`, `scenario info`, `benchmark-job list`, `benchmark-job summary`, and `agent show`. Text mode now uses the shared `output()` utility's uncolored key-value format. **Note**: TUI axon events and SQL workbench screens have been moved to PR #233. ## Test plan ### Automated tests (155+ new tests) | Test file | Tests | Coverage | |-----------|-------|---------| | `services/benchmarkJobService.test.ts` | 27 | `buildCloneParams` (all source/secret variants), `createBenchmarkJob` (validation, config mapping), `listBenchmarkJobs`, `getBenchmarkJob`, `getBenchmarkRun`, `listBenchmarkRunScenarioRuns` | | `services/axonService.test.ts` | 15 | `listActiveAxons` (smart search, pagination), `getAxon`, `listAxonEvents` (hasMore detection, row mapping, origin enum mapping), `executeAxonSql` | | `services/benchmarkService.test.ts` | 16 | `listScenarioRuns` (both code paths), `listPublicBenchmarks`, `createBenchmarkRun`, `listBenchmarkRuns`, `getBenchmarkRun`, `getScenarioRun`, `listBenchmarks`, `getBenchmark` | | `services/objectServiceApi.test.ts` | 6 | `createObject`, `completeObject`, `uploadToPresignedUrl` | | `services/objectService.test.ts` | +5 | `buildObjectDetailFields` edge cases (hours format, missing size, public field) | | `commands/object/upload.test.ts` | +3 | Symlink entries stored with correct type/linkname, symlinks inside directory trees, duplicate path rejection | | `commands/axon/events.test.ts` | 5 | Output format, limit defaults, error handling | | `commands/blueprint/create.test.ts` | 26 | Normal create (name+options, missing name error), --base (ID/name lookup, exact match, fallback, not found, default name, custom name, all source params copied, override per flag, preserves non-overridden params), output format, error handling | | `commands/scenario/list.test.ts` | 8 | Pagination, sorting, output formats, filter, error handling | | `components/allocateSectionLines.test.ts` | 10 | Zero/partial/full budget, single-field priority, null filtering, multi-section distribution | ### Manual tests - [x] TUI: Objects list → press `c` → verify create form renders with Name, Content Type, File Path fields - [x] TUI: Create object with non-tar content type → verify single text input for file path - [x] TUI: Create object → switch content type to tar/tgz → verify file path field becomes multi-item list manager - [x] TUI: Type a path in single input, switch to tar → verify path appears as 1st entry in list - [x] TUI: Add multiple paths in tar mode, switch to text → verify 1st entry populates single input; switch back to tar → verify entries >=2 are preserved - [x] TUI: Create object without file path → verify pre-signed URL shown, `c` copies to clipboard - [x] TUI: Create object with file path → verify upload + completion flow - [x] TUI: Create object with multiple paths and tar/tgz content type → verify archive upload - [x] TUI: Object create form → add metadata key-value pairs → verify passed to API - [x] TUI: Benchmark job create → add metadata key-value pairs → verify passed to API - [x] TUI: Benchmark Definitions → verify `Tab` key toggles between Public/Custom with visual tab bar - [x] TUI: Benchmark Definitions → verify old `t` key no longer toggles - [x] TUI: Benchmark Definitions list → verify total count matches API total, not page item count - [x] TUI: Benchmark Runs list → verify total count displays correctly - [x] TUI: Scenario Runs list → verify total count displays correctly - [x] TUI: Benchmark Jobs list → verify total count displays correctly - [x] TUI: Any detail screen → press `i` for Full Details → verify top border is visible (not clipped) - [x] TUI: Blueprint detail → verify "Duplicate Blueprint" operation is NOT shown - [x] TUI: Blueprint list → verify `Tab` key toggles between Public/Custom with visual tab bar - [x] TUI: Object create → select tar/tgz, specify a single .tar file → uploads as-is (not re-archived) - [x] TUI: Object create → select tar/tgz, specify a directory → creates an archive - [x] TUI: Devbox create → verify gateway/MCP/mount sections show `[d] Delete` consistently - [x] TUI: Benchmark job create → enter metadata section → verify up/down arrows only navigate metadata items - [x] TUI: Benchmark list → verify `s` creates a job, `c` no longer triggers create - [x] CLI: `rli blueprint create --base <id>` → verify creates copy with {base}-copy name - [x] CLI: `rli blueprint create --base <id> --name custom` → verify custom name - [x] CLI: `rli blueprint create --base <id> --resources LARGE` → verify override applied - [x] CLI: `rli blueprint create --base <id> --metadata env=prod` → verify metadata override - [x] CLI: `rli blueprint create --help` → verify --base flag listed, --name not marked required - [ ] CLI: `rli object upload <file> --metadata key=value` → verify metadata on created object - [ ] CLI: `rli benchmark-job run --agent ... --benchmark ... --metadata key=value` → verify metadata - [ ] CLI: `rli axon events <id>` → verify JSON output by default, origin shows enum names - [ ] CLI: `rli axon events <id> -o text` → verify key-value output - [ ] CLI: `rli scenario list` → verify JSON output by default - [ ] CLI: `rli scenario list -o text` → verify key-value output - [ ] CLI: `rli benchmark-job list` → verify JSON output by default - [ ] CLI: `rli benchmark-job summary <id>` → verify JSON output by default - [ ] CLI: `rli scenario info <id>` → verify JSON output by default - [ ] CLI: `rli agent show <id>` → verify JSON output by default - [ ] CLI: Upload with duplicate paths → verify error message instead of invalid tar - [ ] CLI: Upload with symlink path → verify symlink stored correctly in tar archive - [x] Type check: `npx tsc --noEmit` passes cleanly 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 419a961 commit 7565d45

48 files changed

Lines changed: 4134 additions & 1029 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,12 +202,14 @@ rli mcp install # Install Runloop MCP server configurat
202202

203203
```bash
204204
rli axon list # List active axons
205+
rli axon events <id> # List events for an axon
205206
```
206207

207208
### Scenario Commands (alias: `scn`)
208209

209210
```bash
210211
rli scenario info <id> # Display scenario definition details
212+
rli scenario list # List scenario runs
211213
```
212214

213215
### Benchmark-job Commands (alias: `bmj`)

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@
8888
"ink-link": "5.0.0",
8989
"ink-spinner": "5.0.0",
9090
"ink-text-input": "6.0.0",
91-
"nanotar": "^0.3.0",
9291
"react": "19.2.0",
92+
"tar-stream": "3.1.7",
9393
"yaml": "2.8.3",
9494
"zustand": "5.0.10"
9595
},
@@ -128,6 +128,7 @@
128128
"@types/jest": "29.5.14",
129129
"@types/node": "22.19.7",
130130
"@types/react": "19.2.10",
131+
"@types/tar-stream": "3.1.4",
131132
"@typescript-eslint/eslint-plugin": "8.54.0",
132133
"@typescript-eslint/parser": "8.54.0",
133134
"esbuild": "0.27.2",

pnpm-lock.yaml

Lines changed: 80 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/commands/agent/create.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ interface CreateOptions {
1515
ref?: string;
1616
objectId?: string;
1717
setupCommands?: string[];
18-
public?: boolean;
1918
output?: string;
2019
}
2120

@@ -105,7 +104,6 @@ export async function createAgentCommand(
105104
const agent = await createAgent({
106105
name: options.name,
107106
...(options.agentVersion ? { version: options.agentVersion } : {}),
108-
...(options.public ? { is_public: true } : {}),
109107
source: { type: sourceType, [sourceType]: sourceOptions },
110108
});
111109

src/commands/agent/show.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export async function showAgentCommand(
4040
): Promise<void> {
4141
try {
4242
const agent = await resolveAgent(idOrName);
43-
output(agent, { format: options.output, defaultFormat: "text" });
43+
output(agent, { format: options.output, defaultFormat: "json" });
4444
} catch (error) {
4545
outputError("Failed to get agent", error);
4646
}

src/commands/axon/events.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* List axon events command
3+
*/
4+
5+
import { listAxonEvents as listAxonEventsService } from "../../services/axonService.js";
6+
import { output, outputError, parseLimit } from "../../utils/output.js";
7+
8+
interface EventsOptions {
9+
limit?: string;
10+
output?: string;
11+
}
12+
13+
export async function listAxonEventsCommand(
14+
axonId: string,
15+
options: EventsOptions,
16+
): Promise<void> {
17+
try {
18+
const parsed = parseLimit(options.limit);
19+
const limit = parsed === Infinity ? 50 : parsed;
20+
const result = await listAxonEventsService(axonId, { limit });
21+
22+
output(result.events, { format: options.output, defaultFormat: "json" });
23+
} catch (error) {
24+
outputError("Failed to get axon events", error);
25+
}
26+
}

src/commands/axon/list.tsx

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import React from "react";
66
import { Box, Text, useInput, useApp } from "ink";
77
import figures from "figures";
8-
import chalk from "chalk";
98
import { formatTimeAgo } from "../../components/ResourceListView.js";
109
import { listActiveAxons, type Axon } from "../../services/axonService.js";
1110
import { output, outputError, parseLimit } from "../../utils/output.js";
@@ -34,45 +33,6 @@ interface ListOptions {
3433

3534
const CLI_PAGE_SIZE = 100;
3635

37-
function printTable(axons: Axon[]): void {
38-
if (axons.length === 0) {
39-
console.log(chalk.dim("No active axons found"));
40-
return;
41-
}
42-
43-
const COL_ID = 34;
44-
const COL_NAME = 28;
45-
const COL_CREATED = 12;
46-
47-
const header =
48-
"ID".padEnd(COL_ID) +
49-
" " +
50-
"NAME".padEnd(COL_NAME) +
51-
" " +
52-
"CREATED".padEnd(COL_CREATED);
53-
console.log(chalk.bold(header));
54-
console.log(chalk.dim("─".repeat(header.length)));
55-
56-
for (const axon of axons) {
57-
const id =
58-
axon.id.length > COL_ID ? axon.id.slice(0, COL_ID - 1) + "…" : axon.id;
59-
const nameRaw = axon.name ?? "";
60-
const name =
61-
nameRaw.length > COL_NAME
62-
? nameRaw.slice(0, COL_NAME - 1) + "…"
63-
: nameRaw;
64-
const created = formatTimeAgo(axon.created_at_ms);
65-
console.log(
66-
`${id.padEnd(COL_ID)} ${name.padEnd(COL_NAME)} ${created.padEnd(COL_CREATED)}`,
67-
);
68-
}
69-
70-
console.log();
71-
console.log(
72-
chalk.dim(`${axons.length} axon${axons.length !== 1 ? "s" : ""}`),
73-
);
74-
}
75-
7636
export async function listAxonsCommand(options: ListOptions): Promise<void> {
7737
try {
7838
const maxResults = parseLimit(options.limit);
@@ -87,14 +47,6 @@ export async function listAxonsCommand(options: ListOptions): Promise<void> {
8747
startingAfter: options.startingAfter,
8848
});
8949
axons = page;
90-
if (format === "text" && hasMore && axons.length > 0) {
91-
console.log(
92-
chalk.dim(
93-
"More results may be available; use --starting-after with the last ID to continue.",
94-
),
95-
);
96-
console.log();
97-
}
9850
} else {
9951
const all: Axon[] = [];
10052
let cursor: string | undefined;

0 commit comments

Comments
 (0)