added model picker

This commit is contained in:
Davit
2026-04-25 01:52:24 +04:00
parent 2d732c282c
commit 7ce858f81b
13 changed files with 814 additions and 15 deletions
+18
View File
@@ -171,6 +171,24 @@ export interface AgentBudgetResponse {
export type AgentBudgetPatch = Partial<Record<AgentBudgetKey, number | null>>;
export interface AgentProviderModel {
key: string;
name: string;
contextWindow: number | null;
local: boolean;
available: boolean;
missing: boolean;
tags: string[];
}
export interface AgentProviderModelsResponse {
agentId: string;
known: boolean;
currentModel: string | null;
provider: string | null;
models: AgentProviderModel[];
}
// ── Agent skills (per-agent allowlist) ──
export interface AgentSkillSummary {
+46
View File
@@ -462,6 +462,50 @@ const updateSubagentsConfig: RequestHandler = async (req, res, next) => {
}
};
const getProviderModels: RequestHandler = async (req, res, next) => {
try {
const agentRepo = AppDataSource.getRepository(Agent);
const agent = await agentRepo.findOneBy({ _id: Number(req.params.id) });
if (!agent?.openclawAgentId) {
return res.status(404).json({ error: 'Agent not found' });
}
return res.json(ocService.getAgentProviderModels(agent.openclawAgentId));
} catch (error) {
return next(error);
}
};
const updateProviderModel: RequestHandler = async (req, res, next) => {
try {
const agentRepo = AppDataSource.getRepository(Agent);
const convRepo = AppDataSource.getRepository(Conversation);
const agent = await agentRepo.findOneBy({ _id: Number(req.params.id) });
if (!agent?.openclawAgentId) {
return res.status(404).json({ error: 'Agent not found' });
}
const body = (req.body ?? {}) as { model?: string; conversationId?: number | string };
const modelKey = typeof body.model === 'string' ? body.model : '';
let sessionKey: string | null = null;
if (body.conversationId !== undefined && body.conversationId !== null) {
const conv = await convRepo.findOneBy({ _id: Number(body.conversationId) });
sessionKey = conv?.sessionKey || (conv ? String(conv._id) : null);
}
const result = await ocService.setAgentProviderModel(
agent.openclawAgentId,
modelKey,
sessionKey
);
if (!result.ok) return res.status(400).json(result);
return res.json(result);
} catch (error) {
return next(error);
}
};
const serveWorkspaceUpload: RequestHandler = async (req, res, next) => {
try {
const agentRepo = AppDataSource.getRepository(Agent);
@@ -497,4 +541,6 @@ export {
updateSkillsConfig,
getSubagentsConfig,
updateSubagentsConfig,
getProviderModels,
updateProviderModel,
};
+139
View File
@@ -397,6 +397,74 @@ paths:
$ref: '#/components/responses/422'
500:
$ref: '#/components/responses/500'
/agent/{id}/provider-models:
get:
tags:
- agent
security:
- bearerAuth: []
operationId: getAgentProviderModels
summary: List models for the agent's currently configured provider
description: >
Returns the agent's current model and every model exposed by that
provider (derived from the `provider/model` prefix). Use to power a
scoped model picker — no provider switching is offered because that
would require different credentials.
parameters:
- $ref: '#/components/parameters/id'
responses:
200:
description: Provider-scoped model catalog
content:
application/json:
schema:
$ref: '#/components/schemas/agentProviderModelsResponse'
404:
description: Agent not found
401:
$ref: '#/components/responses/401'
500:
$ref: '#/components/responses/500'
patch:
tags:
- agent
security:
- bearerAuth: []
operationId: updateAgentProviderModel
summary: Hot-swap the agent's model (same provider)
description: >
Updates the agent's configured model via the Gateway RPC `agents.update`
so the change takes effect on the next turn without a gateway restart.
When `conversationId` is provided, any pinned `modelOverride` on that
session is cleared via `sessions.patch` so the running chat picks up
the new default immediately.
parameters:
- $ref: '#/components/parameters/id'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/agentProviderModelPatch'
responses:
200:
description: Updated model configuration
content:
application/json:
schema:
$ref: '#/components/schemas/agentProviderModelMutationResponse'
400:
description: Model update failed
content:
application/json:
schema:
$ref: '#/components/schemas/agentProviderModelMutationResponse'
401:
$ref: '#/components/responses/401'
422:
$ref: '#/components/responses/422'
500:
$ref: '#/components/responses/500'
/agent/{id}/skills:
get:
tags:
@@ -710,6 +778,77 @@ components:
config:
$ref: '#/components/schemas/agentBudgetResponse'
#
agentProviderModel:
type: object
description: A single model entry surfaced by `openclaw models list --provider <id>`.
properties:
key:
type: string
description: Full model key (e.g. `google/gemini-2.5-pro`).
name:
type: string
contextWindow:
type: integer
nullable: true
local:
type: boolean
available:
type: boolean
description: Whether the agent currently has credentials for this provider.
missing:
type: boolean
tags:
type: array
items:
type: string
#
agentProviderModelsResponse:
type: object
properties:
agentId:
type: string
known:
type: boolean
currentModel:
type: string
nullable: true
provider:
type: string
nullable: true
description: Provider id derived from the `provider/model` prefix.
models:
type: array
items:
$ref: '#/components/schemas/agentProviderModel'
#
agentProviderModelPatch:
type: object
required:
- model
properties:
model:
type: string
description: Full model key (must belong to the agent's current provider).
conversationId:
type: integer
nullable: true
description: When provided, also clears any session override so the change applies mid-chat.
#
agentProviderModelMutationResponse:
type: object
properties:
ok:
type: boolean
error:
type: string
nullable: true
restartHint:
type: string
nullable: true
description: Present when a gateway restart or new conversation is required.
config:
$ref: '#/components/schemas/agentProviderModelsResponse'
#
agentSkillSummary:
type: object
properties:
+5
View File
@@ -36,6 +36,11 @@ router
.get(auth, validate.id, controller.getBudget)
.patch(auth, validate.budgetPatch, controller.updateBudget);
router
.route('/agent/:id(\\d+)/provider-models')
.get(auth, validate.id, controller.getProviderModels)
.patch(auth, validate.providerModelPatch, controller.updateProviderModel);
router
.route('/agent/:id(\\d+)/conversation/:conversationId(\\d+)/session-settings')
.get(auth, controller.getSessionSettings)
+23
View File
@@ -142,4 +142,27 @@ export default {
return true;
}),
]),
providerModelPatch: validate([
param('id').isInt().withMessage('Incorrect request url'),
body().custom((value) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error('Body must be an object.');
}
const v = value as Record<string, unknown>;
const allowed = ['model', 'conversationId'];
const unknown = Object.keys(v).find((k) => !allowed.includes(k));
if (unknown) throw new Error(`Unknown field: ${unknown}`);
if (typeof v.model !== 'string' || !v.model.trim()) {
throw new Error('"model" must be a non-empty string.');
}
if (v.conversationId !== undefined && v.conversationId !== null) {
const n = Number(v.conversationId);
if (!Number.isInteger(n) || n <= 0) {
throw new Error('"conversationId" must be a positive integer when provided.');
}
}
return true;
}),
]),
};
@@ -0,0 +1,194 @@
/* eslint-disable no-console */
import fs from 'fs';
import os from 'os';
import {
AgentProviderModel,
AgentProviderModelsResponse,
OpenclawAgentEntry,
OpenclawConfig,
} from '../../@types/openclaw';
import { gateway, ocExec } from '../openclawGateway';
import { openclawConfigPath } from './paths';
import { errMsg, execErrText } from '../../utils/errors';
const CLI_OPTS = {
cwd: os.homedir(),
env: { ...process.env, NO_COLOR: '1' } as NodeJS.ProcessEnv,
timeout: 20000,
};
function readConfig(): OpenclawConfig | null {
try {
return JSON.parse(fs.readFileSync(openclawConfigPath(), 'utf-8')) as OpenclawConfig;
} catch {
return null;
}
}
function findAgentIndex(config: OpenclawConfig | null, openclawAgentId: string): number {
const list = config?.agents?.list ?? [];
return list.findIndex((a) => a?.id === openclawAgentId);
}
function extractModelString(entry: OpenclawAgentEntry | undefined): string | null {
const m = entry?.model;
if (!m) return null;
if (typeof m === 'string') return m;
if (typeof m === 'object') return m.primary ?? null;
return null;
}
/** `provider/model` → `provider`. Returns null when slashless (local, alias, etc.). */
function parseProvider(modelKey: string | null): string | null {
if (!modelKey) return null;
const idx = modelKey.indexOf('/');
return idx > 0 ? modelKey.slice(0, idx) : null;
}
interface OcModelsListRow {
key?: string;
name?: string;
contextWindow?: number;
input?: string;
local?: boolean;
available?: boolean;
missing?: boolean;
tags?: string[];
}
/**
* Per-provider memoisation of the CLI catalog. `openclaw models list --all
* --provider <id> --json` costs ~1.52s per invocation and the result is
* effectively static per OpenClaw build, so we cache by provider for a short
* window. Model swaps mutate config but never the catalog, so no explicit
* invalidation is needed — the TTL covers both "new build installed" and
* "user configured a new alias" cases.
*/
const PROVIDER_MODELS_TTL_MS = 5 * 60 * 1000;
const providerModelsCache = new Map<string, { at: number; models: AgentProviderModel[] }>();
function listProviderModelsViaCli(provider: string): AgentProviderModel[] {
const cached = providerModelsCache.get(provider);
if (cached && Date.now() - cached.at < PROVIDER_MODELS_TTL_MS) {
return cached.models;
}
try {
const out = ocExec(['models', 'list', '--provider', provider, '--all', '--json'], {
...CLI_OPTS,
encoding: 'utf-8',
});
const parsed = JSON.parse(out) as { models?: OcModelsListRow[] };
const rows = Array.isArray(parsed?.models) ? parsed.models : [];
const models = rows
.filter((r): r is OcModelsListRow & { key: string } => typeof r?.key === 'string' && !!r.key)
.map((r) => ({
key: r.key,
name: typeof r.name === 'string' && r.name ? r.name : r.key,
contextWindow: typeof r.contextWindow === 'number' ? r.contextWindow : null,
local: !!r.local,
available: !!r.available,
missing: !!r.missing,
tags: Array.isArray(r.tags) ? r.tags.filter((t): t is string => typeof t === 'string') : [],
}))
.sort((a, b) => a.key.localeCompare(b.key));
providerModelsCache.set(provider, { at: Date.now(), models });
return models;
} catch (err) {
console.error('[agent-provider-models] list failed:', execErrText(err));
return cached?.models ?? [];
}
}
export function getAgentProviderModels(openclawAgentId: string): AgentProviderModelsResponse {
const config = readConfig();
const agentIndex = findAgentIndex(config, openclawAgentId);
const entry = agentIndex >= 0 ? config?.agents?.list?.[agentIndex] : undefined;
const currentModel =
extractModelString(entry) ??
(typeof config?.agents?.defaults?.model?.primary === 'string'
? (config.agents.defaults.model.primary as string)
: null);
const provider = parseProvider(currentModel);
return {
agentId: openclawAgentId,
known: agentIndex >= 0,
currentModel,
provider,
models: provider ? listProviderModelsViaCli(provider) : [],
};
}
export interface SetAgentProviderModelResult {
ok: boolean;
error?: string;
restartHint?: string | null;
config?: AgentProviderModelsResponse;
}
/**
* Hot-apply a model change for the given agent. Uses the Gateway RPC
* `agents.update` so the daemon's in-memory registry picks it up, then clears
* any pinned `modelOverride` on the active session (if any) via
* `sessions.patch { key, model: null }` so the next turn honours the new
* agent default. Falls back to the CLI when the gateway is unreachable.
*/
export async function setAgentProviderModel(
openclawAgentId: string,
modelKey: string,
sessionKey: string | null
): Promise<SetAgentProviderModelResult> {
const config = readConfig();
const agentIndex = findAgentIndex(config, openclawAgentId);
if (agentIndex < 0) {
return { ok: false, error: `Agent "${openclawAgentId}" not found in openclaw config.` };
}
const trimmed = modelKey.trim();
if (!trimmed) {
return { ok: false, error: '"model" must be a non-empty string.' };
}
let restartHint: string | null = null;
let wroteViaGateway = false;
const gwReady = await gateway.ensureConnected();
if (gwReady) {
try {
await gateway.request(
'agents.update',
{ agentId: openclawAgentId, model: trimmed },
{ timeoutMs: 5000 }
);
wroteViaGateway = true;
} catch (err) {
console.warn('[agent-provider-models] agents.update failed, falling back:', errMsg(err));
}
}
if (!wroteViaGateway) {
try {
const cfgPath = `agents.list[${agentIndex}].model`;
ocExec(['config', 'set', cfgPath, JSON.stringify(trimmed), '--strict-json'], CLI_OPTS);
restartHint = 'Restart the gateway or start a new conversation to apply.';
} catch (err) {
const stderr = execErrText(err);
return { ok: false, error: stderr || 'Failed to update agent model.' };
}
}
if (wroteViaGateway && sessionKey) {
try {
const fullKey = `agent:${openclawAgentId}:${sessionKey}`;
await gateway.request('sessions.patch', { key: fullKey, model: null }, { timeoutMs: 5000 });
} catch (err) {
restartHint = `Session override could not be cleared: ${errMsg(err)}. Start a new conversation to apply.`;
}
}
return {
ok: true,
restartHint,
config: getAgentProviderModels(openclawAgentId),
};
}
+1
View File
@@ -30,6 +30,7 @@ export {
copyFileToWorkspace,
} from './workspace';
export { getAgentModel, getAgentModelsForOpenclawIds } from './config';
export { getAgentProviderModels, setAgentProviderModel } from './agentProviderModels';
export { getAgentBudget, setAgentBudget, BUDGET_FIELDS } from './budget';
export { getAgentSkillsConfig, setAgentSkills } from './agentSkills';
export { getAgentSubagentsConfig, setAgentSubagents } from './agentSubagents';
+45
View File
@@ -126,6 +126,31 @@ export interface AgentSubagentsMutationResponse {
config?: AgentSubagentsResponse;
}
export interface AgentProviderModel {
key: string;
name: string;
contextWindow: number | null;
local: boolean;
available: boolean;
missing: boolean;
tags: string[];
}
export interface AgentProviderModelsResponse {
agentId: string;
known: boolean;
currentModel: string | null;
provider: string | null;
models: AgentProviderModel[];
}
export interface AgentProviderModelMutationResponse {
ok: boolean;
error?: string;
restartHint?: string | null;
config?: AgentProviderModelsResponse;
}
export const WORKSPACE_TAB_FILES = [
{ label: 'AGENTS', file: 'AGENTS.md' },
{ label: 'SOUL', file: 'SOUL.md' },
@@ -273,6 +298,24 @@ export const agentsApi = baseApi.injectEndpoints({
}),
invalidatesTags: (_res, _err, { agentId }) => [{ type: 'AgentSubagents', id: agentId }],
}),
getAgentProviderModels: build.query<AgentProviderModelsResponse, string>({
query: (agentId) => `/agent/${agentId}/provider-models`,
providesTags: (_res, _err, agentId) => [{ type: 'AgentProviderModels', id: agentId }],
}),
updateAgentProviderModel: build.mutation<
AgentProviderModelMutationResponse,
{ agentId: string; model: string; conversationId?: number | string }
>({
query: ({ agentId, model, conversationId }) => ({
url: `/agent/${agentId}/provider-models`,
method: 'PATCH',
body: { model, ...(conversationId !== undefined ? { conversationId } : {}) },
}),
invalidatesTags: (_res, _err, { agentId }) => [
{ type: 'AgentProviderModels', id: agentId },
'Agent',
],
}),
}),
});
@@ -294,4 +337,6 @@ export const {
useUpdateAgentSkillsMutation,
useGetAgentSubagentsQuery,
useUpdateAgentSubagentsMutation,
useGetAgentProviderModelsQuery,
useUpdateAgentProviderModelMutation,
} = agentsApi;
@@ -0,0 +1 @@
export { default as AgentModelPicker } from './ui/AgentModelPicker';
@@ -0,0 +1,318 @@
import { useRef, useState } from 'react';
import {
Box,
ButtonBase,
Chip,
CircularProgress,
Divider,
Menu,
MenuItem,
Tooltip,
Typography,
} from '@mui/material';
import { CheckCircle, ExpandMore, WarningAmber } from '@mui/icons-material';
import {
useGetAgentProviderModelsQuery,
useUpdateAgentProviderModelMutation,
type AgentProviderModel,
} from '../../../../entities/agent';
interface AgentModelPickerProps {
agentId: string;
currentModel?: string | null;
conversationId?: number | string;
}
function stripProvider(key: string): string {
const idx = key.indexOf('/');
return idx > 0 ? key.slice(idx + 1) : key;
}
function parseProvider(key: string | null | undefined): string | null {
if (!key) return null;
const idx = key.indexOf('/');
return idx > 0 ? key.slice(0, idx) : null;
}
function labelFor(model: AgentProviderModel | null | undefined, fallback: string): string {
if (!model) return fallback;
return model.name || stripProvider(model.key);
}
/** `1000000` → `1M`, `272000` → `272k`, `4096` → `4k`. */
function formatCtx(n: number): string {
if (n >= 1_000_000) {
const m = n / 1_000_000;
return `${m % 1 === 0 ? m.toFixed(0) : m.toFixed(1)}M`;
}
if (n >= 1000) return `${Math.round(n / 1000)}k`;
return String(n);
}
export default function AgentModelPicker({
agentId,
currentModel,
conversationId,
}: AgentModelPickerProps) {
const anchorRef = useRef<HTMLButtonElement | null>(null);
const [open, setOpen] = useState(false);
const [pendingKey, setPendingKey] = useState<string | null>(null);
const [hint, setHint] = useState<string | null>(null);
const [prevAgentId, setPrevAgentId] = useState(agentId);
if (agentId !== prevAgentId) {
setPrevAgentId(agentId);
if (open) setOpen(false);
if (pendingKey !== null) setPendingKey(null);
if (hint !== null) setHint(null);
}
const { currentData, isFetching } = useGetAgentProviderModelsQuery(agentId, {
skip: !agentId || !open,
});
const [update, { isLoading: saving }] = useUpdateAgentProviderModelMutation();
const provider = parseProvider(currentModel);
if (!agentId || !currentModel || !provider) return null;
const data = currentData && currentData.provider === provider ? currentData : null;
const effectiveCurrent = data?.currentModel ?? currentModel;
const matched = data?.models.find((m) => m.key === effectiveCurrent) ?? null;
const currentLabel = labelFor(matched, stripProvider(effectiveCurrent));
async function handlePick(model: AgentProviderModel) {
if (model.key === effectiveCurrent || saving) return;
setPendingKey(model.key);
setHint(null);
try {
const result = await update({ agentId, model: model.key, conversationId }).unwrap();
setHint(result.restartHint ?? null);
setOpen(false);
} catch (err) {
const msg =
err && typeof err === 'object' && 'data' in err
? ((err as { data?: { error?: string } }).data?.error ?? 'Failed to change model.')
: 'Failed to change model.';
setHint(msg);
} finally {
setPendingKey(null);
}
}
return (
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.5 }}>
<Tooltip title={`Switch ${provider} model`} placement="bottom-start">
<ButtonBase
ref={anchorRef}
onClick={() => setOpen(true)}
disabled={saving}
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.25,
px: 0.75,
py: 0.25,
borderRadius: 1,
border: '1px solid',
borderColor: 'divider',
bgcolor: 'action.hover',
color: 'text.secondary',
fontSize: '0.7rem',
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
transition: 'all 0.12s',
'&:hover': {
bgcolor: 'action.selected',
color: 'text.primary',
},
'&.Mui-disabled': {
opacity: 0.6,
},
}}
>
<Box
component="span"
sx={{ color: 'text.disabled', textTransform: 'lowercase', mr: 0.25 }}
>
{provider}
</Box>
<Box component="span">{currentLabel}</Box>
{saving ? (
<CircularProgress size={10} sx={{ ml: 0.5 }} />
) : (
<ExpandMore sx={{ fontSize: 14, ml: 0.25 }} />
)}
</ButtonBase>
</Tooltip>
{hint && (
<Tooltip title={hint}>
<WarningAmber sx={{ fontSize: 14, color: 'warning.main' }} />
</Tooltip>
)}
<Menu
anchorEl={anchorRef.current}
open={open}
onClose={() => setOpen(false)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
slotProps={{
paper: {
sx: {
width: 340,
mt: 0.5,
borderRadius: 1,
border: '1px solid',
borderColor: 'divider',
bgcolor: (theme) =>
theme.palette.mode === 'dark' ? theme.palette.grey[900] : theme.palette.grey[100],
backgroundImage: 'none',
},
},
list: {
sx: {
py: 0,
maxHeight: 420,
overflowY: 'auto',
'&::-webkit-scrollbar': { width: 6 },
'&::-webkit-scrollbar-thumb': {
bgcolor: 'divider',
borderRadius: 3,
},
},
},
}}
>
{isFetching && !data && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, px: 2, py: 2 }}>
<CircularProgress size={14} />
<Typography variant="caption" color="text.secondary">
Loading {provider} models
</Typography>
</Box>
)}
{data && data.models.length === 0 && !isFetching && (
<Box sx={{ px: 2, py: 2 }}>
<Typography variant="caption" color="text.secondary">
No {provider} models available.
</Typography>
</Box>
)}
{data?.models.map((m, idx) => {
const selected = m.key === effectiveCurrent;
const isPending = pendingKey === m.key;
const displayName = labelFor(m, stripProvider(m.key));
return (
<Box key={m.key}>
{idx > 0 && <Divider sx={{ borderStyle: 'dashed', opacity: 0.5 }} />}
<MenuItem
onClick={() => void handlePick(m)}
selected={selected}
disabled={saving}
sx={{
px: 1.5,
py: 1,
alignItems: 'center',
gap: 1.25,
bgcolor: 'transparent',
transition: 'background-color 0.12s',
'&:hover': {
bgcolor: 'action.selected',
},
'&.Mui-selected': {
bgcolor: 'action.selected',
'&:hover': {
bgcolor: 'action.selected',
},
},
}}
>
<Box sx={{ minWidth: 0, flex: 1 }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.75,
minWidth: 0,
}}
>
<Typography
variant="body2"
sx={{
fontWeight: selected ? 600 : 500,
color: selected ? 'success.main' : 'text.primary',
lineHeight: 1.25,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
minWidth: 0,
}}
>
{displayName}
</Typography>
{selected && (
<CheckCircle sx={{ fontSize: 14, color: 'success.main', flexShrink: 0 }} />
)}
</Box>
<Typography
variant="caption"
sx={{
display: 'block',
color: 'text.disabled',
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
fontSize: '0.66rem',
lineHeight: 1.4,
mt: 0.25,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{m.key}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexShrink: 0 }}>
{m.local && (
<Chip
label="local"
size="small"
variant="outlined"
sx={{
height: 18,
fontSize: '0.62rem',
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: 0.5,
borderColor: 'divider',
color: 'text.secondary',
'& .MuiChip-label': { px: 0.75 },
}}
/>
)}
{m.contextWindow ? (
<Chip
label={formatCtx(m.contextWindow)}
size="small"
sx={{
height: 20,
fontSize: '0.66rem',
fontWeight: 600,
bgcolor: 'action.hover',
color: 'text.secondary',
'& .MuiChip-label': { px: 0.9 },
}}
/>
) : null}
{isPending && <CircularProgress size={14} sx={{ ml: 0.5 }} />}
</Box>
</MenuItem>
</Box>
);
})}
</Menu>
</Box>
);
}
+1
View File
@@ -80,6 +80,7 @@ export const baseApi = createApi({
'AgentBudget',
'AgentSkills',
'AgentSubagents',
'AgentProviderModels',
],
endpoints: () => ({}),
});
+22 -14
View File
@@ -1,8 +1,9 @@
import { useState } from 'react';
import { Box, TextField, IconButton, Typography, CircularProgress } from '@mui/material';
import { Box, TextField, IconButton, Typography, CircularProgress, Stack } from '@mui/material';
import { Edit, Check, Settings, TuneOutlined } from '@mui/icons-material';
import { Link } from 'react-router';
import { useGetAgentQuery, useUpdateAgentMutation } from '../../../entities/agent';
import { AgentModelPicker } from '../../../features/agent/provider-model';
interface ChatHeaderProps {
agentId: string;
@@ -98,19 +99,26 @@ export default function ChatHeader({
</>
) : (
<>
<Typography
variant="h6"
fontWeight={600}
sx={{
flex: 1,
minWidth: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{agent.name}
</Typography>
<Stack sx={{ flex: 1, minWidth: 0 }} spacing={0.25}>
<Typography
variant="h6"
fontWeight={600}
sx={{
minWidth: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
lineHeight: 1.2,
}}
>
{agent.name}
</Typography>
<AgentModelPicker
agentId={agentId}
currentModel={agent.model ?? null}
conversationId={conversationId}
/>
</Stack>
<IconButton
size="small"
onClick={onToggleSessionSettings}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "openclaw-client",
"version": "2.4.5",
"version": "2.4.6",
"description": "Web-based chat interface for OpenClaw AI agents",
"private": true,
"type": "module",