Skip to content

Commit dbe2a5c

Browse files
jason-rlclaude
andauthored
feat: agent object picker, multi-mount support, and TUI improvements (#217)
## Summary - Add object picker for agent creation with ResourcePicker integration - Multi-agent mount support with validation and parallel resolution - Create devbox from agent detail screen - Fall back to git ref for version display in agent list/detail - Extract mount path utilities to shared module with tests - Various cleanup: remove unnecessary casts, consolidate imports, inline columns, fix delete refresh race Squashed rebase of `jc/agent-object-picker-cli` (`5d0f313`) on top of current main, avoiding the issues caused by the squash-merge of `1481d7a`. ## Test plan - [x] `pnpm build` passes - [x] Lint passes (0 errors) - [ ] Manual TUI testing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b401a38 commit dbe2a5c

29 files changed

Lines changed: 1550 additions & 439 deletions

jest.components.config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ export default {
8484
global: {
8585
branches: 20,
8686
functions: 20,
87-
lines: 30,
88-
statements: 30,
87+
lines: 29,
88+
statements: 29,
8989
},
9090
},
9191

src/commands/agent/create.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { output, outputError } from "../../utils/output.js";
77

88
interface CreateOptions {
99
name: string;
10-
agentVersion: string;
10+
agentVersion?: string;
1111
source: string;
1212
package?: string;
1313
registryUrl?: string;
@@ -103,7 +103,7 @@ export async function createAgentCommand(
103103

104104
const agent = await createAgent({
105105
name: options.name,
106-
version: options.agentVersion,
106+
...(options.agentVersion ? { version: options.agentVersion } : {}),
107107
source: { type: sourceType, [sourceType]: sourceOptions },
108108
});
109109

src/commands/agent/list.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,18 @@ const columns: ColumnDef[] = [
6767
{
6868
header: "VERSION",
6969
raw: (a) => {
70+
if (a.source?.type === "object") return "-";
71+
const v = a.version || a.source?.git?.ref || "";
72+
if (!v) return "-";
7073
const pkg = a.source?.npm?.package_name || a.source?.pip?.package_name;
71-
return pkg ? `${pkg}@${a.version}` : a.version;
74+
return pkg ? `${pkg}@${v}` : v;
7275
},
7376
styled(a) {
77+
if (a.source?.type === "object") return "-";
78+
const v = a.version || a.source?.git?.ref || "";
79+
if (!v) return "-";
7480
const pkg = a.source?.npm?.package_name || a.source?.pip?.package_name;
75-
return pkg ? chalk.dim(pkg + "@") + a.version : a.version;
81+
return pkg ? chalk.dim(pkg + "@") + v : v;
7682
},
7783
},
7884
{
@@ -325,8 +331,9 @@ export const ListAgentsUI = ({
325331
"version",
326332
"Version",
327333
(a: Agent) => {
328-
if (a.source?.type === "object") return "";
329-
const v = a.version || "";
334+
if (a.source?.type === "object") return "-";
335+
const v = a.version || a.source?.git?.ref || "";
336+
if (!v) return "-";
330337
if (v.length > 16) return `${v.slice(0, 8)}${v.slice(-4)}`;
331338
return v;
332339
},
@@ -734,7 +741,7 @@ export const ListAgentsUI = ({
734741
{showPopup && selectedAgentItem && (
735742
<Box marginTop={2} justifyContent="center">
736743
<ActionsPopup
737-
resource={selectedAgentItem}
744+
devbox={selectedAgentItem}
738745
operations={operations.map((op) => ({
739746
key: op.key,
740747
label: op.label,

src/commands/axon/list.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ export const ListAxonsUI = ({
454454
{showPopup && selectedAxonItem && (
455455
<Box marginTop={2} justifyContent="center">
456456
<ActionsPopup
457-
resource={selectedAxonItem}
457+
devbox={selectedAxonItem}
458458
operations={operations.map((op) => ({
459459
key: op.key,
460460
label: op.label,

src/commands/blueprint/list.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -969,7 +969,7 @@ const ListBlueprintsUI = ({
969969
{showPopup && selectedBlueprintItem && (
970970
<Box marginTop={2} justifyContent="center">
971971
<ActionsPopup
972-
resource={selectedBlueprintItem}
972+
devbox={selectedBlueprintItem}
973973
operations={allOperations.map((op) => ({
974974
key: op.key,
975975
label: op.label,

src/commands/devbox/create.ts

Lines changed: 98 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ import {
99
getAgent,
1010
type Agent,
1111
} from "../../services/agentService.js";
12+
import { getObject } from "../../services/objectService.js";
13+
import {
14+
DEFAULT_MOUNT_PATH,
15+
sanitizeMountSegment,
16+
adjustFileExtension,
17+
getDefaultAgentMountPath,
18+
} from "../../utils/mount.js";
1219

1320
interface CreateOptions {
1421
name?: string;
@@ -32,7 +39,7 @@ interface CreateOptions {
3239
gateways?: string[];
3340
mcp?: string[];
3441
agent?: string[];
35-
agentPath?: string;
42+
object?: string[];
3643
output?: string;
3744
}
3845

@@ -293,28 +300,100 @@ export async function createDevbox(options: CreateOptions = {}) {
293300
createRequest.mcp = parseMcpSpecs(options.mcp);
294301
}
295302

296-
// Handle agent mount
303+
// Parse agent mounts (format: name_or_id or name_or_id:/mount/path)
304+
const resolvedAgents: { agent: Agent; path?: string }[] = [];
297305
if (options.agent && options.agent.length > 0) {
298-
if (options.agent.length > 1) {
299-
throw new Error(
300-
"Mounting multiple agents via rli is not supported yet",
301-
);
306+
const parsedAgentSpecs: { idOrName: string; path?: string }[] = [];
307+
for (const spec of options.agent) {
308+
const colonIdx = spec.indexOf(":");
309+
// Only treat colon as separator if what follows looks like an absolute path
310+
if (colonIdx > 0 && spec[colonIdx + 1] === "/") {
311+
parsedAgentSpecs.push({
312+
idOrName: spec.substring(0, colonIdx),
313+
path: spec.substring(colonIdx + 1),
314+
});
315+
} else {
316+
parsedAgentSpecs.push({ idOrName: spec });
317+
}
302318
}
303-
const agent = await resolveAgent(options.agent[0]);
304-
const mount: Record<string, unknown> = {
305-
type: "agent_mount",
306-
agent_id: agent.id,
307-
agent_name: null,
308-
};
309-
// agent_path only makes sense for git and object agents. Since
310-
// we don't know at this stage what type of agent it is,
311-
// however, we'll let the server error inform the user if they
312-
// add this option in a case where it doesn't make sense.
313-
if (options.agentPath) {
314-
mount.agent_path = options.agentPath;
319+
const resolved = await Promise.all(
320+
parsedAgentSpecs.map(async ({ idOrName, path }) => ({
321+
agent: await resolveAgent(idOrName),
322+
path,
323+
})),
324+
);
325+
resolvedAgents.push(...resolved);
326+
}
327+
328+
// Parse object mounts (format: object_id or object_id:/mount/path)
329+
const objectMounts: { object_id: string; object_path: string }[] = [];
330+
if (options.object && options.object.length > 0) {
331+
const parsedObjectSpecs: {
332+
objectId: string;
333+
explicitPath?: string;
334+
}[] = [];
335+
for (const spec of options.object) {
336+
const colonIdx = spec.indexOf(":");
337+
if (colonIdx > 0 && spec[colonIdx + 1] === "/") {
338+
parsedObjectSpecs.push({
339+
objectId: spec.substring(0, colonIdx),
340+
explicitPath: spec.substring(colonIdx + 1),
341+
});
342+
} else {
343+
parsedObjectSpecs.push({ objectId: spec });
344+
}
315345
}
346+
const resolved = await Promise.all(
347+
parsedObjectSpecs.map(async ({ objectId, explicitPath }) => {
348+
if (explicitPath) {
349+
return { object_id: objectId, object_path: explicitPath };
350+
}
351+
// No path specified — fetch object to generate default
352+
const obj = await getObject(objectId);
353+
const name = obj.name;
354+
const contentType = obj.content_type;
355+
if (name) {
356+
const adjusted = adjustFileExtension(name, contentType);
357+
const s = sanitizeMountSegment(adjusted);
358+
const objectPath = s
359+
? `${DEFAULT_MOUNT_PATH}/${s}`
360+
: `${DEFAULT_MOUNT_PATH}/object_${objectId.slice(-8)}`;
361+
return { object_id: objectId, object_path: objectPath };
362+
}
363+
return {
364+
object_id: objectId,
365+
object_path: `${DEFAULT_MOUNT_PATH}/object_${objectId.slice(-8)}`,
366+
};
367+
}),
368+
);
369+
objectMounts.push(...resolved);
370+
}
371+
372+
// Add mounts (agents + objects)
373+
if (resolvedAgents.length > 0 || objectMounts.length > 0) {
316374
if (!createRequest.mounts) createRequest.mounts = [];
317-
(createRequest.mounts as unknown[]).push(mount);
375+
for (const { agent, path } of resolvedAgents) {
376+
const mount: Record<string, unknown> = {
377+
type: "agent_mount",
378+
agent_id: agent.id,
379+
agent_name: null,
380+
};
381+
const sourceType = agent.source?.type;
382+
const needsPath = sourceType === "git" || sourceType === "object";
383+
const effectivePath =
384+
path || (needsPath ? getDefaultAgentMountPath(agent) : undefined);
385+
if (effectivePath) {
386+
mount.agent_path = effectivePath;
387+
}
388+
(createRequest.mounts as unknown[]).push(mount);
389+
}
390+
for (const om of objectMounts) {
391+
(createRequest.mounts as unknown[]).push({
392+
type: "object_mount",
393+
object_id: om.object_id,
394+
object_path: om.object_path,
395+
});
396+
}
318397
}
319398

320399
if (Object.keys(launchParameters).length > 0) {

src/commands/devbox/list.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -797,7 +797,7 @@ const ListDevboxesUI = ({
797797
{showPopup && selectedDevbox && (
798798
<Box marginTop={2} justifyContent="center">
799799
<ActionsPopup
800-
resource={selectedDevbox}
800+
devbox={selectedDevbox}
801801
operations={operations}
802802
selectedOperation={selectedOperation}
803803
onClose={() => setShowPopup(false)}

src/commands/gateway-config/list.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -740,7 +740,7 @@ const ListGatewayConfigsUI = ({
740740
{showPopup && selectedConfigItem && (
741741
<Box marginTop={2} justifyContent="center">
742742
<ActionsPopup
743-
resource={selectedConfigItem}
743+
devbox={selectedConfigItem}
744744
operations={operations.map((op) => ({
745745
key: op.key,
746746
label: op.label,

src/commands/mcp-config/list.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,7 @@ const ListMcpConfigsUI = ({
664664
{showPopup && selectedConfigItem && (
665665
<Box marginTop={2} justifyContent="center">
666666
<ActionsPopup
667-
resource={selectedConfigItem}
667+
devbox={selectedConfigItem}
668668
operations={operations.map((op) => ({
669669
key: op.key,
670670
label: op.label,

src/commands/network-policy/list.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -769,7 +769,7 @@ const ListNetworkPoliciesUI = ({
769769
{showPopup && selectedPolicyItem && (
770770
<Box marginTop={2} justifyContent="center">
771771
<ActionsPopup
772-
resource={selectedPolicyItem}
772+
devbox={selectedPolicyItem}
773773
operations={operations.map((op) => ({
774774
key: op.key,
775775
label: op.label,

0 commit comments

Comments
 (0)