Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ interface ModelSelectorProps {
onReasoningEffortChange?: (value: string) => void;
}

const formatContextLimit = (tokens: number): string => {
export const formatContextLimit = (tokens: number): string => {
if (tokens >= 1_000_000) {
const m = tokens / 1_000_000;
return `${Number.isInteger(m) ? m : m.toFixed(1)}M`;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import type React from "react";
import { ScrollArea } from "#/components/ScrollArea/ScrollArea";
import { ProviderIcon } from "#/pages/AISettingsPage/ProvidersPage/components/ProviderIcon";
import { formatProviderLabel } from "#/utils/aiProviders";
import { formatContextLimit } from "../ModelSelector";
import { ToolCall } from "./ToolCall";
import { asNumber, asRecord, asString, type ToolStatus } from "./utils";

const ModelRow: React.FC<{ model: unknown }> = ({ model }) => {
const rec = asRecord(model);
if (!rec) {
return null;
}
const displayName = asString(rec.display_name).trim();
const modelName = asString(rec.model).trim();
const contextLimit = asNumber(rec.context_limit);
const context =
contextLimit !== undefined && contextLimit > 0
? formatContextLimit(contextLimit)
: "";
const efforts = Array.isArray(rec.reasoning_efforts)
? rec.reasoning_efforts.map(asString).filter((effort) => effort.trim())
: [];
const effortRange =
efforts.length > 1
? `${efforts[0]} - ${efforts[efforts.length - 1]}`
: (efforts[0] ?? "");
const title = displayName || modelName || "Unknown model";

return (
<div className="grid grid-cols-[minmax(0,1fr)_96px_56px] items-center gap-x-2 px-2 py-1">
<span className="min-w-0 truncate text-xs font-medium leading-[18px] text-content-primary">
{title}
</span>
{effortRange ? (
<span className="shrink-0 text-right text-xs font-normal leading-[18px] tabular-nums text-content-secondary">
{effortRange}
</span>
) : (
<span aria-hidden="true" />
)}
{context ? (
<span className="shrink-0 text-right text-xs font-normal leading-[18px] tabular-nums text-content-secondary">
({context})
</span>
) : (
<span aria-hidden="true" />
)}
</div>
);
};

const ListSubagentModelsContent: React.FC<{ models: unknown[] }> = ({
models,
}) => {
// Group by provider like the model picker so models from one provider
// sit under a single headed section instead of a flat list.
const grouped = new Map<string, unknown[]>();
for (const model of models) {
const rec = asRecord(model);
if (!rec) {
continue;
}
const provider = asString(rec.provider).trim() || "unknown";
const group = grouped.get(provider);
if (group) {
group.push(model);
continue;
}
grouped.set(provider, [model]);
}

return (
<ScrollArea
className="mt-1.5 rounded-md border border-solid border-border-default"
viewportClassName="max-h-64"
scrollBarClassName="w-1.5"
>
<div className="px-1 py-1">
{Array.from(grouped.entries(), ([provider, providerModels], index) => (
<div
key={provider}
className={
index > 0
? "border-0 border-t border-solid border-border-default"
: undefined
}
>
<div className="flex items-center gap-1.5 px-2 py-1">
<ProviderIcon provider={provider} className="size-3.5" />
<span className="text-xs font-semibold leading-[18px] text-content-secondary">
{formatProviderLabel(provider)}
</span>
</div>
{providerModels.map((model, modelIndex) => {
const rec = asRecord(model);
const configID = rec ? asString(rec.model_config_id).trim() : "";
return <ModelRow key={configID || modelIndex} model={model} />;
})}
</div>
))}
</div>
</ScrollArea>
);
};

/**
* Collapsed-by-default rendering for `list_subagent_models` tool calls.
* Shows "Listed N subagent models" with a chevron; expanding reveals the
* models accepted by spawn_agent's model_config_id argument, grouped by
* provider like the chat model picker.
*/
export const ListSubagentModelsTool: React.FC<{
models: unknown[];
status: ToolStatus;
isError: boolean;
errorMessage?: string;
}> = ({ models, status, isError, errorMessage }) => {
const hasContent = models.length > 0;
const isRunning = status === "running";

const label = isRunning
? "Listing subagent models…"
: `Listed ${models.length} subagent ${models.length === 1 ? "model" : "models"}`;

return (
<ToolCall.Root
className="max-w-sm"
status={status}
isError={isError}
errorMessage={errorMessage || "Failed to list subagent models"}
hasContent={hasContent}
>
<ToolCall.Header iconName="list_subagent_models" label={label} />
<ToolCall.Content>
<ListSubagentModelsContent models={models} />
</ToolCall.Content>
</ToolCall.Root>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,20 @@ const allToolShowcaseItems: ToolShowcaseItem[] = [
total: 1,
},
},
{
name: "list_subagent_models",
result: {
models: [
{
model_config_id: "model-1",
display_name: "Fast Model",
model: "fast-1",
provider: "openai",
is_default: true,
},
],
},
},
{
name: "read_template",
args: { template_id: "template-1" },
Expand Down Expand Up @@ -1324,6 +1338,117 @@ export const ListAgentsError: Story = {
},
};

// ---------------------------------------------------------------------------
// ListSubagentModels stories
// ---------------------------------------------------------------------------

export const ListSubagentModelsCompleted: Story = {
args: {
name: "list_subagent_models",
status: "completed",
args: {},
result: {
models: [
{
model_config_id: "10000000-0000-0000-0000-000000000001",
display_name: "Fast Model",
model: "fast-1",
provider: "openai",
context_limit: 200_000,
is_default: true,
reasoning_efforts: ["low", "medium", "high"],
},
{
model_config_id: "20000000-0000-0000-0000-000000000002",
display_name: "Large Model",
model: "large-2",
provider: "anthropic",
context_limit: 1_000_000,
is_default: false,
reasoning_efforts: [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh",
"max",
],
},
{
model_config_id: "30000000-0000-0000-0000-000000000003",
display_name: "",
model: "gemini-3.6-flash",
provider: "google",
context_limit: 262_000,
is_default: false,
},
],
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const header = canvas.getByRole("button", {
name: /Listed 3 subagent models/,
});
expect(header).toBeInTheDocument();
await userEvent.click(header);
expect(canvas.getByText("OpenAI")).toBeInTheDocument();
expect(canvas.getByText("Anthropic")).toBeInTheDocument();
expect(canvas.getByText("Google")).toBeInTheDocument();
expect(canvas.getByText("Fast Model")).toBeInTheDocument();
expect(canvas.getByText("(200K)")).toBeInTheDocument();
expect(canvas.getByText("low - high")).toBeInTheDocument();
expect(canvas.getByText("Large Model")).toBeInTheDocument();
expect(canvas.getByText("(1M)")).toBeInTheDocument();
expect(canvas.getByText("none - max")).toBeInTheDocument();
expect(canvas.getByText("(262K)")).toBeInTheDocument();
expect(canvas.getByText("gemini-3.6-flash")).toBeInTheDocument();
},
};

export const ListSubagentModelsRunning: Story = {
args: {
name: "list_subagent_models",
status: "running",
args: {},
result: undefined,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText("Listing subagent models…")).toBeInTheDocument();
},
};

export const ListSubagentModelsEmpty: Story = {
args: {
name: "list_subagent_models",
status: "completed",
args: {},
result: {
models: [],
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText("Listed 0 subagent models")).toBeInTheDocument();
},
};

export const ListSubagentModelsError: Story = {
args: {
name: "list_subagent_models",
status: "error",
isError: true,
args: {},
result: "list_subagent_models is only available on root chats",
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText("Listed 0 subagent models")).toBeInTheDocument();
},
};

// ---------------------------------------------------------------------------
// ListTemplates stories
// ---------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { DiffFileHeader } from "./DiffFileHeader";
import { EditFilesTool } from "./EditFilesTool";
import { ExecuteTool as ExecuteToolComponent } from "./ExecuteTool";
import { ListAgentsTool } from "./ListAgentsTool";
import { ListSubagentModelsTool } from "./ListSubagentModelsTool";
import { ListTemplatesTool } from "./ListTemplatesTool";
import { ProcessOutputTool } from "./ProcessOutputTool";
import { ProposePlanTool } from "./ProposePlanTool";
Expand Down Expand Up @@ -573,6 +574,24 @@ const ListAgentsRenderer: FC<ToolRendererProps> = ({
);
};

const ListSubagentModelsRenderer: FC<ToolRendererProps> = ({
status,
result,
isError,
}) => {
const rec = asRecord(result);
const models = rec && Array.isArray(rec.models) ? rec.models : [];

return (
<ListSubagentModelsTool
models={models}
status={status}
isError={isError}
errorMessage={rec ? asString(rec.error || rec.message) : undefined}
Comment thread
DanielleMaywood marked this conversation as resolved.
/>
);
};

const ReadTemplateRenderer: FC<ToolRendererProps> = ({
status,
result,
Expand Down Expand Up @@ -1024,6 +1043,7 @@ export const toolRenderers: Record<string, FC<ToolRendererProps>> = {
start_workspace: StartWorkspaceRenderer,
list_templates: ListTemplatesRenderer,
list_agents: ListAgentsRenderer,
list_subagent_models: ListSubagentModelsRenderer,
read_template: ReadTemplateRenderer,
read_skill: ReadSkillRenderer,
read_skill_file: ReadSkillFileRenderer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const toolIcons: Partial<Record<string, LucideIcon>> = {
start_workspace: PowerIcon,
chat_summarized: BotIcon,
list_agents: BotIcon,
list_subagent_models: BotIcon,
thinking: LightbulbIcon,
propose_plan: RouteIcon,
ask_user_question: BadgeQuestionMarkIcon,
Expand Down
Loading